Python super init множественное наследование

Множественное наследование

Мы продолжаем изучать тему наследования. В языке Python допускается множественное наследование, когда один дочерний класс образуется сразу от нескольких базовых, согласно синтаксису:

class A(base1, base2, …, baseN):

Давайте посмотрим, как это работает и для чего вообще нужно. Признаюсь честно, я не сразу смог придумать учебный пример, где можно было бы показать целесообразность применения множественного наследования. То есть, он применяется не так часто, как обычное наследование от одного класса. Но, тем не менее это тоже важный механизм и некоторые подходы к программированию его активно используют. Например, идея миксинов (mixins) в Python реализуется через множественное наследование. И учебный пример я решил связать именно с ними, а заодно показать еще один прием в программировании.

class Goods: def __init__(self, name, weight, price): print("init MixinLog") self.name = name self.weight = weight self.price = price def print_info(self): print(f", , ")

Пока ничего нового здесь нет. Но потом, к нам подходит тимлид и говорит: — Дорогой сеньор, добавь, пожалуйста, возможность логирования товаров магазина. И как бы вы поступили на месте этого сеньора? Плохой сеньор начнет прописывать логику логирования либо непосредственно в базовом классе Goods, либо уровнем выше (в иерархии наследования). А хороший воспользуется идеей миксинов. Для этого он создаст еще один класс, который можно назвать:

class MixinLog: ID = 0 def __init__(self): print("init MixinLog") self.ID += 1 self.id = self.ID def save_sell_log(self): print(f": товар продан в 00:00 часов")

Этот класс работает совершенно независимо от классов Goods и Notebook и лишь добавляет функционал по логированию товаров с использованием их id. Такие независимые базовые классы и получили название миксинов – примесей. Добавим этот класс в цепочку наследования:

class NoteBook(Goods, MixinLog): pass

И видим ошибку. Очевидно, она связана с тем, что у второго класса MixinLog не был вызван инициализатор. Почему так произошло? Как мы уже знаем, при создании объектов инициализатор ищется сначала в дочернем классе, но так как его там нет, то в первом базовом Goods. Он там есть, выполняется и на этом инициализация нашего объекта NoteBook завершается. Однако, нам нужно также взывать инициализатор и второго базового класса MixinLog. В данном случае, сделать это можно с помощью объекта-посредника super(), которая и делегирует вызов метода __init__ класса MixinLog:

class Goods: def __init__(self, name, weight, price): super().__init__() print("init Goods") self.name = name self.weight = weight self.price = price …

Теперь, после запуска программы, мы видим, что оба инициализатора сработали и ошибок никаких нет. Но откуда функция super() «знает», что нужно обратиться ко второму базовому классу MixinLog, а, скажем, не к базовому классу object, от которого неявно наследуются все классы верхнего уровня? В Python существует специальный алгоритм обхода базовых классов при множественном наследовании. Сокращенно, он называется: MRO – Method Resolution Order И говорит, в каком порядке обходить базовые классы: Мы можем увидеть эту цепочку обхода базовых классов, если распечатать специальную коллекцию __mro__ любого класса:

Читайте также:  Tik tok bot python

В консоли появится следующая последовательность: (, , , ) То есть, методы сначала ищутся в самом классе NoteBook, затем, в классе Goods, далее идет класс MixinLog и от него уже идет к классу object. Это цепочка обхода для нашего конкретного примера. При другой иерархии наследования эта цепочка может быть другой, но одно всегда неизменно –первый базовый класс, указанный при наследовании, выбирается первым (после дочернего, разумеется). И это важный момент. Вы всегда можете быть уверены, что инициализатор первого базового класса сработает в первую очередь. Почему это важно? Смотрите, при создании объекта NoteBook мы передаем ему три аргумента. Эти три аргумента, затем, передаются в инициализатор. И так как первым будет вызван инициализатор класса Goods, то мы уверены, что эти аргументы будут переданы именно в него, а не в какой-то другой инициализатор других базовых классов. И какая бы цепочка наследования у нас ни была, все равно первым будет вызываться метод __init__ класса Goods, потому что он записан первым. Это гарантирует работоспособность нашей программы при разных иерархиях множественного наследования. Ради интереса, давайте поменяем местами базовые классы:

class NoteBook(MixinLog, Goods): pass

И мы сразу получаем ошибку, что в метод __init__ передаются четыре аргумента, а он принимает только один, так как здесь отрабатывает инициализатор уже класса MixinLog. Так что порядок следования базовых классов при множественном наследовании имеет важное значение. Первым должен идти «основной» класс и у него, как правило, инициализатор имеет несколько параметров. А далее, записываются классы, у которых, опять же, как правило, инициализаторы имеют только параметр self. Это второй важный момент. Когда мы собираемся использовать множественное наследование, то структуру классов следует продумывать так, чтобы инициализаторы вспомогательных классов имели только один параметр self, иначе будут сложности их использования. В чем они состоят? Давайте для примера пропишем в инициализаторе класса MixinLog один параметр p1

