Thứ tự phân giải phương thức (MRO) trong các lớp kiểu mới?


94

Trong cuốn sách Python in a Nutshell (Phiên bản thứ 2) có một ví dụ sử dụng
các lớp kiểu cũ để chứng minh cách các phương thức được giải quyết theo thứ tự phân giải cổ điển và
nó khác với thứ tự mới như thế nào.

Tôi đã thử cùng một ví dụ bằng cách viết lại ví dụ theo kiểu mới nhưng kết quả không khác gì so với những gì thu được với các lớp kiểu cũ. Phiên bản python tôi đang sử dụng để chạy ví dụ là 2.5.2. Dưới đây là ví dụ:

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__  

Cuộc gọi instance.amethod()in ra Base1, nhưng theo hiểu biết của tôi về MRO với kiểu lớp mới thì đầu ra đáng lẽ phải như vậy Base3. Cuộc gọi Derived.__mro__in ra:

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

Tôi không chắc liệu hiểu biết của tôi về MRO với các lớp kiểu mới có sai hay tôi đang mắc một sai lầm ngớ ngẩn mà tôi không thể phát hiện ra. Vui lòng giúp tôi hiểu rõ hơn về MRO.

Câu trả lời:


184

Sự khác biệt quan trọng giữa thứ tự phân giải cho các lớp kế thừa và lớp kiểu mới xảy ra khi cùng một lớp tổ tiên xuất hiện nhiều lần trong phương pháp tiếp cận theo chiều sâu "ngây thơ" - ví dụ: hãy xem xét trường hợp "kế thừa kim cương":

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

ở đây, kiểu kế thừa, thứ tự phân giải là D - B - A - C - A: vì vậy khi tra cứu Dx, A là cơ sở đầu tiên trong độ phân giải để giải nó, do đó ẩn định nghĩa trong C. Trong khi:

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

ở đây, kiểu mới, thứ tự là:

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

với việc Abuộc phải đến theo thứ tự giải quyết chỉ một lần và sau tất cả các lớp con của nó, để ghi đè (tức là ghi đè thành viên của C x) thực sự hoạt động hợp lý.

Đó là một trong những lý do mà các lớp kiểu cũ nên tránh: đa kế thừa với các mẫu "giống như kim cương" không hoạt động hợp lý với chúng, trong khi nó lại làm với kiểu mới.


2
"[lớp tổ tiên] A [bị] buộc phải đến theo thứ tự phân giải chỉ một lần và sau tất cả các lớp con của nó, để ghi đè (tức là ghi đè thành viên x của C) thực sự hoạt động hợp lý." - Hiển linh! Nhờ câu này mà tôi có thể làm lại MRO trong đầu. \ o / Cảm ơn bạn rất nhiều.
Esteis

23

Thứ tự phân giải phương thức của Python thực sự phức tạp hơn là chỉ hiểu mẫu hình kim cương. Để thực sự hiểu nó, hãy xem tuyến tính hóa C3 . Tôi thấy nó thực sự hữu ích khi sử dụng câu lệnh in khi mở rộng các phương pháp theo dõi đơn hàng. Ví dụ, bạn nghĩ đầu ra của mẫu này sẽ là bao nhiêu? (Lưu ý: 'X' được giả sử là hai cạnh giao nhau, không phải là nút và ^ biểu thị các phương thức gọi 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

Bạn đã nhận được ABDCEFG?

x = A()
x.m()

Sau rất nhiều lần thử một lỗi, tôi đã đưa ra một cách giải thích lý thuyết đồ thị không chính thức về tuyến tính hóa C3 như sau: (Ai đó làm ơn cho tôi biết nếu điều này là sai.)

Hãy xem xét ví dụ này:

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

Bạn nên sửa mã thứ hai của mình: bạn đã đặt lớp "I" làm dòng đầu tiên và cũng sử dụng super để nó tìm siêu lớp "G" nhưng "I" là lớp đầu tiên nên nó sẽ không bao giờ có thể tìm thấy lớp "G" vì ở đó không phải là "G" trên "I". Đặt lớp "I" giữa "G" và "F" :)
Aaditya Ura

Mã ví dụ không chính xác. supercó các đối số bắt buộc.
danny

2
Bên trong định nghĩa lớp super () không yêu cầu đối số. Xem https://docs.python.org/3/library/functions.html#super
Ben

Lý thuyết đồ thị của bạn không cần thiết phải phức tạp. Sau bước 1, hãy chèn các cạnh từ các lớp ở bên trái sang các lớp ở bên phải (trong bất kỳ danh sách kế thừa nào), sau đó thực hiện sắp xếp tôpô và bạn đã hoàn tất.
Kevin,

@Kevin Tôi không nghĩ điều đó chính xác. Theo ví dụ của tôi, ACDBEFGH có phải là một loại tôpô hợp lệ không? Nhưng đó không phải là thứ tự giải quyết.
Ben

5

Kết quả bạn nhận được là chính xác. Hãy thử thay đổi lớp cơ sở của Base3thành Base1và so sánh với cùng một hệ thống phân cấp cho các lớp cổ điển:

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

Bây giờ nó xuất ra:

Base3
Base1

Đọc giải thích này để biết thêm thông tin.


1

Bạn đang thấy hành vi đó bởi vì độ phân giải của phương pháp là ưu tiên chiều sâu, không phải chiều rộng. Quyền thừa kế của Dervied trông như thế nào

         Base2 -> Base1
        /
Derived - Base3

Vì thế instance.amethod()

  1. Kiểm tra Base2, không tìm thấy amethod.
  2. Thấy rằng Base2 đã kế thừa từ Base1 và kiểm tra Base1. Base1 có một amethod, vì vậy nó được gọi.

Điều này được phản ánh trong Derived.__mro__. Chỉ cần lặp lại Derived.__mro__và dừng lại khi bạn tìm thấy phương pháp đang được tìm kiếm.


Tôi nghi ngờ rằng lý do tôi nhận được câu trả lời là "Base1" là vì độ phân giải của phương pháp là theo chiều sâu, tôi nghĩ rằng có nhiều điều hơn là cách tiếp cận theo chiều sâu. Xem ví dụ của Denis, nếu độ sâu đầu tiên thì o / p phải là "Base1". Ngoài ra, hãy tham khảo ví dụ đầu tiên trong liên kết mà bạn đã cung cấp, MRO cũng được hiển thị cho biết rằng độ phân giải của phương pháp không chỉ được xác định bằng cách duyệt theo thứ tự đầu tiên theo chiều sâu.
sateesh

Xin lỗi, liên kết đến tài liệu trên MRO do Denis cung cấp. Vui lòng kiểm tra lại, tôi nhầm rằng bạn đã cung cấp cho tôi liên kết đến python.org.
sateesh

4
Nói chung, đó là chiều sâu, nhưng có những điều thông minh để xử lý tài sản thừa kế giống như kim cương như Alex giải thích.
jamessan
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.