¿Cómo cambiar dinámicamente la clase base de instancias en tiempo de ejecución?

Este artículo tiene un fragmento que muestra el uso de __bases__ para cambiar dinámicamente la jerarquía de herencia de algunos códigos de Python, agregando una clase a una colección de clases de clases existente de la que se hereda. Ok, eso es difícil de leer, el código es probablemente más claro:

 class Friendly: def hello(self): print 'Hello' class Person: pass p = Person() Person.__bases__ = (Friendly,) p.hello() # prints "Hello" 

Es decir, Person no hereda de Friendly en el nivel de origen, sino que esta relación de herencia se agrega dinámicamente en tiempo de ejecución mediante la modificación del atributo __bases__ de la clase Person. Sin embargo, si cambia Friendly y Person para que sean nuevas clases de estilo (al heredar de un objeto), obtendrá el siguiente error:

 TypeError: __bases__ assignment: 'Friendly' deallocator differs from 'object' 

Un poco de Google en este sentido parece indicar algunas incompatibilidades entre las clases de estilo nuevo y antiguo en lo que respecta a cambiar la jerarquía de herencia en tiempo de ejecución. Específicamente: “Los objetos de clase de estilo nuevo no admiten la asignación a su atributo de base ” .

Mi pregunta, ¿es posible hacer que el ejemplo Friendly / Person anterior funcione con clases de estilo nuevo en Python 2.7+, posiblemente mediante el uso del atributo __mro__ ?

Descargo de responsabilidad: me doy cuenta de que este es un código oscuro. Me doy cuenta completamente de que en los códigos de producción reales, los trucos como este tienden a ser ilegibles, esto es puramente un experimento mental y para que los entusiastas aprendan algo sobre cómo Python trata los problemas relacionados con la herencia múltiple.

Bien, una vez más, esto no es algo que normalmente debería hacer, solo con fines informativos.

Donde Python busca un método en un objeto de instancia está determinado por el atributo __mro__ de la clase que define ese objeto (el atributo de solución de solución de método). Por lo tanto, si pudiéramos modificar el __mro__ de Person , obtendríamos el comportamiento deseado. Algo como:

 setattr(Person, '__mro__', (Person, Friendly, object)) 

El problema es que __mro__ es un atributo de solo lectura, y por lo tanto setattr no funcionará. Tal vez si eres un gurú de Python hay una forma de evitar eso, pero claramente no alcanzo el estatus de gurú ya que no puedo pensar en uno.

Una posible solución es simplemente redefinir la clase:

 def modify_Person_to_be_friendly(): # so that we're modifying the global identifier 'Person' global Person # now just redefine the class using type(), specifying that the new # class should inherit from Friendly and have all attributes from # our old Person class Person = type('Person', (Friendly,), dict(Person.__dict__)) def main(): modify_Person_to_be_friendly() p = Person() p.hello() # works! 

Lo que esto no hace es modificar las instancias de Person creadas previamente para tener el método hello() . Por ejemplo (solo modificando main() ):

 def main(): oldperson = Person() ModifyPersonToBeFriendly() p = Person() p.hello() # works! But: oldperson.hello() # does not 

Si los detalles de la llamada de type no están claros, entonces lea la excelente respuesta de e-satis en ‘¿Qué es una metaclase en Python?’ .

También he estado luchando con esto, y su solución me intrigó, pero Python 3 nos lo quita:

 AttributeError: attribute '__dict__' of 'type' objects is not writable 

De hecho, tengo una necesidad legítima de un decorador que reemplace la superclase (individual) de la clase decorada. Requeriría una descripción demasiado larga para incluir aquí (lo intenté, pero no pude obtener una complejidad razonable y limitada), surgió en el contexto del uso por parte de muchas aplicaciones Python de un servidor empresarial basado en Python donde diferentes aplicaciones necesitaban variaciones ligeramente diferentes de algunos de los códigos.)

La discusión en esta página y otras similares proporcionaron indicios de que el problema de asignar a __bases__ solo se produce para las clases sin superclase definida (es decir, cuya única superclase es el objeto). Pude resolver este problema (para Python 2.7 y 3.2) definiendo las clases cuya superclase necesitaba reemplazar como subclases de una clase trivial:

 ## T is used so that the other classes are not direct subclasses of object, ## since classes whose base is object don't allow assignment to their __bases__ attribute. class T: pass class A(T): def __init__(self): print('Creating instance of {}'.format(self.__class__.__name__)) ## ordinary inheritance class B(A): pass ## dynamically specified inheritance class C(T): pass A() # -> Creating instance of A B() # -> Creating instance of B C.__bases__ = (A,) C() # -> Creating instance of C ## attempt at dynamically specified inheritance starting with a direct subclass ## of object doesn't work class D: pass D.__bases__ = (A,) D() ## Result is: ## TypeError: __bases__ assignment: 'A' deallocator differs from 'object' 

