Metode Resolution Order (MRO) di kelas gaya baru?

99

Dalam buku Python in a Nutshell (Edisi ke-2) terdapat contoh yang menggunakan
kelas gaya lama untuk mendemonstrasikan bagaimana metode diselesaikan dalam urutan resolusi klasik dan
apa bedanya dengan orde baru.

Saya mencoba contoh yang sama dengan menulis ulang contoh dalam gaya baru tetapi hasilnya tidak berbeda dengan apa yang diperoleh dengan kelas gaya lama. Versi python yang saya gunakan untuk menjalankan contoh adalah 2.5.2. Berikut contohnya:

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__  

Panggilan tersebut instance.amethod()dicetak Base1, tetapi sesuai pemahaman saya tentang MRO dengan gaya kelas baru, outputnya seharusnya Base3. Panggilan Derived.__mro__mencetak:

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

Saya tidak yakin apakah pemahaman saya tentang MRO dengan kelas gaya baru tidak benar atau saya melakukan kesalahan konyol yang tidak dapat saya deteksi. Tolong bantu saya untuk lebih memahami MRO.

sateesh
sumber

Jawaban:

188

Perbedaan penting antara urutan resolusi untuk kelas lama vs kelas gaya baru muncul saat kelas leluhur yang sama muncul lebih dari sekali dalam pendekatan "naif" dan mendalam - misalnya, pertimbangkan kasus "pewarisan berlian":

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

di sini, gaya lama, urutan resolusinya adalah D - B - A - C - A: jadi saat mencari Dx, A adalah basis pertama dalam urutan resolusi untuk menyelesaikannya, sehingga menyembunyikan definisi di C.

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

di sini, gaya baru, urutannya adalah:

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

dengan Aterpaksa datang dalam urutan resolusi hanya sekali dan setelah semua subkelasnya, sehingga timpaan (yaitu, penggantian anggota C x) benar-benar berfungsi dengan baik.

Itu salah satu alasan mengapa kelas gaya lama harus dihindari: pewarisan berganda dengan pola "seperti berlian" tidak bekerja secara masuk akal dengan mereka, sementara itu terjadi dengan gaya baru.

Alex Martelli
sumber
2
"[kelas leluhur] A [adalah] dipaksa untuk datang dalam urutan resolusi hanya sekali dan setelah semua subkelasnya, sehingga menimpa (yaitu, penggantian C dari anggota x) benar-benar bekerja dengan baik." - Epiphany! Berkat kalimat ini, saya bisa melakukan MRO di kepala saya lagi. \ o / Terima kasih banyak.
Esteis
25

Urutan resolusi metode Python sebenarnya lebih kompleks dari sekedar memahami pola berlian. Untuk benar-benar memahaminya, lihat linierisasi C3 . Saya merasa sangat membantu untuk menggunakan pernyataan cetak saat memperluas metode untuk melacak pesanan. Misalnya, menurut Anda apa hasil dari pola ini? (Catatan: 'X' dianggap dua tepi bersilangan, bukan simpul dan ^ menandakan metode yang memanggil 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

Apakah Anda mendapatkan ABDCEFG?

x = A()
x.m()

Setelah banyak trial-error, saya mendapatkan interpretasi teori grafik informal linierisasi C3 sebagai berikut: (Seseorang tolong beri tahu saya jika ini salah.)

Pertimbangkan contoh ini:

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()
Ben
sumber
Anda harus memperbaiki kode kedua Anda: Anda telah menempatkan kelas "I" sebagai baris pertama dan juga menggunakan super sehingga menemukan kelas super "G" tetapi "I" adalah kelas satu sehingga tidak akan pernah dapat menemukan kelas "G" karena di sana ada "G" di atas "I". Tempatkan kelas "I" antara "G" dan "F" :)
Aaditya Ura
Kode contoh salah. supermemiliki argumen yang dibutuhkan.
danny
2
Di dalam definisi kelas super () tidak membutuhkan argumen. Lihat https://docs.python.org/3/library/functions.html#super
Ben
Teori grafik Anda sangat rumit. Setelah langkah 1, sisipkan tepi dari kelas di kiri ke kelas di sebelah kanan (dalam daftar pewarisan apa pun), lalu lakukan pengurutan topologis dan Anda selesai.
Kevin
@Kevin Saya rasa itu tidak benar. Mengikuti contoh saya, bukankah ACDBEFGH akan menjadi jenis topologi yang valid? Tapi itu bukan urutan resolusinya.
Ben
5

Hasil yang Anda dapatkan benar. Coba ubah kelas dasar Base3menjadi Base1dan bandingkan dengan hierarki yang sama untuk kelas klasik:

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

Sekarang outputnya:

Base3
Base1

Baca penjelasan ini untuk informasi lebih lanjut.

Denis Otkidach
sumber
1

Anda melihat perilaku itu karena resolusi metode adalah yang mengutamakan kedalaman, bukan yang pertama luas. Seperti warisan Dervied

         Base2 -> Base1
        /
Derived - Base3

Begitu instance.amethod()

  1. Periksa Base2, tidak menemukan metode.
  2. Melihat bahwa Base2 mewarisi dari Base1, dan memeriksa Base1. Base1 memiliki amethod, sehingga dipanggil.

Ini tercermin dalam Derived.__mro__. Cukup ulangi Derived.__mro__dan hentikan saat Anda menemukan metode yang dicari.

jamessan
sumber
Saya ragu bahwa alasan saya mendapatkan "Base1" sebagai jawaban adalah karena resolusi metode adalah depth-first, saya pikir ada lebih dari itu daripada pendekatan depth-first. Lihat contoh Denis, jika kedalaman output pertama seharusnya "Base1". Lihat juga contoh pertama pada link yang telah Anda berikan, di sana juga MRO yang ditampilkan menunjukkan bahwa resolusi metode tidak hanya ditentukan dengan melakukan traverse dalam urutan depth-first.
sateesh
Maaf, tautan ke dokumen di MRO disediakan oleh Denis. Harap periksa, saya salah mengira Anda memberi saya tautan ke python.org.
sateesh
4
Ini umumnya lebih dalam, tetapi ada kecerdasan untuk menangani warisan seperti berlian seperti yang dijelaskan Alex.
jamessan