orientada - llamar metodo de otra clase python
¿Cómo cambiar dinámicamente la clase base de instancias en tiempo de ejecución? (6)
Este artículo 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, al agregar una clase a una colección de clases existente de las clases que 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, la 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 (heredando el objeto), obtendrá el siguiente error:
TypeError: __bases__ assignment: ''Friendly'' deallocator differs from ''object''
Un poco de Google en esto parece indicar algunas incompatibilidades entre las clases de estilo antiguo 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 nuevo estilo no son compatibles con la asignación a sus atributos base " .
Mi pregunta, ¿es posible hacer que el ejemplo anterior de Amigo / Persona funcione usando 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.
De acuerdo, una vez más, esto no es algo que normalmente debería hacer, esto es sólo para 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 M étod o ). 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 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 no hace es modificar cualquier instancia de Person
creada previamente para que tenga 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 de type
no están claros, entonces lea la excelente respuesta de e-satis en ''¿Qué es una metaclase en Python?'' .
Derecho del bate, todas las advertencias de jugar con la jerarquía de clase dinámicamente están en efecto.
Pero si tiene que hacerse, entonces, al parecer, hay un truco que "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 él y sus clases derivadas sintieron el efecto del cambio, incluyendo la actualización de su mro
.
He estado luchando con esto también, y estaba intrigado con tu solución, pero Python 3 nos la quita:
AttributeError: attribute ''__dict__'' of ''type'' objects is not writable
De hecho, tengo una necesidad legítima de un decorador que reemplace la superclase (única) 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 asignar a __bases__
solo ocurre para las clases que no tienen una superclase definida (es decir, cuya única superclase es el objeto). Pude resolver este problema (para Python 2.7 y 3.2) al definir 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''
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 de https://.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 me parece muy legible y la usaría en el código de producción. Esto es muy similar a functors en OCaml.
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 la unidad.
- Funciona con tipos que tienen una metaclase personalizada.
- Aún 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
Usado 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)
Use esto desde dentro del código de prueba de la 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()
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!