No puedo responder por las consecuencias, pero este código hace lo que quiere en py2.7.2.

 class Friendly(object): def hello(self): print 'Hello' class Person(object): pass # we can't change the original classes, so we replace them class newFriendly: pass newFriendly.__dict__ = dict(Friendly.__dict__) Friendly = newFriendly class newPerson: pass newPerson.__dict__ = dict(Person.__dict__) Person = newPerson p = Person() Person.__bases__ = (Friendly,) p.hello() # prints "Hello" 

Sabemos que esto es posible. Guay. ¡Pero nunca lo usaremos!

A la derecha del bate, todas las advertencias de alterar dinámicamente la jerarquía de clases están vigentes.

Pero si hay que hacerlo, entonces, aparentemente, hay un problema que trata el problema de "deallocator differs from 'object" issue when modifying the __bases__ attribute para las nuevas clases de estilo.

Puedes definir un objeto de clase.

 class Object(object): pass 

Que deriva una clase del type metaclase incorporado. Eso es todo, ahora sus nuevas clases de estilo pueden modificar __bases__ sin ningún problema.

En mis pruebas, esto realmente funcionó muy bien, ya que todas las instancias existentes (antes de cambiar la herencia) de la misma y sus clases derivadas sintieron el efecto del cambio, incluido su actualización de mro .

Necesitaba una solución para esto que:

  • Funciona con Python 2 (> = 2.7) y Python 3 (> = 3.2).
  • Permite cambiar las bases de clase después de importar dinámicamente una dependencia.
  • Permite cambiar las bases de clase del código de prueba de unidad.
  • Funciona con tipos que tienen una metaclase personalizada.
  • Todavía permite que unittest.mock.patch funcione como se espera.

Esto es lo que se me ocurrió:

 def ensure_class_bases_begin_with(namespace, class_name, base_class): """ Ensure the named class's bases start with the base class. :param namespace: The namespace containing the class name. :param class_name: The name of the class to alter. :param base_class: The type to be the first base class for the newly created type. :return: ``None``. Call this function after ensuring `base_class` is available, before using the class named by `class_name`. """ existing_class = namespace[class_name] assert isinstance(existing_class, type) bases = list(existing_class.__bases__) if base_class is bases[0]: # Already bound to a type with the right bases. return bases.insert(0, base_class) new_class_namespace = existing_class.__dict__.copy() # Type creation will assign the correct '__dict__' attribute. del new_class_namespace['__dict__'] metaclass = existing_class.__metaclass__ new_class = metaclass(class_name, tuple(bases), new_class_namespace) namespace[class_name] = new_class 

Utilizado así dentro de la aplicación:

 # foo.py # Type `Bar` is not available at first, so can't inherit from it yet. class Foo(object): __metaclass__ = type def __init__(self): self.frob = "spam" def __unicode__(self): return "Foo" # … later … import bar ensure_class_bases_begin_with( namespace=globals(), class_name=str('Foo'), # `str` type differs on Python 2 vs. 3. base_class=bar.Bar) 

Utilice así desde el código de prueba de unidad:

 # test_foo.py """ Unit test for `foo` module. """ import unittest import mock import foo import bar ensure_class_bases_begin_with( namespace=foo.__dict__, class_name=str('Foo'), # `str` type differs on Python 2 vs. 3. base_class=bar.Bar) class Foo_TestCase(unittest.TestCase): """ Test cases for `Foo` class. """ def setUp(self): patcher_unicode = mock.patch.object( foo.Foo, '__unicode__') patcher_unicode.start() self.addCleanup(patcher_unicode.stop) self.test_instance = foo.Foo() patcher_frob = mock.patch.object( self.test_instance, 'frob') patcher_frob.start() self.addCleanup(patcher_frob.stop) def test_instantiate(self): """ Should create an instance of `Foo`. """ instance = foo.Foo() 

Las respuestas anteriores son buenas si necesita cambiar una clase existente en tiempo de ejecución. Sin embargo, si solo está buscando crear una nueva clase que hereda alguna otra clase, existe una solución mucho más limpia. Obtuve esta idea de https://stackoverflow.com/a/21060094/3533440 , pero creo que el siguiente ejemplo ilustra mejor un caso de uso legítimo.

 def make_default(Map, default_default=None): """Returns a class which behaves identically to the given Map class, except it gives a default value for unknown keys.""" class DefaultMap(Map): def __init__(self, default=default_default, **kwargs): self._default = default super().__init__(**kwargs) def __missing__(self, key): return self._default return DefaultMap DefaultDict = make_default(dict, default_default='wug') d = DefaultDict(a=1, b=2) assert d['a'] is 1 assert d['b'] is 2 assert d['c'] is 'wug' 

Corríjame si me equivoco, pero esta estrategia me parece muy legible y la usaría en el código de producción. Esto es muy similar a los funtores en OCaml.