Method Resolution Order (MRO) ในคลาสรูปแบบใหม่?


99

ในหนังสือPython in a Nutshell (2nd Edition)มีตัวอย่างที่ใช้
คลาสแบบเก่าเพื่อสาธิตวิธีการแก้ไขตามลำดับความละเอียดแบบคลาสสิกและ
แตกต่างอย่างไรกับลำดับใหม่

ฉันลองใช้ตัวอย่างเดียวกันนี้โดยเขียนตัวอย่างใหม่ในรูปแบบใหม่ แต่ผลลัพธ์ก็ไม่ต่างจากที่ได้จากคลาสแบบเก่า เวอร์ชัน python ที่ฉันใช้เพื่อเรียกใช้ตัวอย่างคือ2.5.2 ด้านล่างนี้คือตัวอย่าง:

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__  

การโทรinstance.amethod()พิมพ์ออกBase1มา แต่ตามความเข้าใจของฉันเกี่ยวกับ MRO ที่มีรูปแบบคลาสใหม่ผลลัพธ์ควรจะเป็นBase3กับรูปแบบใหม่ของการเรียนการส่งออกควรจะได้รับ การโทรDerived.__mro__พิมพ์:

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

ฉันไม่แน่ใจว่าความเข้าใจของฉันเกี่ยวกับ MRO กับคลาสรูปแบบใหม่นั้นไม่ถูกต้องหรือฉันกำลังทำผิดพลาดโง่ ๆ ซึ่งฉันไม่สามารถตรวจพบได้ โปรดช่วยฉันในการทำความเข้าใจ MRO ให้ดีขึ้น

คำตอบ:


186

ความแตกต่างที่สำคัญระหว่างลำดับการแก้ปัญหาสำหรับคลาสดั้งเดิมกับคลาสสไตล์ใหม่เกิดขึ้นเมื่อคลาสบรรพบุรุษเดียวกันเกิดขึ้นมากกว่าหนึ่งครั้งในแนวทาง "ไร้เดียงสา" แบบเจาะลึก - แรกเช่นพิจารณากรณี "การสืบทอดเพชร":

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

ที่นี่รูปแบบดั้งเดิมลำดับความละเอียดคือ D - B - A - C - A: ดังนั้นเมื่อค้นหา Dx A จึงเป็นฐานแรกในการแก้ปัญหาจึงซ่อนคำจำกัดความใน C ในขณะที่:

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

ที่นี่รูปแบบใหม่คำสั่งซื้อคือ:

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

ด้วยการAบังคับให้มาตามลำดับความละเอียดเพียงครั้งเดียวและหลังจากคลาสย่อยทั้งหมดดังนั้นการลบล้าง (เช่นการแทนที่สมาชิกของ C x) จึงทำงานได้อย่างสมเหตุสมผล

เป็นหนึ่งในเหตุผลที่ควรหลีกเลี่ยงคลาสแบบเก่า: การสืบทอดหลายรูปแบบที่มีรูปแบบ "เหมือนเพชร" ไม่สามารถใช้งานได้อย่างสมเหตุสมผลกับรูปแบบเหล่านี้ในขณะที่ใช้กับรูปแบบใหม่


2
"[คลาสบรรพบุรุษ] A [ถูก] บังคับให้มาตามลำดับความละเอียดเพียงครั้งเดียวและหลังจากคลาสย่อยทั้งหมดดังนั้นการแทนที่ (เช่นการแทนที่สมาชิก x ของ C) จะทำงานได้อย่างสมเหตุสมผล" - ศักดิ์สิทธิ์! ขอบคุณประโยคนี้ฉันสามารถทำ MRO ในหัวได้อีกครั้ง \ o / ขอบคุณมาก
Esteis

25

