Method Resolution Order (MRO) w klasach nowego stylu?


99

W książce Python in a Nutshell (2nd Edition) znajduje się przykład, który używa
klas starego stylu, aby zademonstrować, w jaki sposób metody są rozwiązywane w klasycznej kolejności i
czym różni się od nowej kolejności.

Wypróbowałem ten sam przykład, przepisując przykład w nowym stylu, ale wynik nie różni się od tego, co uzyskano przy użyciu klas starego stylu. Wersja języka Python, której używam do uruchomienia przykładu, to 2.5.2. Poniżej przykład:

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__  

Wywołanie jest instance.amethod()drukowane Base1, ale zgodnie z moim zrozumieniem MRO z nowym stylem klas wynik powinien być Base3. Wezwanie Derived.__mro__drukuje:

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

Nie jestem pewien, czy moje rozumienie MRO z klasami nowych stylów jest niepoprawne, czy też popełniam głupi błąd, którego nie jestem w stanie wykryć. Proszę, pomóż mi w lepszym zrozumieniu MRO.

Odpowiedzi:


186

Zasadnicza różnica między kolejnością rozwiązywania klas klas starszych a klasami nowego stylu pojawia się, gdy ta sama klasa przodków występuje więcej niż raz w „naiwnym” podejściu głębi - np. Rozważmy przypadek „dziedziczenia diamentu”:

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

tutaj, w tradycyjnym stylu, kolejność rozdzielczości to D - B - A - C - A: więc patrząc w górę Dx, A jest pierwszą podstawą rozdzielczości, aby ją rozwiązać, ukrywając w ten sposób definicję w C. Podczas gdy:

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

tutaj, w nowym stylu, kolejność jest następująca:

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

z Awymuszonym pojawieniem się w kolejności rozstrzygnięcia tylko raz i po wszystkich jego podklasach, aby nadpisania (tj. nadpisanie elementu członkowskiego przez C x) faktycznie działały rozsądnie.

Jest to jeden z powodów, dla których należy unikać klas w starym stylu: wielokrotne dziedziczenie z wzorami „diamentowymi” po prostu nie działa z nimi rozsądnie, podczas gdy ma to miejsce w przypadku nowego stylu.


2
„[klasa nadrzędna] A [jest] zmuszona do przyjścia w kolejności rozstrzygnięcia tylko raz i po wszystkich swoich podklasach, tak aby nadpisania (tj. nadpisanie elementu x przez C) faktycznie działały rozsądnie”. - Święto Trzech Króli! Dzięki temu zdaniu znów mogę zrobić MRO w głowie. \ o / Dziękuję bardzo.
Esteis

25

Kolejność rozwiązywania metod w Pythonie jest w rzeczywistości bardziej złożona niż samo zrozumienie wzoru rombu. Aby naprawdę to zrozumieć, spójrz na linearyzację C3 . Zauważyłem, że naprawdę pomaga użycie instrukcji print podczas rozszerzania metod śledzenia zamówienia. Na przykład, jak myślisz, jaki byłby wynik tego wzorca? (Uwaga: przypuszcza się, że 'X' to dwie przecinające się krawędzie, a nie węzeł, a ^ oznacza metody, które wywołują 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

Czy otrzymałeś ABDCEFG?

x = A()
x.m()

Po wielu próbach i błędach wpadłem na nieformalną interpretację teorii grafów linearyzacji C3 w następujący sposób: (Niech ktoś mi powie, jeśli to jest błędne).

Rozważmy ten przykład:

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

Powinieneś poprawić swój drugi kod: umieściłeś klasę "I" jako pierwszą linię i użyłeś super, więc znajdowanie super klasy "G", ale "I" jest pierwszą klasą, więc nigdy nie będzie w stanie znaleźć klasy "G", ponieważ tam nie ma „G” górnego „I”. Umieść klasę „I” pomiędzy „G” i „F” :)
Aaditya Ura

Przykładowy kod jest nieprawidłowy. superma wymagane argumenty.
Danny

2
W definicji klasy super () nie wymaga argumentów. Zobacz https://docs.python.org/3/library/functions.html#super
Ben

Twoja teoria grafów jest niepotrzebnie skomplikowana. Po kroku 1 wstaw krawędzie z klas po lewej stronie do klas po prawej (na dowolnej liście dziedziczenia), a następnie wykonaj sortowanie topologiczne i gotowe.
Kevin

@Kevin Nie sądzę, że to prawda. Idąc za moim przykładem, czy ACDBEFGH nie byłoby prawidłowym sortowaniem topologicznym? Ale to nie jest kolejność rozdzielczości.
Ben

5

Otrzymany wynik jest prawidłowy. Spróbuj zmienić klasę bazową Base3na Base1i porównaj z tą samą hierarchią klas klasycznych:

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

Teraz wyświetla:

Base3
Base1

Przeczytaj to wyjaśnienie, aby uzyskać więcej informacji.


1

Widzisz to zachowanie, ponieważ rozdzielczość metody jest najpierw głęboka, a nie szerokość. Jak wygląda spadek po Dervied

         Base2 -> Base1
        /
Derived - Base3

Więc instance.amethod()

  1. Sprawdza Base2, nie znajduje metody.
  2. Widzi, że Base2 odziedziczył po Base1 i sprawdza Base1. Base1 ma amethod, więc zostaje wywołany.

Znajduje to odzwierciedlenie w Derived.__mro__. Po prostu powtórz Derived.__mro__i zatrzymaj się, gdy znajdziesz poszukiwaną metodę.


Wątpię, że powodem, dla którego otrzymuję odpowiedź „Base1”, jest to, że rozdzielczość metody jest oparta na pierwszej głębi. Myślę, że jest w niej coś więcej niż podejście „najpierw głębia”. Zobacz przykład Denisa, jeśli była to głębia, najpierw o / p powinno być „Baza1”. Odnieś się również do pierwszego przykładu w podanym łączu, tam również pokazany MRO wskazuje, że rozdzielczość metody nie jest określana tylko przez przechodzenie w kolejności w głąb.
sateesh

Przepraszamy, link do dokumentu na temat MRO jest dostarczony przez Denisa. Sprawdź to, pomyliłem się, że podałeś mi link do python.org.
sateesh

4
Ogólnie rzecz biorąc, jest to najpierw głębia, ale są sprytne sposoby radzenia sobie z dziedziczeniem podobnym do diamentu, jak wyjaśnił Alex.
jamessan
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.