¿Cómo funciona realmente el super () de Python, en el caso general?

Hay una gran cantidad de excelentes recursos en super() , incluida esta gran publicación de blog que aparece mucho, así como muchas preguntas sobre el desbordamiento de stack. Sin embargo, siento que todos ellos no llegan a explicar cómo funciona en el caso más general (con gráficos de herencia arbitrarios), así como lo que está sucediendo debajo del capó.

Considere este ejemplo básico de la herencia de diamante:

 class A(object): def foo(self): print 'A foo' class B(A): def foo(self): print 'B foo before' super(B, self).foo() print 'B foo after' class C(A): def foo(self): print 'C foo before' super(C, self).foo() print 'C foo after' class D(B, C): def foo(self): print 'D foo before' super(D, self).foo() print 'D foo after' 

Si lee las reglas de Python para el orden de resolución de métodos de fonts como esta o busca en la página de wikipedia la linealización C3, verá que el MRO debe ser (D, B, C, A, object) . Por supuesto, esto está confirmado por D.__mro__ :

 (, , , , ) 

Y

 d = D() d.foo() 

huellas dactilares

 D foo before B foo before C foo before A foo C foo after B foo after D foo after 

que coincide con el MRO. Sin embargo, super(B, self).foo() en cuenta que el super(B, self).foo() en B en realidad llama a C.foo , mientras que en b = B(); b.foo() b = B(); b.foo() simplemente iría directamente a A.foo . Claramente, el uso de super(B, self).foo() no es simplemente un atajo para A.foo(self) como a veces se enseña.

super() es obviamente consciente de las llamadas anteriores y del MRO general que la cadena está tratando de seguir. Puedo ver dos maneras en que esto podría lograrse. Lo primero es hacer algo como pasar el super objeto en sí mismo como el argumento self al siguiente método de la cadena, que actuaría como el objeto original pero también contendría esta información. Sin embargo, esto también parece que rompería muchas cosas ( super(D, d) is d es falso) y al hacer un poco de experimentación puedo ver que este no es el caso.

La otra opción es tener algún tipo de contexto global que almacene el MRO y la posición actual en él. Me imagino que el algoritmo para super va algo como:

  1. ¿Hay actualmente un contexto en el que estamos trabajando? Si no, crea uno que contenga una cola. Obtenga el MRO para el argumento de clase, empuje todos los elementos excepto el primero en la cola.
  2. Extraiga el siguiente elemento de la cola MRO del contexto actual, utilícelo como la clase actual cuando construya la super instancia.
  3. Cuando se accede a un método desde la super instancia, búsquelo en la clase actual y llámelo usando el mismo contexto.

Sin embargo, esto no tiene en cuenta cosas extrañas, como el uso de una clase base diferente como el primer argumento de una llamada a super , o incluso llamar a un método diferente. Me gustaría saber el algoritmo general para esto. Además, si este contexto existe en algún lugar, ¿puedo inspeccionarlo? ¿Puedo meterme con eso? Una idea terrible, por supuesto, pero Python normalmente espera que seas un adulto maduro, incluso si no lo eres.

Esto también introduce una gran cantidad de consideraciones de diseño. Si escribí B pensando solo en su relación con A , luego alguien más escribe C y una tercera persona escribe D , mi método B.foo() tiene que llamar a super de una manera que sea compatible con C.foo() aunque ¡No existía en el momento en que lo escribí! Si quiero que mi clase sea fácilmente extensible, tendré que explicar esto, pero no estoy seguro de que sea más complicado que simplemente asegurarme de que todas las versiones de foo tengan firmas idénticas. También está la cuestión de cuándo colocar el código antes o después de la llamada a super , incluso si no hay ninguna diferencia teniendo en cuenta únicamente las clases base de B

super () es, obviamente, consciente de las llamadas anteriores antes de que

No es. Cuando haces super(B, self).foo , super conoce el MRO porque es solo type(self).__mro__ , y sabe que debería comenzar a buscar foo en el punto en el MRO inmediatamente después de B Un equivalente puro en python puro sería

 class super(object): def __init__(self, klass, obj): self.klass = klass self.obj = obj def __getattr__(self, attrname): classes = iter(type(self.obj).__mro__) # search the MRO to find self.klass for klass in classes: if klass is self.klass: break # start searching for attrname at the next class after self.klass for klass in classes: if attrname in klass.__dict__: attr = klass.__dict__[attrname] break else: raise AttributeError # handle methods and other descriptors try: return attr.__get__(self.obj, type(self.obj)) except AttributeError: return attr 

Si escribí B pensando solo en su relación con A, luego alguien más escribe C y una tercera persona escribe D, mi método B.foo () tiene que llamar a super de una manera que sea compatible con C. foo () aunque ¡No existía en el momento en que lo escribí!

No hay ninguna expectativa de que pueda ser capaz de heredar múltiples de clases arbitrarias. A menos que foo esté específicamente diseñado para ser sobrecargado por clases de hermanos en una situación de herencia múltiple, D no debería existir.