Method Resolution Order (MRO) in neuen Klassen?


93

In dem Buch Python in a Nutshell (2. Ausgabe) gibt es ein Beispiel, das
alte Stilklassen verwendet, um zu demonstrieren, wie Methoden in klassischer Auflösungsreihenfolge aufgelöst werden und
wie sie sich von der neuen Reihenfolge unterscheiden.

Ich habe das gleiche Beispiel versucht, indem ich das Beispiel in einem neuen Stil umgeschrieben habe, aber das Ergebnis unterscheidet sich nicht von dem, was mit alten Stilklassen erzielt wurde. Die Python-Version, mit der ich das Beispiel ausführe, ist 2.5.2. Unten ist das Beispiel:

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__  

Der Aufruf wird instance.amethod()gedruckt Base1, aber nach meinem Verständnis des MRO mit neuem Klassenstil sollte die Ausgabe gewesen sein Base3. Der Anruf wird Derived.__mro__gedruckt:

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

Ich bin mir nicht sicher, ob mein Verständnis von MRO mit neuen Stilklassen falsch ist oder ob ich einen dummen Fehler mache, den ich nicht erkennen kann. Bitte helfen Sie mir beim besseren Verständnis von MRO.

Antworten:


183

Der entscheidende Unterschied zwischen der Auflösungsreihenfolge für Legacy-Klassen und Klassen neuen Stils besteht darin, dass dieselbe Ahnenklasse im "naiven" Deep-First-Ansatz mehr als einmal vorkommt - z. B. betrachten Sie einen "Diamant-Vererbungs" -Fall:

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

Hier ist die Auflösungsreihenfolge im Legacy-Stil D - B - A - C - A: Wenn Sie also nach Dx suchen, ist A die erste Basis in der Auflösungsreihenfolge, um sie zu lösen, wodurch die Definition in C ausgeblendet wird.

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

hier im neuen Stil lautet die Reihenfolge:

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

mit Agezwungen, nur einmal und nach all seinen Unterklassen in Auflösungsreihenfolge zu kommen, so dass Überschreibungen (dh Cs Überschreibung des Mitglieds x) tatsächlich sinnvoll funktionieren.

Dies ist einer der Gründe, warum Klassen im alten Stil vermieden werden sollten: Mehrfachvererbung mit "diamantähnlichen" Mustern funktioniert bei ihnen einfach nicht sinnvoll, während dies bei neuen Stilen der Fall ist.


2
"[die Ahnenklasse] A [ist] gezwungen, nur einmal und nach all seinen Unterklassen in Auflösungsreihenfolge zu kommen, damit Überschreibungen (dh Cs Überschreibung von Mitglied x) tatsächlich sinnvoll funktionieren." - Offenbarung! Dank dieses Satzes kann ich wieder MRO in meinem Kopf machen. \ o / Vielen Dank.
Esteis

23

Die Reihenfolge der Python-Methodenauflösung ist tatsächlich komplexer als nur das Verständnis des Rautenmusters. Um es wirklich zu verstehen, werfen Sie einen Blick auf die C3-Linearisierung . Ich habe festgestellt, dass es wirklich hilfreich ist, print-Anweisungen zu verwenden, wenn Methoden zur Verfolgung der Reihenfolge erweitert werden. Was denkst du zum Beispiel wäre die Ausgabe dieses Musters? (Hinweis: Das 'X' soll zwei sich kreuzende Kanten sein, kein Knoten, und ^ bezeichnet Methoden, die super () aufrufen.)

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

Hast du ABDCEFG bekommen?

x = A()
x.m()

Nach vielen Versuchen und Fehlern kam ich zu einer informellen graphentheoretischen Interpretation der C3-Linearisierung wie folgt: (Jemand, bitte lassen Sie mich wissen, wenn dies falsch ist.)

Betrachten Sie dieses Beispiel:

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

Sie sollten Ihren zweiten Code korrigieren: Sie haben die Klasse "I" als erste Zeile eingegeben und auch "super" verwendet, um die Superklasse "G" zu finden, aber "I" ist die erste Klasse, sodass sie die Klasse "G" dort nie finden kann ist kein "G" oberes "I". Setzen Sie die Klasse "I" zwischen "G" und "F" :)
Aaditya Ura

Der Beispielcode ist falsch. superhat Argumente benötigt.
Danny

2
Innerhalb einer Klassendefinition benötigt super () keine Argumente. Siehe https://docs.python.org/3/library/functions.html#super
Ben

Ihre Graphentheorie ist unnötig kompliziert. Fügen Sie nach Schritt 1 Kanten von Klassen links zu Klassen rechts (in eine beliebige Vererbungsliste) ein, und führen Sie dann eine topologische Sortierung durch, und Sie sind fertig.
Kevin

@ Kevin Ich denke nicht, dass das richtig ist. Wäre ACDBEFGH nach meinem Beispiel keine gültige topologische Sorte? Aber das ist nicht die Auflösungsreihenfolge.
Ben

5

Das Ergebnis ist korrekt. Versuchen Sie, die Basisklasse von Base3to zu ändern , Base1und vergleichen Sie sie mit derselben Hierarchie für klassische Klassen:

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

Jetzt gibt es aus:

Base3
Base1

Lesen Sie diese Erklärung für weitere Informationen.


1

Sie sehen dieses Verhalten, weil die Methodenauflösung in der Tiefe und nicht in der Breite an erster Stelle steht. Dervieds Erbschaft sieht aus wie

         Base2 -> Base1
        /
Derived - Base3

So instance.amethod()

  1. Überprüft Base2 und findet keine Methode.
  2. Sieht, dass Base2 von Base1 geerbt hat, und überprüft Base1. Base1 hat ein amethod, also wird es aufgerufen.

Dies spiegelt sich in wider Derived.__mro__. Einfach wiederholen Derived.__mro__und anhalten, wenn Sie die gesuchte Methode gefunden haben.


Ich bezweifle, dass der Grund, warum ich "Base1" als Antwort bekomme, darin besteht, dass die Methodenauflösung "Tiefe zuerst" ist. Ich denke, es steckt mehr dahinter als ein Ansatz "Tiefe zuerst". Siehe Denis 'Beispiel, wenn es Tiefe wäre, hätte o / p "Base1" sein sollen. Beziehen Sie sich auch auf das erste Beispiel in dem von Ihnen bereitgestellten Link. Dort zeigt auch die gezeigte MRO an, dass die Methodenauflösung nicht nur durch Durchlaufen in der Tiefe erster Ordnung bestimmt wird.
Sateesh

Der Link zum Dokument zu MRO wird von Denis bereitgestellt. Bitte überprüfen Sie das. Ich habe mich geirrt, dass Sie mir den Link zu python.org zur Verfügung gestellt haben.
Sateesh

4
Es ist im Allgemeinen Tiefe zuerst, aber es gibt Klugheiten, um mit diamantähnlicher Vererbung umzugehen, wie Alex erklärte.
Jamessan
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.