2012-03-02 19 views
55

This article tiene un fragmento que muestra el uso de __bases__ para cambiar dinámicamente la jerarquía de herencia de algún código de Python, agregando una clase a una colección de clases existente de las clases de las que hereda. Ok, eso es difícil de leer, el código es probablemente más claro:¿Cómo cambiar dinámicamente la clase base de instancias en tiempo de ejecución?

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 la fuente, sino que se añade esta relación de herencia dinámicamente en tiempo de ejecución mediante la modificación del atributo __bases__ de la clase Persona. Sin embargo, si cambia Friendly y Person a ser nuevas clases de estilo (heredando de objeto), se obtiene el siguiente error:

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

Un poco de google sobre esto parece indicate some incompatibilities entre el nuevo estilo y las clases de estilo antiguo en lo que respecta a cambiar la jerarquía de herencia en tiempo de ejecución. Específicamente: "New-style class objects don't support assignment to their bases attribute".

Mi pregunta, ¿es posible hacer que el ejemplo amistoso/persona anterior utilice clases de estilo nuevo en Python 2.7+, posiblemente mediante el uso del atributo __mro__?

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

+0

También es bueno para los estudiantes leer esto si no están familiarizados con metaclass, tipo(), ...: http://www.slideshare.net/gwiener/metaclasses-in-python :) – loolooyyyy

+0

Aquí está mi uso caso. Estoy importando una biblioteca que tiene clase B que hereda de la clase A. – FutureNerd

+2

Aquí está mi caso de uso real. Estoy importando una biblioteca que tiene clase B heredando de la clase A. Quiero crear New_A heredando desde A, con new_A_method(). Ahora quiero crear New_B heredando de ... bueno, de B como si B heredara de New_A, de modo que los métodos de B, los métodos de A, y new_A_method() estén todos disponibles para las instancias de New_B. ¿Cómo puedo hacer esto sin parche de monos en la clase A existente? – FutureNerd

Respuesta

29

Bien, de nuevo, esto no es algo que deba hacer normalmente, esto es sólo para fines informativos.

Dónde Python busca un método en un objeto de instancia se determina por el atributo de la clase __mro__ que define ese objeto (la M é todo R eSolution O atributo rder). Por lo tanto, si pudiéramos modificar el __mro__ de Person, obtendríamos el comportamiento deseado. Algo así como:

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

El problema es que __mro__ es un atributo de sólo lectura, y por lo tanto setattr no va a funcionar. Tal vez si eres un gurú de Python hay una forma de evitarlo, 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 cualquier Person instancias creadas previamente para tener el método hello().Por ejemplo (simplemente modificando main()):

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

Si los detalles de la llamada type no están claras, a continuación, lea e-satis' excellent answer on 'What is a metaclass in Python?'.

+0

-1: por qué forzar a la nueva clase a heredar de Frielndly solo cuando se podía preservar. el original '__mro__' al llamar:' tipo ('Persona', (Amigable) + Persona .__ mro__, dict (Persona .__ dict__)) '(y mejor aún, agregue protecciones para que los Amigos no terminen dos veces allí). hay otros problemas aquí: en cuanto a dónde se define y usa realmente la clase "Persona": su función solo la cambia en el módulo actual; otros módulos que ejecuten Person no se verán afectados; será mejor que realice un parche mono en el módulo. definido. (e incluso hay problemas) – jsbueno

+6

'-1' En primer lugar, se ha perdido el motivo de la excepción. Puedes felizmente modificar 'Person .__ class __.__ bases__' en Python 2 y 3, siempre y cuando' Person' no herede 'object' ** directamente **. Vea las respuestas de akaRem y Sam Gulve a continuación. Esta solución solo está solucionando su propia incomprensión del problema. –

7

No puedo responder a 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!

13

He estado luchando con esto también, y estaba intrigado por su solución, pero Python 3 se lo lleva lejos de nosotros:

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

que en realidad tienen una necesidad legítima de un decorador que sustituye a la (única) superclase de la clase decorada. Se requeriría una descripción demasiado larga para incluirla aquí (lo intenté, pero no pude lograrlo con una complejidad razonablemente larga y limitada; surgió en el contexto del uso de muchas aplicaciones de Python de un servidor empresarial basado en Python donde diferentes aplicaciones necesitaban variaciones ligeramente diferentes de parte del código.)

La discusión en esta página y en otras similares proporcionó indicios de que el problema de la asignación a __bases__ solo ocurre para las clases sin superclase definida (es decir, cuya única superclase es objeto). Yo era capaz de resolver este problema (tanto para Python 2.7 y 3.2) mediante la definición de las clases cuya superclase que necesitaba para reemplazar como son 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' 
3

derecho del bate, todas las salvedades de Messing con jerarquía de clase dinámicamente están en efecto.

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

Se puede definir un objeto de clase

class Object(object): pass 

que se deriva una clase a partir de la incorporada en el metaclase type. Eso es todo, ahora sus nuevas clases de estilo pueden modificar el __bases__ sin ningún problema.

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

1

necesitaba una solución para esto que:

  • funciona tanto 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 la unidad.
  • Funciona con tipos que tienen una metaclase personalizada.
  • Todavía permite que unittest.mock.patch funcione como se esperaba.

Esto es lo que 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 

Se utiliza como esto 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) 

uso como esto 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() 
0

Las respuestas anteriores son buenas si necesita cambiar una clase existente en tiempo de ejecución. Sin embargo, si solo busca crear una clase nueva que herede de alguna otra clase, existe una solución mucho más limpia. Obtuve esta idea en 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ígeme si me equivoco, pero esta estrategia parece muy legible para mí, y lo utilizaría en el código de producción. Esto es muy similar a functors en OCaml.

+1

No estoy seguro de qué tiene que ver esto con la pregunta, ya que la pregunta realmente era acerca de cambiar dinámicamente las clases base en tiempo de ejecución. En cualquier caso, ¿cuál es la ventaja de esto simplemente heredando '' 'Map''' directamente y anulando los métodos según sea necesario (muy parecido a lo que hace el' '' collections.defaultdict''' estándar)? Tal como está, '' 'make_default''' solo puede devolver un tipo de cosa, así que ¿por qué no simplemente hacer' '' DefaultMap''' el identificador de nivel superior en lugar de tener que llamar '' 'make_default''' para obtener el clase para instanciar? –

+0

¡Gracias por los comentarios! (1) Aterricé aquí mientras trataba de hacer una herencia dinámica, así que pensé que podría ayudar a alguien que siguiera mi mismo camino. (2) Creo que se podría usar 'make_default' para crear una versión predeterminada de algún otro tipo de clase similar a un diccionario (' Map'). No se puede heredar de 'Map' directamente porque' Map' no se define hasta el tiempo de ejecución. En este caso, querría heredar de 'dict' directamente, pero imaginamos que podría haber un caso en el que no sabría de qué tipo de diccionario heredaría hasta el tiempo de ejecución. – fredcallaway

Cuestiones relacionadas