ลำดับการแก้ปัญหาวิธีของ Python นั้นซับซ้อนกว่าการเข้าใจรูปแบบเพชร หากต้องการจริงๆเข้าใจว่ามันจะดูที่C3 เชิงเส้น ฉันพบว่ามันช่วยได้มากในการใช้คำสั่งพิมพ์เมื่อขยายวิธีการติดตามคำสั่งซื้อ ตัวอย่างเช่นคุณคิดว่าผลลัพธ์ของรูปแบบนี้จะเป็นอย่างไร? (หมายเหตุ: 'X' สมมติว่าเป็นสองขอบข้ามไม่ใช่โหนดและ ^ หมายถึงเมธอดที่เรียก 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

คุณได้รับ ABDCEFG หรือไม่?

x = A()
x.m()

หลังจากลองผิดลองถูกมาหลายครั้งฉันก็ได้พบกับการตีความทฤษฎีกราฟแบบไม่เป็นทางการของการทำให้เป็นเส้นตรง C3 ดังนี้

ลองพิจารณาตัวอย่างนี้:

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

คุณควรแก้ไขรหัสที่สองของคุณ: คุณใส่คลาส "I" เป็นบรรทัดแรกและใช้ super ด้วยดังนั้นจึงพบว่า super class "G" แต่ "I" เป็นคลาสแรกดังนั้นจะไม่สามารถหาคลาส "G" ได้เนื่องจากมี ไม่ใช่ "G" บน "I" ใส่คลาส "I" ระหว่าง "G" และ "F" :)
Aaditya Ura

รหัสตัวอย่างไม่ถูกต้อง superมีข้อโต้แย้งที่จำเป็น
danny

2
ภายในนิยามคลาส super () ไม่ต้องการอาร์กิวเมนต์ ดูhttps://docs.python.org/3/library/functions.html#super
เบ็น

ทฤษฎีกราฟของคุณซับซ้อนโดยไม่จำเป็น หลังจากขั้นตอนที่ 1 ให้แทรกขอบจากคลาสทางด้านซ้ายไปยังคลาสทางด้านขวา (ในรายการการสืบทอดใด ๆ ) จากนั้นทำการเรียงลำดับโทโพโลยีและคุณทำเสร็จแล้ว
Kevin

@ เควินฉันคิดว่าไม่ถูกต้อง ตามตัวอย่างของฉัน ACDBEFGH จะไม่ใช่การเรียงลำดับโทโพโลยีที่ถูกต้องหรือไม่? แต่นั่นไม่ใช่ลำดับความละเอียด
เบ็น

5

ผลลัพธ์ที่คุณได้รับนั้นถูกต้อง ลองเปลี่ยนคลาสพื้นฐานของBase3ถึงBase1และเปรียบเทียบกับลำดับชั้นเดียวกันสำหรับคลาสคลาสสิก:

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

ตอนนี้ผลลัพธ์:

Base3
Base1

อ่านคำอธิบายนี้สำหรับข้อมูลเพิ่มเติม


1

คุณเห็นพฤติกรรมนั้นเนื่องจากวิธีการแก้ปัญหาเป็นแบบเชิงลึกไม่ใช่ความกว้างก่อน มรดกของ Dervied ดูเหมือนว่า

         Base2 -> Base1
        /
Derived - Base3

ดังนั้น instance.amethod()

  1. ตรวจสอบ Base2 ไม่พบ amethod
  2. เห็นว่า Base2 ได้รับมาจาก Base1 และตรวจสอบ Base1 Base1 มี a amethodดังนั้นจึงถูกเรียก

Derived.__mro__นี่คือภาพสะท้อนใน เพียงแค่ทำซ้ำDerived.__mro__และหยุดเมื่อคุณพบวิธีการที่กำลังมองหา


ฉันสงสัยว่าเหตุผลที่ฉันได้รับ "Base1" เป็นคำตอบเพราะวิธีการแก้ปัญหานั้นมีความลึกเป็นอันดับแรกฉันคิดว่ามีอะไรมากกว่าวิธีการเชิงลึกก่อน ดูตัวอย่างของ Denis หากเป็นค่าความลึก o / p แรกควรเป็น "Base1" โปรดดูตัวอย่างแรกในลิงค์ที่คุณให้ไว้นอกจากนี้ MRO ที่แสดงยังบ่งชี้ว่าความละเอียดของวิธีการไม่ได้ถูกกำหนดโดยการสำรวจตามลำดับความลึกก่อน
sateesh

ขออภัยที่ลิงก์ไปยังเอกสารใน MRO นั้นจัดทำโดย Denis โปรดตรวจสอบว่าฉันเข้าใจผิดว่าคุณให้ลิงก์ไปยัง python.org
sateesh

4
โดยทั่วไปแล้วจะต้องมีความลึกเป็นอันดับแรก แต่ก็มีความชาญฉลาดในการจัดการมรดกที่เหมือนเพชรตามที่อเล็กซ์อธิบาย
jamessan
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.