python: super () – como el objeto proxy que inicia la búsqueda MRO en una clase específica

Según los documentos, devuelve super(cls, obj)

un objeto proxy que delega las llamadas de método a una clase padre o hermana de tipo cls

Entiendo por qué super() ofrece esta funcionalidad, pero necesito algo ligeramente diferente: necesito crear un objeto proxy que delegue las llamadas de métodos (y las búsquedas de atributos) a la clase cls ; y como en super , si cls no implementa el método / atributo, mi proxy debe continuar buscando en el orden MRO (de la nueva clase no de la original ). ¿Hay alguna función que pueda escribir que logre eso?

Ejemplo:

 class X: def act(): #... class Y: def act(): #... class A(X, Y): def act(): #... class B(X, Y): def act(): #... class C(A, B): def act(): #... c = C() b = some_magic_function(B, c) # `b` needs to delegate calls to `act` to B, and look up attribute `s` in B # I will pass `b` somewhere else, and have no control over it 

Por supuesto, podría hacer b = super(A, c) , pero eso se basa en conocer la jerarquía de clases exacta y el hecho de que B sigue a A en el MRO. Se rompería silenciosamente si alguna de estas dos suposiciones cambiara en el futuro. (Tenga en cuenta que super no hace tales suposiciones!)

Si solo b.act() llamar a b.act() , podría usar B.act(c) . Pero estoy pasando b a otra persona, y no tengo idea de qué harán con eso. Necesito asegurarme de que no me traicione y comenzar a actuar como una instancia de class C en algún momento.

Una pregunta aparte, la documentación para super() (en Python 3.2) solo habla de su delegación de métodos, y no aclara que las búsquedas de atributos para el proxy también se realizan de la misma manera. ¿Es una omisión accidental?

EDITAR

El enfoque de delegado actualizado funciona también en el siguiente ejemplo:

 class A: def f(self): print('A.f') def h(self): print('A.h') self.f() class B(A): def g(self): self.f() print('B.g') def f(self): print('B.f') def t(self): super().h() a_true = A() # instance of A ends up executing Af a_true.h() b = B() a_proxy = Delegate(A, b) # *unlike* super(), the updated `Delegate` implementation would call Af, not Bf a_proxy.h() 

Tenga en cuenta que el class Delegate actualizada está más cerca de lo que quiero que de super() por dos razones:

  1. super() solo lo hace para la primera llamada; Las llamadas subsiguientes ocurrirán normalmente, ya que para entonces se usa el objeto, no su proxy.

  2. super() no permite el acceso de atributos.

Por lo tanto, mi pregunta tal como se formula tiene una respuesta (casi) perfecta en Python.

Resulta que, en un nivel superior, estaba tratando de hacer algo que no debía ( vea mis comentarios aquí ).

Esta clase debe cubrir los casos más comunes:

 class Delegate: def __init__(self, cls, obj): self._delegate_cls = cls self._delegate_obj = obj def __getattr__(self, name): x = getattr(self._delegate_cls, name) if hasattr(x, "__get__"): return x.__get__(self._delegate_obj) return x 

Úsalo así:

 b = Delegate(B, c) 

(con los nombres de su código de ejemplo.)

Restricciones:

  1. No puede recuperar algunos atributos especiales como __class__ etc. de la clase que pasa en el constructor a través de este proxy. (Estas restituciones también se aplican a super .)

  2. Esto podría comportarse de manera flexible si el atributo que desea recuperar es algún tipo de descriptor de tipo de conexión.

Editar : Si desea que el código en la actualización de su pregunta funcione como lo desea, puede usar el código de seguimiento:

 class Delegate: def __init__(self, cls): self._delegate_cls = cls def __getattr__(self, name): x = getattr(self._delegate_cls, name) if hasattr(x, "__get__"): return x.__get__(self) return x 

Esto pasa el objeto proxy como self parámetro self a cualquier método llamado, y no necesita el objeto original, por lo tanto, lo eliminé del constructor.

Si también desea que los atributos de instancia sean accesibles, puede usar esta versión:

 class Delegate: def __init__(self, cls, obj): self._delegate_cls = cls self._delegate_obj = obj def __getattr__(self, name): if name in vars(self._delegate_obj): return getattr(self._delegate_obj, name) x = getattr(self._delegate_cls, name) if hasattr(x, "__get__"): return x.__get__(self) return x 

Una pregunta aparte, la documentación para super () (en Python 3.2) solo habla de su delegación de métodos, y no aclara que las búsquedas de atributos para el proxy también se realizan de la misma manera. ¿Es una omisión accidental?

No, esto no es accidental. super() no hace nada para las búsquedas de atributos. La razón es que los atributos en una instancia no están asociados con una clase en particular, simplemente están ahí. Considera lo siguiente:

 class A: def __init__(self): self.foo = 'foo set from A' class B(A): def __init__(self): super().__init__() self.bar = 'bar set from B' class C(B): def method(self): self.baz = 'baz set from C' class D(C): def __init__(self): super().__init__() self.foo = 'foo set from D' self.baz = 'baz set from D' instance = D() instance.method() instance.bar = 'not set from a class at all' 

