Давайте разберемся как работает вызов иерархических методов в Python.
Для начала самый простой вариант (в котором все понятно):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
class A: def test(self): print('A') return 'A' class B(A): def test(self): print('B') return 'B' + super().test() class C(A): def test(self): print('C') return 'C' + super().test() class D(B): def test(self): print('D') return 'D' + super().test() class E(C): def test(self): print('E') return 'E' + super().test() |
Есть иерархия классов (А это корень иерархии), заним потомки B и C, и самый низ иерархии D и E. Суть примера в том, что каждый из потомков вносит какое то изменение в функционал родителя. Имеем вот такую процедуру проверки:
1 2 3 |
def test_method(cls): print(cls) print(cls().test()) |
Этот код принимает класс на входе, печатает его название, создает экземпляр класса и вызывает метод test().
Выполним код:
1 2 3 4 5 6 |
if __name__ == '__main__': test_method(A) test_method(B) test_method(C) test_method(D) test_method(E) |
В результате получаем вполне ожидаемый вывод:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<class '__main__.A'> A A <class '__main__.B'> B A BA <class '__main__.C'> C A CA <class '__main__.D'> D B A DBA <class '__main__.E'> E C A ECA |
При вызове метода, происходит вызов метода родителя по цепочки до вершины иерархии. Для любого кто знаком с ООП это стандартное поведение которое и ожидается. Однако если ввести в иерархию еще один уровень то ситуация усложняется.
Простой пример ромбовидного наследования
Рассмотрим случай ромбовидного наследования, введём в иерархию еще два класса (забегая вперед два потому, что их поведение будет разнится в зависимости от порядка указания родителей)
1 |
class F1(D, E):<br> def test(self):<br> print('F1')<br> return 'F1_' + super().test()<br><br>class F2(E, D):<br> def test(self):<br> print('F2')<br> return 'F2_' + super().test() |
Для этих двух классов вывод нашего теста будет следующим:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<class '__main__.F1'> F1 D B E C A F1_DBECA <class '__main__.F2'> F2 E C D B A F2_ECDBA |
Самое интересное что стоит заменить, что были вызваны абсолютно все методы родителей (включая, что может показаться неожиданным, так сказать боковых родителей). Например для F1 сначала вызывается методы в класса D и B (это его прямые родители), потом “неожиданно” E и C и затем только завершается все вызовом метода класса A.
И тут отметим первое правило при таких вызовах, каждый метод будет вызван только один раз.
Второй не менее интересный момент это наличие правила “линеаризации”, суть которого в том, что сначала обходится иерархия и выстраивается порядок вызовов, при построении порядка если метод встречается повторно то всегда остается более поздний вызов. В примере с классом F1, метод класса А вызывается последним. Это происходит потому что сначала происходит обход иерархии первого класса D, в конце прохождения этой иерархии мы имеем такую цепочку D->B->A, затем анализируется цепочка E->C->A, при их объединении метод класса А, встретится дважды именно второй вызов и останется. При этом если в заголовке сменить порядок следования классов родителей, как в примере с классом F2, то мы получим другой порядок сначала цепочка E->C->A, а затем цепочка D->B->A и конечный результат будет F2->E->C->D->B->A.
Управление направлением подъема по иерархии
Встроенный метод super с параметрами
Но что если в одной ситуации вы захотите пройти по левой цепочке, а в каких-то по правой? Обратимся к встроенному методу super. Его можно вызвать с параметрами передав в качестве параметра имя класса и ссылку.
Модифицируем пример классы F1 и F2, следующим образом:
1 2 3 4 5 6 7 8 9 |
class F1(D, E): def test(self): print('F1') return 'F1_' + super(E, self).test() class F2(E, D): def test(self): print('F2') return 'F2_' + super(D, self).test() |
И мы получим совсем другой вывод:
1 2 3 4 5 6 7 8 9 10 |
<class '__main__.F1'> F1 C A F1_CA <class '__main__.F2'> F2 B A F2_BA |
В этом случае цепочки стали короче, там нет вызова методов по соседней ветке иерархии. Однако, что странно куда то подевались вызовы непосредственных родителей для F1 это метод класса D, а для F2 – класса E. И вот тут нас ждет сюрприз, что super возвращает ссылку не указанного класса, а его родителя. Сразу оговорюсь что изменение вызова на return ‘F1_’ + super(F1, self).test() выдаст тот же результат, что и super вообще без параметров.
Вызов метода через класс
Но а что делать если нужно полностью пройти по одной цепочке? В этом случае можно вспомнить, что методы класса можно вызвать непосредственно на самом классе и просто передать ему ссылку на объект, например:
1 2 3 4 5 6 7 8 9 |
class F1(D, E): def test(self): print('F1') return 'F1_' + E.test(self) class F2(E, D): def test(self): print('F2') return 'F2_' + D.test(self) |
Новые изменения в наши многострадальные классы привели к следующему выводу:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<class '__main__.F1'> F1 E C A F1_ECA <class '__main__.F2'> F2 D B A F2_DBA |
И тут все как мы хотели прошли по выбранной цепочке, при этом в примере специально не выбран первый из родителей, что бы показать, что цепочка идет по выбранному пути.
И все бы ничего, если бы ни несколько ложек дегтя, но об этом в следующей “серии”.