Method Resolution Order (MRO) nelle classi di nuovo stile?


94

Nel libro Python in a Nutshell (2a edizione) c'è un esempio che usa
classi vecchio stile per dimostrare come i metodi vengono risolti nell'ordine di risoluzione classico e
come è diverso con il nuovo ordine.

Ho provato lo stesso esempio riscrivendo l'esempio in nuovo stile ma il risultato non è diverso da quello ottenuto con le classi vecchio stile. La versione di python che sto usando per eseguire l'esempio è 2.5.2. Di seguito è riportato l'esempio:

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 chiamata viene instance.amethod()stampata Base1, ma secondo la mia comprensione dell'MRO con il nuovo stile di classi, l'output avrebbe dovuto essere Base3. La chiamata viene Derived.__mro__stampata:

(<class '__main__.Derived'>, <class '__main__.Base2'>, <class '__main__.Base1'>, <class '__main__.Base3'>, <type 'object'>)

Non sono sicuro se la mia comprensione di MRO con le nuove classi di stile sia errata o se sto facendo uno stupido errore che non sono in grado di rilevare. Per favore aiutami a comprendere meglio MRO.

Risposte:


183

La differenza cruciale tra l'ordine di risoluzione per le classi legacy e quelle di nuovo stile si ha quando la stessa classe antenata si verifica più di una volta nell'approccio "ingenuo", approfondito - ad esempio, si consideri un caso di "ereditarietà diamante":

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

qui, in stile legacy, l'ordine di risoluzione è D - B - A - C - A: quindi quando si cerca Dx, A è la prima base per risolverlo, nascondendo così la definizione in C. Mentre:

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

qui, nuovo stile, l'ordine è:

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

con Acostretto a venire in ordine di risoluzione solo una volta e dopo tutte le sue sottoclassi, in modo che le sostituzioni (cioè la sostituzione di C del membro x) funzionino effettivamente in modo ragionevole.

È uno dei motivi per cui le classi vecchio stile dovrebbero essere evitate: l'ereditarietà multipla con modelli "a rombo" semplicemente non funziona in modo sensato con loro, mentre lo fa con il nuovo stile.


2
"[la classe antenata] A [è] costretta a venire in ordine di risoluzione solo una volta e dopo tutte le sue sottoclassi, in modo che gli override (cioè l'override di C del membro x) funzionino effettivamente in modo sensato." - Epifania! Grazie a questa frase, posso fare di nuovo MRO nella mia testa. \ o / Grazie mille.
Esteis

23

L'ordine di risoluzione del metodo di Python è in realtà più complesso della semplice comprensione del motivo a rombi. Per capirlo davvero , dai un'occhiata alla linearizzazione C3 . Ho trovato che aiuta davvero usare le istruzioni print quando estendi i metodi per tracciare l'ordine. Ad esempio, quale sarebbe l'output di questo pattern? (Nota: la 'X' dovrebbe essere due bordi incrociati, non un nodo e ^ indica metodi che chiamano 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

Hai ricevuto ABDCEFG?

x = A()
x.m()

Dopo un sacco di tentativi con un errore, mi è venuta in mente un'interpretazione informale della teoria dei grafi della linearizzazione C3 come segue: (Qualcuno mi faccia sapere se è sbagliato.)

Considera questo esempio:

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()

Dovresti correggere il tuo secondo codice: hai messo la classe "I" come prima riga e hai anche usato super in modo che trovi la super classe "G" ma "I" è di prima classe quindi non sarà mai in grado di trovare la classe "G" perché lì non è una "G" superiore "I". Metti la classe "I" tra "G" e "F" :)
Aaditya Ura

Il codice di esempio non è corretto. superha argomenti richiesti.
danny

2
All'interno di una definizione di classe super () non richiede argomenti. Vedi https://docs.python.org/3/library/functions.html#super
Ben

La tua teoria dei grafi è inutilmente complicata. Dopo il passaggio 1, inserisci i bordi dalle classi a sinistra alle classi a destra (in qualsiasi elenco di ereditarietà), quindi esegui un ordinamento topologico e il gioco è fatto.
Kevin

@ Kevin non credo che sia corretto. Seguendo il mio esempio, ACDBEFGH non sarebbe un ordinamento topologico valido? Ma non è questo l'ordine di risoluzione.
Ben

5

Il risultato che ottieni è corretto. Prova a cambiare la classe di base Base3in Base1e confrontala con la stessa gerarchia per le classi classiche:

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()

Ora emette:

Base3
Base1

Leggi questa spiegazione per maggiori informazioni.


1

Stai vedendo quel comportamento perché la risoluzione del metodo è prima in profondità, non in larghezza. L'eredità di Dervied sembra

         Base2 -> Base1
        /
Derived - Base3

Così instance.amethod()

  1. Controlla Base2, non trova un metodo.
  2. Vede che Base2 ha ereditato da Base1 e controlla Base1. Base1 ha un amethod, quindi viene chiamato.

Questo si riflette in Derived.__mro__. Ripeti semplicemente Derived.__mro__e fermati quando trovi il metodo che stai cercando.


Dubito che il motivo per cui ottengo "Base1" come risposta è perché la risoluzione del metodo è prima in profondità, penso che ci sia di più di un approccio basato sulla profondità. Vedi l'esempio di Denis, se fosse la profondità prima o / p avrebbe dovuto essere "Base1". Fai anche riferimento al primo esempio nel link che hai fornito, anche lì l'MRO mostrato indica che la risoluzione del metodo non è determinata solo attraversando in profondità il primo ordine.
sateesh

Spiacenti, il link al documento su MRO è fornito da Denis. Per favore controlla, ho scambiato che mi hai fornito il link a python.org.
sateesh

4
Generalmente è prima di tutto, ma ci sono intelligenze per gestire l'eredità simile al diamante come ha spiegato Alex.
Jamessan
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.