0

Quy tắc MRO trong đa kế thừa ở Python

Nếu bạn thực sự muốn biết đa kế thừa trong Python hoạt động như thế nào , thì bài viết này là dành cho bạn. Hãy bắt đầu với một số định nghĩa:

  1. Cho một lớp C trong một hệ thống phân cấp thừa kế phức tạp, việc xác định thứ tự các phương thức được ghi đè là một nhiệm vụ quan trọng, để xác định thứ tự của tổ tiên của C.
  2. Danh sách tổ tiên của một lớp C, bao gồm cả chính lớp đó, được sắp xếp từ tổ tiên gần nhất đến xa nhất, được gọi là danh sách ưu tiên của lớp hoặc tuyến tính hóa của C.
  3. Thứ tự độ phân giải phương thức (Method Resolution Order - MRO) là tập hợp các quy tắc để xây dựng cây kế thừa. MRO của C có nghĩa là cây kế thừa của lớp C.
  4. Chẳng hạn, trong trường hợp phân cấp thừa kế đơn, nếu C là lớp con của C1 và C1 là lớp con của C2, thì việc tuyến tính hóa của C chỉ đơn giản là danh sách [C, C1, C2]. Tuy nhiên, với nhiều hệ thống phân cấp thừa kế, việc xây dựng tuyến tính hóa trở nên cồng kềnh hơn, vì việc xây dựng tuyến tính hóa tôn trọng trật tự ưu tiên cục bộ và đơn điệu là khó khăn hơn.
  5. Định nghĩa về tính đơn điệu. Một MRO là đơn điệu khi điều sau là đúng: nếu C1 đi trước C2 trong cây kế thừa C, thì C1 đến trước C2 trong cây kế thừa của bất kì lớp con nào của C. Mặt khác, việc tạo ra một lớp mới có thể thay đổi thứ tự độ phân giải của các phương thức , có khả năng xảy ra các lỗi rất tinh tế. Ví dụ về điều này xảy ra sẽ được hiển thị sau.
  6. Không phải tất cả các lớp đều có thể tuyến tính hóa được. Có những trường hợp, trong các hệ thống phân cấp phức tạp, trong đó không thể lấy được một lớp sao cho tuyến tính hóa của nó tuân thủ tất cả các định nghĩa trên. Ví dụ :
>>> O = object
>>> class X(O): pass
>>> class Y(O): pass
>>> class A(X,Y): pass
>>> class B(Y,X): pass

có thể được biểu diễn bằng biểu đồ thừa kế sau, trong đó tôi đã ký hiệu với O lớp object, là khởi đầu của bất kỳ hệ thống phân cấp nào.

 -----------
|           |
|    O      |
|  /   \    |
 - X    Y  /
   |  / | /
   | /  |/
   A    B
   \   /
     ?

Trong trường hợp này, không thể lấy được một lớp C mới từ A và B, vì X đi trước Y ở A, nhưng Y đi trước X ở B, do đó thứ tự giải quyết phương thức sẽ không rõ ràng trong C.

Quy tắc MRO

Hãy để tôi giới thiệu một vài ký hiệu đơn giản sẽ hữu ích cho các cuộc thảo luận sau. Tôi sẽ sử dụng các kí hiệu viết tắt :

C1 C2 ... CN

để biểu thị danh sách các lớp [C1, C2, ... , CN]. Đầu danh sách là phần tử đầu tiên của nó và phần còn lại gọi là đuôi. Tôi cũng dùng kí hiệu:

C + (C1 C2 ... CN) = C C1 C2 ... CN

để biểu thị kết quả của việc gộp 2 danh sách [C] + [C1, C2, ... ,CN]. Bây giờ tôi sẽ giải thích cách MRO hoạt động: Hãy xem xét một lớp C trong hệ thống phân cấp thừa kế, với C kế thừa từ các lớp cơ sở B1, B2, ..., BN. Chúng ta muốn tính toán tuyến tính hóa L [C] của lớp C. Quy tắc là như sau:

tuyến tính hóa của C là tổng của C cộng với sự hợp nhất của tuyến tính hóa của cha mẹ và danh sách của cha mẹ.

viết bằng kí hiệu sẽ như sau:

L[C(B1 ... BN)] = C + merge(L[B1] ... L[BN], B1 ... BN)

