Алмазная проблема - множественное наследование python - метод вызывается только один раз - но как? - PullRequest
2 голосов
/ 28 апреля 2020

В этом примере ниже метод m для класса A вызывается только один раз.

Я понимаю, что это особенность, это способ Pythoni c для решения проблемы, при которой метод A m будет вызван дважды (если он был реализован наивным способом) в этот алмазоподобный сценарий наследования.

Это все здесь описано:
https://www.python-course.eu/python3_multiple_inheritance.php

(1) Но под капотом ... как они достигли такого поведения, то есть этого класса A 'm метод вызывается ТОЛЬКО один раз?!
В упрощенном виде: какая строка "пропускается" во время выполнения - это строка #1 или строка # 2?

Может ли кто-нибудь пролить на это больше света?
Я никогда не использовал серьезно множественное наследование, потому что я в основном программирую на Java. Так что мне действительно любопытно узнать об этом сценарии здесь, а точнее о внутренней работе, стоящей за ним.

Примечание: я просто хочу получить общее представление о том, как это работает в Python, не очень-то разбираясь в каждой крошечной детали здесь.

(2) Что если я захочу (в этом же сценарии и по какой-то причине) метод A m вызываться дважды (или N раз в зависимости от того, сколько базовых классы D у нас есть) , пока еще используется super(). Это возможно? super() поддерживает такой режим работы?

(3) Это просто какое-то дерево или DAG алгоритм посещения, где они отслеживают, какой метод класса m уже был посещен, и просто не посещают его (не вызывают) дважды ? Если так, то упрощенно говоря Я думаю, "# 2" - это пропущенная строка.

class A:
    def m(self):
        print("m of A called")

class B(A):
    def m(self):
        print("m of B called")
        super().m() # 1 

class C(A):
    def m(self):
        print("m of C called")
        super().m()  # 2 

class D(B,C):
    def m(self):
        print("m of D called")
        super().m()


if (__name__ == '__main__'):
    x = D()
    x.m()

1 Ответ

5 голосов
/ 28 апреля 2020

Это связано с Порядком разрешения метода , о котором статья, на которую вы ссылались, уже дала некоторое представление (и дополнительную информацию из этой другой статьи, а также ):

Возникает вопрос, как суперфункции принимают решение. Как он решает, какой класс следует использовать? Как мы уже упоминали, он использует так называемый порядок разрешения методов (MRO). Он основан на алгоритме C3 линеаризации суперкласса . Это называется линеаризацией, потому что древовидная структура разбита на линейный порядок. Для создания этого списка можно использовать метод mro:

>>> from super_init import A,B,C,D`  
>>> D.mro() [<class 'super_init.D'>, <class 'super_init.B'>, <class 'super_init.C'>, <class 'super_init.A'>, <class 'object'>]`

Обратите внимание на MRO, откуда он идет от D> B> C> A. Если вы полагаете, что super() просто вызывает родительский класс текущей области - это не так. Он просматривает класс MRO вашего объекта (то есть D.mro()) с текущим классом (то есть B, C ...), чтобы определить, какой следующий класс в строке разрешит Метод.

super() фактически использует два аргумента, но при вызове с нулевыми аргументами внутри класса он неявно передается:

Также обратите внимание, что, кроме формы с нулевым аргументом, super() не ограничивается использованием внутренних методов. Форма с двумя аргументами точно определяет аргументы и делает соответствующие ссылки. Форма с нулевым аргументом работает только внутри определения класса, так как компилятор заполняет необходимые детали для правильного извлечения определяемого класса, а также доступа к текущему экземпляру для обычных методов.

Чтобы быть точным в точке B.m() вызов super() фактически преобразуется в:

super(B, x).m()
# because the self being passed at the time is instance of D, which is x

Этот вызов разрешается в пределах D.mro() начиная с класса B, который фактически равен C, не A, как вы себе представляли. Следовательно, сначала вызывается C.m(), а внутри него super(C, x).m() разрешается до A.m(), и это называется.