¿Qué clase “posee” foo , bar y baz ?

Si quisiera ver la instance como una instancia de C, ¿debería tener un atributo baz antes de llamar al method ? ¿Qué tal después?

Si veo la instance como una instancia de A, ¿qué valor debería tener foo ? ¿Debería ser invisible la bar porque solo se agregó en B, o visible porque se estableció en un valor fuera de la clase?

Todas estas preguntas no tienen sentido en Python. No hay forma posible de diseñar un sistema con la semántica de Python que pueda darles respuestas sensatas. __init__ ni siquiera es especial en términos de agregar atributos a las instancias de la clase; es solo un método perfectamente ordinario que se llama como parte del protocolo de creación de instancias. Cualquier método (o incluso el código de otra clase, o no de ninguna clase) puede crear atributos en cualquier instancia a la que tenga una referencia.

De hecho, todos los atributos de la instance se almacenan en el mismo lugar:

 >>> instance.__dict__ {'baz': 'baz set from C', 'foo': 'foo set from D', 'bar': 'not set from a class at all'} 

No hay manera de saber cuál de ellos se estableció originalmente por qué clase, o se estableció por última vez por qué clase, o cualquier medida de propiedad que desee. Ciertamente, no hay manera de llegar a “el A.foo siendo ensombrecido por D.foo “, como cabría esperar de C ++; son el mismo atributo, y cualquier escritura en él por una clase (o de otra parte) anulará un valor dejado por la otra clase.

La consecuencia de esto es que super() no realiza búsquedas de atributos de la misma manera que hace búsquedas de métodos; No puede, y tampoco puede escribir ningún código.


De hecho, al ejecutar algunos experimentos, ¡ni el super ni el Delegate de Sven realmente admiten la recuperación directa de atributos!

 class A: def __init__(self): self.spoon = 1 self.fork = 2 def foo(self): print('A.foo') class B(A): def foo(self): print('B.foo') b = B() d = Delegate(A, b) s = super(B, b) 

Entonces ambos funcionan como se espera para los métodos:

 >>> d.foo() A.foo >>> s.foo() A.foo 

Pero:

 >>> d.fork Traceback (most recent call last): File "", line 1, in  d.fork File "/tmp/foo.py", line 6, in __getattr__ x = getattr(self._delegate_cls, name) AttributeError: type object 'A' has no attribute 'fork' >>> s.spoon Traceback (most recent call last): File "", line 1, in  s.spoon AttributeError: 'super' object has no attribute 'spoon' 

Por lo tanto, ambos realmente solo trabajan para activar algunos métodos, no para pasar a un código de terceros arbitrario para pretender ser una instancia de la clase a la que desea delegar.

Desafortunadamente, no se comportan de la misma manera en presencia de herencia múltiple. Dado:

 class Delegate: def __init__(self, cls, obj): self._delegate_cls = cls self._delegate_obj = obj def __getattr__(self, name): x = getattr(self._delegate_cls, name) if hasattr(x, "__get__"): return x.__get__(self._delegate_obj) return x class A: def foo(self): print('A.foo') class B: pass class C(B, A): def foo(self): print('C.foo') c = C() d = Delegate(B, c) s = super(C, c) 

Entonces:

 >>> d.foo() Traceback (most recent call last): File "", line 1, in  d.foo() File "/tmp/foo.py", line 6, in __getattr__ x = getattr(self._delegate_cls, name) AttributeError: type object 'B' has no attribute 'foo' >>> s.foo() A.foo 

Debido a que Delegate ignora el MRO completo de cualquier clase, _delegate_obj es una instancia de, solo se usa el MRO de _delegate_cls . Mientras que super hace lo que preguntaste en la pregunta, pero el comportamiento parece bastante extraño: no es una envoltura de una instancia de C pretender que es una instancia de B, porque las instancias directas de B no tienen foo definido.

Aquí está mi bash:

 class MROSkipper: def __init__(self, cls, obj): self.__cls = cls self.__obj = obj def __getattr__(self, name): mro = self.__obj.__class__.__mro__ i = mro.index(self.__cls) if i == 0: # It's at the front anyway, just behave as getattr return getattr(self.__obj, name) else: # Check __dict__ not getattr, otherwise we'd find methods # on classes we're trying to skip try: return self.__obj.__dict__[name] except KeyError: return getattr(super(mro[i - 1], self.__obj), name) 

Confío en el atributo __mro__ de las clases para averiguar correctamente por dónde empezar, luego solo uso super . Usted podría caminar por la cadena de MRO desde ese punto revisando la clase __dict__ s en busca de métodos, si la rareza de retroceder un paso para usar super es demasiado.

No he intentado manejar atributos inusuales; los implementados con descriptores (incluidas las propiedades), o los métodos mágicos buscados detrás de la escena por Python, que a menudo comienzan en la clase en lugar de la instancia directamente. Pero esto se comporta como lo has pedido moderadamente bien (con la advertencia expuesta en la primera parte de mi publicación; buscar atributos de esta manera no te dará ningún resultado diferente que buscarlos directamente en la instancia).