В первой части мы разобрали, основы того, как происходит вызов классов в иерархии Python. Давайте же рассмотрим обещанные интересности.
Отсутствие обращения к родителю
Изменим пример из первой части. Пускай класс B полностью реализует всю функциональность без обращения к родителю (я не призываю так делать без четкого осознания, что вы делаете и зачем, но такое возможно), наш новый пример примет вид (приведу его полностью):
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
class A: def test(self): print('A') return 'A' class B(A): def test(self): print('B') return 'B' 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() class F1(D, E): def test(self): print('F1') return 'F1_' + super().test() class F2(E, D): def test(self): print('F2') return 'F2_' + super().test() def test_method(cls): print(cls) print(cls().test()) if __name__ == '__main__': test_method(A) test_method(B) test_method(C) test_method(D) test_method(E) test_method(F1) test_method(F2) |
Запуск этого тестового примера выдаст на экран следующий результат:
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 29 30 31 32 33 |
<class '__main__.A'> A A <class '__main__.B'> B B <class '__main__.C'> C A CA <class '__main__.D'> D B DB <class '__main__.E'> E C A ECA <class '__main__.F1'> F1 D B F1_DB <class '__main__.F2'> F2 E C D B F2_ECDB |
Результаты для классов A, C, E – остались не изменёнными, вызов методов для B, D – теперь не приводит к вызову метода А (так как метод в классе B больше не обращается к родителю). Но куда более интересное поведение мы имеем с классами F1 и F2. Для класса F1 мы получили короткую цепочку F1->D->B (тут вызов методов оборвался, причем он не пошел дальше по соседней цепочке). Класс F2 также демонстрирует “странное” поведение его цепочка более логична F2->E->C->D->B, но в ней отсутствует вызов метода класса A из иерархии E -> C-> A. Хотя правило линеаризации подсказывало, что метод бы должен был быть.
Из полученного опыта выплывает следующие: отсутствие обращения к родителю отключает этот уровень иерархии, даже для соседней ветки.
Переопределенные методы
Что бы проиллюстрировать следующую особенность немного видео изменим исходный пример (из первой статьи):
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
class A: def test(self): print('A') return self.response_method() def response_method(self): return 'A' class B(A): def test(self): print('B') return 'B' + super().test() def response_method(self): return '_A1' 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() def response_method(self): return '_A2' class F1(D, E): def test(self): print('F1') return 'F1_' + super().test() class F2(E, D): def test(self): print('F2') return 'F2_' + super().test() def test_method(cls): print(cls) print(cls().test()) if __name__ == '__main__': test_method(A) test_method(B) test_method(C) test_method(D) test_method(E) test_method(F1) test_method(F2) |
В этом примере в родительском классе мы ввели дополнительный метод, который вызывается из основного, а затем переопределили его поведение ниже по иерархии. При запуске получим следующий результат:
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 29 30 31 32 33 34 35 36 37 38 39 40 |
<class '__main__.A'> A A <class '__main__.B'> B A B_A1 <class '__main__.C'> C A CA <class '__main__.D'> D B A DB_A1 <class '__main__.E'> E C A EC_A2 <class '__main__.F1'> F1 D B E C A F1_DBEC_A1 <class '__main__.F2'> F2 E C D B A F2_ECDB_A2 |
В приведенном выводе стоит обратить внимание на следующие в иерархии, при поиске методов используете тот же самый алгоритм реалинизации (но при этом используется первый найденный метод). Для иерархии F1->D->B->E->C->A был вызван метод из класса В. В то время для иерархии F2->E->C->D->B->A был вызван метод класса Е.
Управление иерархией вызовов
При этом если рассмотреть пример с уточнением вызова родителей:
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
class A: def test(self): print('A') return self.response_method() def response_method(self): return 'A' class B(A): def test(self): print('B') return 'B' + super().test() def response_method(self): return '_A1' 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() def response_method(self): return '_A2' 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() def test_method(cls): print(cls) print(cls().test()) if __name__ == '__main__': test_method(A) test_method(B) test_method(C) test_method(D) test_method(E) test_method(F1) test_method(F2) |
То результат по выбору переопределенного метода будет совпадать с предыдущим експериментом:
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 29 30 31 |
<class '__main__.A'> A A <class '__main__.B'> B A B_A1 <class '__main__.C'> C A CA <class '__main__.D'> D B A DB_A1 <class '__main__.E'> E C A EC_A2 <class '__main__.F1'> F1 C A F1_C_A1 <class '__main__.F2'> F2 B A F2_B_A2 |
Стоит отметить, что не смотря на то, что фактически вызов метода из класса С не происходил, Python все равно выбирал переопределенный метод из него (это связано с тем, что поиск метода начинается с текущего класса и идет вверх по родителям, его наше переопределение вызовов родителей не затронуло).
Пример с вызовом родителя через имя класса, ведет себя аналогичным образом. Если изменить классы F1 и F2, из примера выше, следующим образом:
1 2 3 4 5 6 7 8 9 10 |
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 15 16 17 18 |
<class '__main__.F1'> F1 E C A F1_EC_A1 <class '__main__.F2'> F2 D B A F2_DB_A2 |
Тут тоже поиск вызываемого метода происходил снизу иерархии. При этом если бы, например класс F1 переопределил метод response_method, то был бы вызван именно он.
Вариант более сложной иерархии
Давайте немного усложним пример, заменим классы F одним классом, введем еще уровни в иерархию:
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
class A: def test(self): print('A') return self.response_method() def response_method(self): return 'A' class B(A): def test(self): print('B') return 'B' + super().test() def response_method(self): return '_A1' 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() def response_method(self): return '_A2' class F(D, E): def test(self): print('F') return 'F_' + super().test() class G(F): def test(self): print('G') return 'G' + super().test() class J(F): def test(self): print('J') return 'J' + super().test() class M1(G, J): def test(self): print('M1') return 'M1_' + super().test() class M2(J, G): def test(self): print('M2') return 'M2_' + super().test() def test_method(cls): print(cls) print(cls().test()) if __name__ == '__main__': test_method(A) test_method(B) test_method(C) test_method(D) test_method(E) test_method(F) test_method(G) test_method(J) test_method(M1) test_method(M2) |
Результат выполнения этой программы будет довольно большим но интересным:
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
<class '__main__.A'> A A <class '__main__.B'> B A B_A1 <class '__main__.C'> C A CA <class '__main__.D'> D B A DB_A1 <class '__main__.E'> E C A EC_A2 <class '__main__.F'> F D B E C A F_DBEC_A1 <class '__main__.G'> G F D B E C A GF_DBEC_A1 <class '__main__.J'> J F D B E C A JF_DBEC_A1 <class '__main__.M1'> M1 G J F D B E C A M1_GJF_DBEC_A1 <class '__main__.M2'> M2 J G F D B E C A M2_JGF_DBEC_A1 Process finished with exit code 0 |
Самые интересные результаты демонстрируют нижние классы в иерархии M1 и M2. Цепочка вызовов M2->J->G->F->D->B->E->C->A (при этом будет вызван переопределенный метод из класса В). Цепочка вызовов M1->G->J->F->D->B->E->C->A (при этом будет вызван переопределенный метод из класса В). Вызов метода из класса В можно объяснить тем, что предки класса F расположены в порядке сначала D затем E (поэтому сначала всегда анализируются методы из цепочки по D). Куда более интересно обратить внимание на то, что происходит сначала вызов метода родителя, затем его соседа, а уже затем их общего родителя.