После этого он разрешается обратно после super() в пределах C.m(), резервное копирование до super() в пределах B.m() и резервное копирование до D.m(). Это легко заметить, если добавить еще несколько строк:

class A:
    def m(self):
        print("m of A called")

class B(A):
    def m(self):
        print("m of B called")
        print(super())
        super().m() # resolves to C.m        
        print('B.m is complete')

class C(A):
    def m(self):
        print("m of C called")
        print(super())
        super().m()  # resolves to A.m
        print('C.m is complete')

class D(B,C):
    def m(self):
        print("m of D called")  
        print(super())
        super().m() # resolves to B.m
        print('D.m is complete')

if (__name__ == '__main__'):
    x = D()
    x.m()
    print(D.mro())

Что приводит к:

m of D called
<super: <class 'D'>, <D object>>
m of B called
<super: <class 'B'>, <D object>>
m of C called
<super: <class 'C'>, <D object>>
m of A called
C.m is complete  # <-- notice how C.m is completed before B.m
B.m is complete
D.m is complete
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

Таким образом, в действительности ничего никогда не вызывается дважды или пропускаются. Вы просто неверно истолковали идею разрешения MRO из вызова на основе области, в которой находится super(), в отличие от вызова из исходного объекта .


Вот еще одна забава Небольшой пример для демонстрации MRO более подробно:

def print_cur_mro(cls, obj):
    # helper function to show current MRO
    print(f"Current MRO: {' > '.join([f'*{m.__name__}*' if m.__name__ == cls.__name__ else m.__name__ for m in type(obj).mro()])}")

class X:
    def m(self):
        print('m of X called')
        print_cur_mro(X, self)
        try:
            super().a_only() # Resolves to A.a_only if called from D(), even though A is not in X inheritance
        except AttributeError as exc:
            # Resolves to AttributeError if not called from D()
            print(type(exc), exc)
        print('X.m is complete')

class A:
    def m(self):
        print("m of A called")
        print_cur_mro(A, self)

    def a_only(self):
        print('a_only called')

class B(X):
    def m(self):
        print("m of B called")
        print_cur_mro(B, self)
        super().m() # Resolves to X.m
        print('B.m is complete')

    def b_only(self):
        print('b_only called')

class C(A):
    def m(self):
        print("m of C called")
        print_cur_mro(C, self)
        try:
            super().b_only() # Resolves to AttributeError if called, since A.b_only doesn't exist if from D()
        except AttributeError as exc:
            print(type(exc), exc)
        super().m() # Resolves to A.m
        print('C.m is complete')

    def c_only(self):
        print('c_only called, calling m of C')
        C.m(self)

class D(B,C):
    def m(self):
        print("m of D called")
        print_cur_mro(D, self)
        super().c_only() # Resolves to C.c_only, since c_only doesn't exist in B or X.
        super().m() # Resolves to B.m
        print('D.m is complete')

if (__name__ == '__main__'):
    x = D()
    x.m()
    print(D.mro())
    x2 = X()
    x2.m()
    print(X.mro())

Результат:

# x.m() call:
m of D called
Current MRO: *D* > B > X > C > A > object
c_only called, calling m of C
m of C called
Current MRO: D > B > X > *C* > A > object
<class 'AttributeError'> 'super' object has no attribute 'b_only'
m of A called
Current MRO: D > B > X > C > *A* > object
C.m is complete
m of B called
Current MRO: D > *B* > X > C > A > object
m of X called
Current MRO: D > B > *X* > C > A > object
a_only called
X.m is complete
B.m is complete
D.m is complete

# D.mro() call:
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.X'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

# x2.m() call:
m of X called
Current MRO: *X* > object
<class 'AttributeError'> 'super' object has no attribute 'a_only'
X.m is complete

# X.mro() call:
[<class '__main__.X'>, <class 'object'>]
...