class MixinLog: def __init__(self, p1): super().__init__(1, 2)
class MixinLog2: def __init__(self, p1, p2): super().__init__() print("init MixinLog 2")

В каждом методе __init__ мы также делаем делегированный вызов инициализатора следующего базового класса. А цепочка наследования будет такой:

class NoteBook(Goods, MixinLog, MixinLog2): pass

Сейчас при запуске у нас не возникает никаких ошибок, так как последовательность MRO имеет вид: (, , , , ) То есть, мы знаем, что функция super() в классе Goods вызовет метод __init__ класса MixinLog с одним дополнительным параметром, а затем, метод __init__ класса MixinLog2 с двумя дополнительными параметрами. И мы «жестко» это прописали. Но, как вы понимаете, если хотя бы немного изменится цепочка наследования, например, так:

class NoteBook(Goods, MixinLog2, MixinLog): pass

то все нарушится и получим ошибки. Чтобы в программах при множественном наследовании не возникало проблем с зависимостью последовательности наследования дополнительных базовых классов, их инициализаторы следует создавать с одним параметром self и в каждом из них прописывать делегированный вызов инициализатора следующего класса командой:

Тогда точно никаких особых проблем при использовании множественного наследования не возникнет. Последнее, что я хочу отметить на этом занятии, это вызов методов с одинаковыми именами из базовых классов. Давайте предположим, что в классе MixinLog имеется метод print_info с тем же именем, что и в классе Goods:

def print_info(self): print("print_info класса MixinLog")

то мы обратимся к методу класса Goods, так как он записан первым в цепочке наследования и в соответствии с алгоритмом обхода MRO он будет найден первым. Но что если мы хотим вызвать этот метод из второго базового класса MixinLog? Как поступить? Сделать это можно двумя способами. Либо напрямую вызвать этот метод через класс MixinLog:

Обратите внимание, что в этом случае нам обязательно нужно указать первым аргументом ссылку на объект класса NoteBook. Либо, определить какой-либо метод в классе NoteBook (пусть он называется также):

class NoteBook(Goods, MixinLog): def print_info(self): MixinLog.print_info(self)

Источник

Python — о множественном наследовании и функции super() простыми словами

Python позволяет указать для класса несколько родителей. Это называется множественным наследованием.

Например, мы хотим добавить какие-то общие свойства нескольким разным классам. Добавлять эти свойства, через класс-наследник для каждого из классов явно некрасиво, нарушает принцип DRY.

Если речь о чем-то простом, то это можно сделать через декоратор. Но если это что-то более развесистое и прикладное, то напрашивается оформить это как объект, и добавить к нужным классам как mixin. В Python нет специального способа добавлять mixin, это осуществляется через множественное наследование.

Загадка

Platypus.__init__() Mammal.__init__() Bird.__init__() Vertebrate.__init__() 

Если хоть один наследник нарушает принципы кооперативного наследования (не вызывает super() ), то метод родителя вообще не будет вызван, хотя вроде бы мы имеем явный вызов этого родителя из другого наследника.

Например, давайте закомментарим вызов super() в классе Bird (строка 8). Вывод изменится следующим образом:

Platypus.__init__() Mammal.__init__() Bird.__init__() 

Причина в том, что из Mammal.__init__ вызывается следующий по MRO класс ( Bird ), а вовсе не родитель Mammal ( Vertebrate ). Родителя ранее вызывал Bird , но мы убрали этот вызов.

В коде ниже я добавил аргументы в __init__ , чтобы проиллюстрировать сказанное выше выдаваемой ошибкой.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
class Vertebrate: def __init__(self): print('Vertebrate.__init__()') class Bird(Vertebrate): def __init__(self, beak_length): print('Bird.__init__()') super().__init__() class Mammal(Vertebrate): def __init__(self, hair_length): print('Mammal.__init__()') super().__init__() class Platypus(Mammal, Bird): def __init__(self): print('Platypus.__init__()') super().__init__(1) duckbill = Platypus() 
Platypus.__init__() Mammal.__init__() . File "animal_class_tree_arguments.py", line 13, in __init__ super().__init__() TypeError: __init__() missing 1 required positional argument: 'beak_length' 

Ошибка показывает, что в Mammal код super().__init__() пытается вызвать Bird.__init__ .

Примечание - особенности работы super()

Одним из ограничений super() является то, что не получится выполнить операции (binary operations, subscriptions и т.д.) над возвращенным объектом, даже если эти операции реализованы в родителе вызывающего класса с помощью “магических методов”.

Если выполнить операцию над экземпляром класса, то Python найдет нужный для выполнения операции “магический метод” в родителе (в примере ниже - __getitem__ для индексирования с помощью оператора [] ).

Но если попытаться выполнить операцию над объектом, возвращаемым super() , получим ошибку:

class Parent: def __getitem__(self, idx): return 0 class Child(Parent): def index_super(self, idx): return super()[idx] kid = Child() print(f'kid[0]: kid[0]>') print(f'kid.index_super(0): kid.index_super(0)>')
kid[0]: 0 . TypeError: 'super' object is not subscriptable 

Источник

Оцените статью