Порядок вирішення методів (MRO) у класах нового стилю?


94

У книзі Python in arathell (2-е видання) є приклад, який використовує
класи старих стилів, щоб продемонструвати, як методи вирішуються в класичному порядку роздільної здатності, і
як це відрізняється від нового порядку.

Я спробував той самий приклад, переписавши приклад у новий стиль, але результат не відрізняється від того, що був отриманий у класах старого стилю. Версія 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 з новими класами стилів є неправильним, чи я роблю дурну помилку, яку не можу виявити. Будь ласка, допоможіть мені краще зрозуміти МРО.

Відповіді:


184

Найважливіша різниця між порядком роздільної здатності для застарілих класів та класами нового стилю виникає, коли один і той самий клас предків зустрічається більше одного разу у "наївному" підході, що застосовується на основі глибини - наприклад, розглянемо випадок "успадкування алмазів":

>>> 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 [змушений] прийти в порядку роздільної здатності лише один раз і після всіх своїх підкласів, так що заміни (тобто перевизначення C члена x) насправді працюють розумно." - Богоявлення! Завдяки цьому реченню я можу знову зробити МРО у своїй голові. \ o / Дякую.
Esteis

23

Порядок роздільної здатності методу Python насправді є більш складним, ніж просто розуміння діамантового малюнка. Щоб насправді це зрозуміти, погляньте на лінеаризацію С3 . Я виявив, що це дійсно допомагає використовувати оператори друку при розширенні методів для відстеження замовлення. Наприклад, яким, на вашу думку, буде результат цього шаблону? (Примітка: `` 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()

Після численних помилок проб, я придумав неформальну інтерпретацію теорії графів лінеаризації С3 наступним чином: (Хтось, будь ласка, дайте мені знати, якщо це неправильно.)

Розглянемо цей приклад:

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" як перший рядок, а також використовували супер, тому він знаходить супер клас "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

Ви бачите таку поведінку, оскільки роздільна здатність методу визначається глибиною, а не шириною. Спадщина Дервіда виглядає так

         Base2 -> Base1
        /
Derived - Base3

Так instance.amethod()

  1. Перевіряє Base2, не знаходить методу.
  2. Переглядає, що Base2 успадкував від Base1, і перевіряє Base1. Base1 має a amethod, тому він викликається.

Це відображено в Derived.__mro__. Просто повторіть Derived.__mro__і зупиніться, коли знайдете шуканий метод.


Я сумніваюся, що причиною, що я отримую "Base1" як відповідь, є те, що роздільна здатність методу є першою по глибині, я думаю, що це не лише підхід, що стосується глибини. Див. Приклад Дениса, якби спочатку глибина, o / p мала б бути "Base1". Також зверніться до першого прикладу в наданому вами посиланні, там також показаний MRO вказує на те, що роздільна здатність методу визначається не просто шляхом обходу в порядку першої глибини.
sateesh

Вибачте, посилання на документ про MRO надає Денис. Перевірте це, я помилково вважав, що ви надали мені посилання на python.org.
sateesh

4
Як правило, це глибина, але є розумні способи обробки алмазоподібної спадщини, як пояснив Алекс.
jamessan
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.