Nếu C là lớp đối tượng không có cha mẹ thì việc tuyến tính hóa sẽ chỉ như này:

L[C] = C

Tuy nhiên việc thường không đơn giản như vậy, người ta thường tính toán việc gộp các danh sách theo quy tắc sau:

Lấy phần tử đứng đầu danh sách đầu tiên, tức là L [B1] [0]; nếu đầu này không nằm trong đuôi của bất kỳ danh sách nào khác, nó gọi là đầu 'tốt', hãy thêm nó vào cây kế thừa của C và xóa nó khỏi các danh sách đang gộp, nếu không hãy nhìn vào đầu của danh sách tiếp theo và lấy đầu của nó, nếu đó là một cái đầu 'tốt'. Sau đó lặp lại thao tác cho đến khi tất cả các lớp được loại bỏ hoặc không thể tìm thấy những cái đầu 'tốt'. Trong trường hợp này, không thể xây dựng hợp nhất, Python 2.3 sẽ từ chối tạo lớp C và sẽ đưa ra một ngoại lệ.

Quy tắc này đảm bảo rằng hoạt động hợp nhất bảo toàn thứ tự, nếu thứ tự có thể được bảo tồn. Mặt khác, nếu trật tự không thể được giữ nguyên (như trong ví dụ ở trên) thì việc hợp nhất không thể được tính toán. Việc tính toán hợp nhất là không đáng kể nếu C chỉ có một cha mẹ (thừa kế đơn); trong trường hợp này:

L[C(B)] = C + merge(L[B],B) = C + L[B]

Tuy nhiên, trong trường hợp nhiều kế thừa, mọi thứ trở nên cồng kềnh hơn và tôi không hy vọng bạn có thể hiểu quy tắc mà không cần một vài ví dụ.

Ví dụ :

Hãy xem xét hệ thống phân cấp sau:

>>> O = object
>>> class F(O): pass
>>> class E(O): pass
>>> class D(O): pass
>>> class C(D,F): pass
>>> class B(D,E): pass
>>> class A(B,C): pass

Trong trường hợp này, biểu đồ thừa kế có thể được vẽ như sau:

                                                  6
                                                 ---
Level 3                 | O |                  (more general)
                                         /  ---  \
                                         /    |    \                      |
                                        /     |     \                     |
                                       /      |      \                    |
                                      ---    ---    ---                   |
    Level 2        3 | D | 4| E |  | F | 5                |
                                      ---    ---    ---                   |
                                       \  \ _ /       |                   |
                                        \    / \ _    |                   |
                                         \  /      \  |                   |
                                          ---      ---                    |
Level 1            1 | B |    | C | 2                 |
                                          ---      ---                    |
                                                \      /                      |
                                                 \    /                      \ /
                                                   ---
Level 0                 0 | A |                (more specialized)
                                                   ---

Các cây thừa kế của O, D, E và F là đơn giản:

L[O] = O
L[D] = D O
L[E] = E O
L[F] = F O

Cây thừa kế của B được tính là

L[B] = B + merge(DO, EO, DE)

Chúng ta thấy rằng D là một cái đầu 'tốt' , do đó chúng ta lấy nó khỏi danh sách và tính tiếp hợp nhất (O, EO, E). Bây giờ O không phải là một cái đầu 'tốt' , vì nó nằm trong phần đuôi của chuỗi EO. Trong trường hợp này, quy tắc nói rằng chúng ta phải bỏ qua chuỗi tiếp theo. Sau đó, chúng ta thấy rằng E là một cái đầu 'tốt' ; chúng ta lấy nó và chúng ta được rút gọn để tính hợp nhất (O, O) mang lại cho O. Do đó:

L[B] =  B D E O

Tương tự ta tìm được :

L[C] = C + merge(DO,FO,DF)
     = C + D + merge(O,FO,F)
     = C + D + F + merge(O,O)
     = C D F O

Giờ chúng ta có thể tính được:

L[A] = A + merge(BDEO,CDFO,BC)
     = A + B + merge(DEO,CDFO,C)
     = A + B + C + merge(DEO,DFO)
     = A + B + C + D + merge(EO,FO)
     = A + B + C + D + E + merge(O,FO)
     = A + B + C + D + E + F + merge(O,O)
     = A B C D E F O

Vậy là xong. Chúc các bạn thành công.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.