¿Orden de resolución del método (MRO) en clases de nuevo estilo?
En el libro Python in a Nutshell (2.ª edición) hay un ejemplo que utiliza
clases de estilo antiguo para demostrar cómo se resuelven los métodos en el orden de resolución clásico y
en qué se diferencia con el nuevo orden.
Probé el mismo ejemplo reescribiéndolo en un estilo nuevo, pero el resultado no es diferente al obtenido con las clases de estilo antiguo. La versión de Python que estoy usando para ejecutar el ejemplo es 2.5.2. A continuación se muestra el ejemplo:
class Base1(object):
def amethod(self): print "Base1"
class Base2(Base1):
pass
class Base3(object):
def amethod(self): print "Base3"
class Derived(Base2,Base3):
pass
instance = Derived()
instance.amethod()
print Derived.__mro__
La llamada instance.amethod()
se imprime Base1
, pero según mi comprensión del MRO con un nuevo estilo de clases, el resultado debería haber sido Base3
. La llamada Derived.__mro__
imprime:
(<class '__main__.Derived'>, <class '__main__.Base2'>, <class '__main__.Base1'>, <class '__main__.Base3'>, <type 'object'>)
No estoy seguro de si mi comprensión de MRO con nuevas clases de estilo es incorrecta o si estoy cometiendo un error tonto que no puedo detectar. Por favor ayúdenme a comprender mejor MRO.
La diferencia crucial entre el orden de resolución para las clases heredadas y las de nuevo estilo surge cuando la misma clase antecesora ocurre más de una vez en el enfoque "ingenuo" y de profundidad primero; por ejemplo, considere un caso de "herencia de diamante":
>>> class A: x = 'a'
...
>>> class B(A): pass
...
>>> class C(A): x = 'c'
...
>>> class D(B, C): pass
...
>>> D.x
'a'
aquí, en el estilo heredado, el orden de resolución es D - B - A - C - A: por lo tanto, al buscar Dx, A es la primera base en el orden de resolución para resolverlo, ocultando así la definición en C. Mientras que:
>>> class A(object): x = 'a'
...
>>> class B(A): pass
...
>>> class C(A): x = 'c'
...
>>> class D(B, C): pass
...
>>> D.x
'c'
>>>
aquí, nuevo estilo, el orden es:
>>> D.__mro__
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>,
<class '__main__.A'>, <type 'object'>)
con A
forzado a aparecer en orden de resolución solo una vez y después de todas sus subclases, de modo que las anulaciones (es decir, la anulación de member por parte de C x
) realmente funcionen de manera sensata.
Es una de las razones por las que se deben evitar las clases de estilo antiguo: la herencia múltiple con patrones "similares a diamantes" simplemente no funciona de manera sensata con ellas, mientras que sí lo hace con las de estilo nuevo.
El orden de resolución del método de Python es en realidad más complejo que simplemente comprender el patrón de diamante. Para entenderlo realmente , eche un vistazo a la linealización C3 . Descubrí que es realmente útil utilizar declaraciones impresas al ampliar los métodos para realizar un seguimiento del pedido. Por ejemplo, ¿cuál crees que sería el resultado de este patrón? (Nota: se supone que la 'X' son dos bordes que se cruzan, no un nodo y ^ significa métodos que llaman a super())
class G():
def m(self):
print("G")
class F(G):
def m(self):
print("F")
super().m()
class E(G):
def m(self):
print("E")
super().m()
class D(G):
def m(self):
print("D")
super().m()
class C(E):
def m(self):
print("C")
super().m()
class B(D, E, F):
def m(self):
print("B")
super().m()
class A(B, C):
def m(self):
print("A")
super().m()
# A^
# / \
# B^ C^
# /| X
# D^ E^ F^
# \ | /
# G
¿Recibiste ABDCEFG?
x = A()
x.m()
Después de muchas pruebas y errores, se me ocurrió una interpretación informal de la teoría de grafos de la linealización C3 de la siguiente manera: (Alguien, por favor, avíseme si esto está mal).
Considere este ejemplo:
class I(G):
def m(self):
print("I")
super().m()
class H():
def m(self):
print("H")
class G(H):
def m(self):
print("G")
super().m()
class F(H):
def m(self):
print("F")
super().m()
class E(H):
def m(self):
print("E")
super().m()
class D(F):
def m(self):
print("D")
super().m()
class C(E, F, G):
def m(self):
print("C")
super().m()
class B():
def m(self):
print("B")
super().m()
class A(B, C, D):
def m(self):
print("A")
super().m()
# Algorithm:
# 1. Build an inheritance graph such that the children point at the parents (you'll have to imagine the arrows are there) and
# keeping the correct left to right order. (I've marked methods that call super with ^)
# A^
# / | \
# / | \
# B^ C^ D^ I^
# / | \ / /
# / | X /
# / |/ \ /
# E^ F^ G^
# \ | /
# \ | /
# H
# (In this example, A is a child of B, so imagine an edge going FROM A TO B)
# 2. Remove all classes that aren't eventually inherited by A
# A^
# / | \
# / | \
# B^ C^ D^
# / | \ /
# / | X
# / |/ \
# E^ F^ G^
# \ | /
# \ | /
# H
# 3. For each level of the graph from bottom to top
# For each node in the level from right to left
# Remove all of the edges coming into the node except for the right-most one
# Remove all of the edges going out of the node except for the left-most one
# Level {H}
#
# A^
# / | \
# / | \
# B^ C^ D^
# / | \ /
# / | X
# / |/ \
# E^ F^ G^
# |
# |
# H
# Level {G F E}
#
# A^
# / | \
# / | \
# B^ C^ D^
# | \ /
# | X
# | | \
# E^F^ G^
# |
# |
# H
# Level {D C B}
#
# A^
# /| \
# / | \
# B^ C^ D^
# | |
# | |
# | |
# E^ F^ G^
# |
# |
# H
# Level {A}
#
# A^
# |
# |
# B^ C^ D^
# | |
# | |
# | |
# E^ F^ G^
# |
# |
# H
# The resolution order can now be determined by reading from top to bottom, left to right. A B C E D F G H
x = A()
x.m()