¿Orden de resolución de método (MRO) en clases de nuevo estilo?


94

En el libro Python in a Nutshell (2nd Edition) hay un ejemplo que usa
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 diferencian con el nuevo orden.

Probé el mismo ejemplo reescribiendo el ejemplo con un nuevo estilo, 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 se instance.amethod()imprime Base1, pero según mi comprensión del MRO con un nuevo estilo de clases, la salida 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. Ayúdenme a comprender mejor el MRO.

Respuestas:


184

La diferencia crucial entre el orden de resolución para las clases heredadas y las clases de estilo nuevo se produce cuando la misma clase de antepasados ​​aparece más de una vez en el enfoque "ingenuo", que prioriza la profundidad; por ejemplo, considere un caso de "herencia de diamantes":

>>> class A: x = 'a'
... 
>>> class B(A): pass
... 
>>> class C(A): x = 'c'
... 
>>> class D(B, C): pass
... 
>>> D.x
'a'

aquí, al estilo heredado, el orden de resolución es D - B - A - C - A: por lo que al buscar Dx, A es la primera base en la resolución para resolverlo, ocultando así la definición en C.

>>> class A(object): x = 'a'
... 
>>> class B(A): pass
... 
>>> class C(A): x = 'c'
... 
>>> class D(B, C): pass
... 
>>> D.x
'c'
>>> 

aquí, estilo nuevo, el orden es:

>>> D.__mro__
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, 
    <class '__main__.A'>, <type 'object'>)

con Aforzado a venir 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 miembro de C x) realmente funcionen con sensatez.

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 ellos, mientras que sí lo hace con el estilo nuevo.


2
"[la clase antecesora] A [está] obligada a venir en orden de resolución solo una vez y después de todas sus subclases, de modo que las invalidaciones (es decir, la invalidación de C del miembro x) realmente funcionen con sensatez". - ¡Epifanía! Gracias a esta frase, puedo volver a hacer MRO en mi cabeza. \ o / Muchas gracias.
Esteis

23

El orden de resolución del método de Python es en realidad más complejo que solo comprender el patrón de diamante. Para entenderlo realmente , eche un vistazo a la linealización C3 . Descubrí que realmente ayuda usar declaraciones de impresión al extender métodos para rastrear el pedido. Por ejemplo, ¿cuál cree 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()

Debe corregir su segundo código: ha puesto la clase "I" como primera línea y también ha utilizado super para encontrar la superclase "G" pero "I" es de primera clase, por lo que nunca podrá encontrar la clase "G" porque no no hay "G" superior "I". Ponga la clase "I" entre "G" y "F" :)
Aaditya Ura

El código de ejemplo es incorrecto. superha requerido argumentos.
danny

2
Dentro de una definición de clase, super () no requiere argumentos. Ver https://docs.python.org/3/library/functions.html#super
Ben

Tu teoría de grafos es innecesariamente complicada. Después del paso 1, inserte los bordes de las clases de la izquierda a las clases de la derecha (en cualquier lista de herencia), y luego haga una clasificación topológica y listo.
Kevin

@Kevin No creo que eso sea correcto. Siguiendo mi ejemplo, ¿no sería ACDBEFGH una clasificación topológica válida? Pero ese no es el orden de resolución.
Ben

5

El resultado que obtienes es correcto. Prueba a cambiar la clase base Base3para Base1y comparar con la misma jerarquía de clases clásicas:

class Base1(object):
    def amethod(self): print "Base1"

class Base2(Base1):
    pass

class Base3(Base1):
    def amethod(self): print "Base3"

class Derived(Base2,Base3):
    pass

instance = Derived()
instance.amethod()


class Base1:
    def amethod(self): print "Base1"

class Base2(Base1):
    pass

class Base3(Base1):
    def amethod(self): print "Base3"

class Derived(Base2,Base3):
    pass

instance = Derived()
instance.amethod()

Ahora sale:

Base3
Base1

Lea esta explicación para obtener más información.


1

Estás viendo ese comportamiento porque la resolución del método es primero en profundidad, no en ancho. La herencia de Dervied parece

         Base2 -> Base1
        /
Derived - Base3

Entonces instance.amethod()

  1. Comprueba Base2, no encuentra un método.
  2. Ve que Base2 ha heredado de Base1 y comprueba Base1. Base1 tiene una amethod, por lo que se llama.

Esto se refleja en Derived.__mro__. Simplemente repita Derived.__mro__y deténgase cuando encuentre el método que está buscando.


Dudo que la razón por la que obtengo "Base1" como respuesta sea porque la resolución del método es primero en profundidad, creo que hay más que un enfoque en profundidad primero. Vea el ejemplo de Denis, si fuera profundidad primero o / p debería haber sido "Base1". Consulte también el primer ejemplo en el enlace que ha proporcionado, allí también el MRO que se muestra indica que la resolución del método no solo se determina atravesando en primer orden de profundidad.
sateesh

Lo sentimos, el enlace al documento sobre MRO es proporcionado por Denis. Por favor verifique eso, me equivoqué con que me proporcionó el enlace a python.org.
sateesh

4
Generalmente es la profundidad primero, pero hay inteligencia para manejar la herencia similar a un diamante, como explicó Alex.
jamessan
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.