Введение в множественное наследование и super()

Введение в множественное наследование и super() для Python-разработчиков. Также в этой статье мы рассмотрим, как справляться с проблемой алмаза.

Краткий обзор наследования

По мере расширения ваших проектов и пакетов Python вы неизбежно захотите использовать классы и т.о. применять один из фундаментальных принципов программирования – принцип DRY (Don’t repeat yourself – не повторяйся!). Наследование классов — это фантастический способ создать класс на основе другого класса, следуя принципу DRY.

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

Так что же такое наследование классов? Подобно тому, как это происходит в генетике, дочерний класс может «наследовать» атрибуты и методы от родителя. Давайте сразу перейдем к коду для демонстрации и лучшего понимания.

В приведенном ниже блоке кода мы продемонстрируем наследование. У нас есть дочерний класс Child, наследуемый от родительского класса Parent.

class Parent:
    def __init__(self):
        self.parent_attribute = 'I am a parent'

    def parent_method(self):
        print('Back in my day...')


# Create a child class that inherits from Parent
class Child(Parent):
    def __init__(self):
        Parent.__init__(self)
        self.child_attribute = 'I am a child'


# Create instance of child
child = Child()

# Show attributes and methods of child class
print(child.child_attribute)
print(child.parent_attribute)
child.parent_method()

Давайте его запустим и получим следующий результат:

I am a child
I am a parent
Back in my day...

Мы видим, что класс Child «унаследовал» атрибуты и методы от класса Parent. Без какой-либо работы с нашей стороны метод Parent.parent_method является частью класса Child. Чтобы воспользоваться методом Parent.__init__(), нам нужно было явно вызвать метод и передать self. Это потому, что, когда мы добавили метод __init__ в Child, мы перезаписали унаследованный __init__.

После этого краткого и очень неполного обзора давайте перейдем к сути статьи.

[python_ad_block]

Введение в super()

В простейшем случае функцию super() можно использовать для замены явного вызова Parent.__init__(self). Наш вводный пример из первого раздела можно переписать с помощью super(), как это показано ниже.

Обратите внимание, что приведенный ниже блок кода написан на Python 3, более ранние версии используют немного другой синтаксис. Кроме того, вывод был опущен, поскольку он идентичен тому, что мы получили в первом примере.

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

class Parent:
    def __init__(self):
        self.parent_attribute = 'I am a parent'

    def parent_method(self):
        print('Back in my day...')


# Create a child class that inherits from Parent
class Child(Parent):
    def __init__(self):
        super().__init__()
        self.child_attribute = 'I am a parent'


# Create instance of child
child = Child()

# Show attributes and methods of child class
print(child.child_attribute)
print(child.parent_attribute)
child.parent_method()

Честно говоря, super() в данном случае дает нам мало преимуществ, если вообще хоть что-то дает. В зависимости от имени нашего родительского класса мы можем сэкономить несколько нажатий клавиш, и нам не нужно передавать self вызову __init__. Однако для других случаев super() может быть крайне полезен. Ниже приведены некоторые плюсы и минусы использования super() в случаях одиночного наследования.

Минусы

Можно сказать, что использование super() при одиночном наследовании делает код менее явным и интуитивно понятным. Создание менее явного кода нарушает дзен Python, в котором говорится: «Явное лучше, чем неявное».

Плюсы

С точки зрения поддерживаемости super() может быть полезен даже при одиночном наследовании. Если по какой-либо причине ваш дочерний класс меняет свой шаблон наследования (т.е. изменяется родительский класс или происходит переход к множественному наследованию), то нет необходимости искать и заменять все устаревшие ссылки на ParentClass.method_name(). Таким образом, использование super() позволит всем изменениям пройти через изменение в операторе класса.

Super() и множественное наследование

Прежде чем мы перейдем к множественному наследованию и super(), хотим сразу предупредить, что тема непростая. Однако потратьте немного своего времени, перечитайте статью несколько раз, и всё встанет на свои места!

Во-первых, что такое множественное наследование? Предыдущие примеры кода охватывали один дочерний класс Child, наследуемый от одного родительского класса Parent. При множественном наследовании существует более одного родительского класса. Дочерний класс может наследовать от 2, 3, 10 и т.д. родительских классов.

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

Множественное наследование без super()

Давайте рассмотрим пример множественного наследования без изменения каких-либо родительских методов и, в свою очередь, без super().

Наш код будет выглядеть следующим образом:

class B:
    def b(self):
        print('b')


class C:
    def c(self):
        print('c')


class D(B, C):
    def d(self):
        print('d')


d = D()
d.b()
d.c()
d.d()

Вот такой результат мы получим:

b
c
d

Порядок разрешения методов

Полученный вывод не слишком удивителен, учитывая концепцию множественного наследования. D унаследовал методы b и c от своих родительских классов, и все в мире хорошо… пока что.

А что, если и B, и C имеют методы с одинаковыми именами? Именно здесь вступает в действие концепция, называемая «multiple-resolution order» (порядок разрешения методов в Python), или сокращенно MRO.

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

Давайте рассмотрим следующий пример:

class B:
    def x(self):
        print('x: B')


class C:
    def x(self):
        print('x: C')


class D(B, C):
    pass


d = D()
d.x()
print(D.mro())

# Output
# x: B
# [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class 'object'>]

Когда мы вызываем унаследованный метод x, мы видим только выходные данные, унаследованные от класса B.

Мы можем увидеть MRO нашего класса D, вызвав метод класса mro(). Из вывода D.mro() мы узнаем следующее. По умолчанию наша программа попытается вызвать методы D, затем прибегнет к B, затем к C и, наконец, к классу object. Если метод не будет найден ни в одном из этих мест, то мы получим сообщение об ошибке, говорящее, что в D нет запрошенного нами метода.

Стоит отметить, что по умолчанию каждый класс наследуется от класса object, который и находится в конце каждого MRO.

Множественное наследование, super() и проблема алмаза

Давайте разберем пример использования super() для обработки порядка разрешения методов __init__ более выгодным образом. В примере мы создаем серию классов обработки текста и объединяем их функциональность в другом классе с множественным наследованием. Мы создадим 4 класса, и структура наследования будет соответствовать структуре, показанной на диаграмме ниже.

Примечание. Эта структура предназначена для иллюстративных целей, и чаще всего существуют более удачные способы реализации этой логики.

На самом деле это пример «проблемы алмаза» множественного наследования. Название проблемы связано с формой фигуры, которую образует схема наследования.

Код, представленный ниже, написан с использованием super():

class Tokenizer:
    """Tokenize text"""
    def __init__(self, text):
        print('Start Tokenizer.__init__()')
        self.tokens = text.split()
        print('End Tokenizer.__init__()')


class WordCounter(Tokenizer):
    """Count words in text"""
    def __init__(self, text):
        print('Start WordCounter.__init__()')
        super().__init__(text)
        self.word_count = len(self.tokens)
        print('End WordCounter.__init__()')


class Vocabulary(Tokenizer):
    """Find unique words in text"""
    def __init__(self, text):
        print('Start init Vocabulary.__init__()')
        super().__init__(text)
        self.vocab = set(self.tokens)
        print('End init Vocabulary.__init__()')


class TextDescriber(WordCounter, Vocabulary):
    """Describe text with multiple metrics"""
    def __init__(self, text):
        print('Start init TextDescriber.__init__()')
        super().__init__(text)
        print('End init TextDescriber.__init__()')


td = TextDescriber('row row row your boat')
print('--------')
print(td.tokens)
print(td.vocab)
print(td.word_count)

Запустим наш код и получим следующий результат:

Start init TextDescriber.__init__()
Start WordCounter.__init__()
Start init Vocabulary.__init__()
Start Tokenizer.__init__()
End Tokenizer.__init__()
End init Vocabulary.__init__()
End WordCounter.__init__()
End init TextDescriber.__init__()
--------
['row', 'row', 'row', 'your', 'boat']
{'boat', 'your', 'row'}
5

Прежде всего, мы видим, что класс TextDescriber унаследовал все атрибуты генеалогического дерева классов. Благодаря множественному наследованию мы можем объединять функциональность более чем одного класса.

Давайте теперь обсудим результаты, полученные от методов инициализации класса:

Каждый метод __init__ вызывался один и только один раз.

Класс TextDescriber унаследован от двух классов, унаследованных от Tokenizer. Почему Tokenizer.__init__ не вызывался дважды?

Если бы мы заменили все наши вызовы super() старомодным способом, мы получили бы 2 вызова Tokenizer.__init__. Использование super() заранее «обдумывает» наш код и пропускает лишнее действие в виде двойного вызова Tokenizer.__init__.

Каждый метод __init__ был запущен до того, как любой из других был завершен.

На порядок начала и окончания каждого __init__ стоит обращать внимание, если вы пытаетесь установить атрибут, имя которого конфликтует с другим родительским классом. Атрибут будет перезаписан, и это может все запутать.

В нашем случае мы избежали конфликтов имен с унаследованными атрибутами, поэтому все работает как положено.

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

Заключение

Итак, сегодня мы поговорили про множественное наследование и super(). Мы узнали о функции super() и о том, как ее можно использовать для замены ParentName.method в одиночном наследовании. Это может быть более удобной практикой.

Также мы обсудили множественное наследование и о то, как мы можем передать функциональность нескольких родительских классов одному дочернему классу.

Кроме того, мы узнали о порядке разрешения методов MRO и о том, как он решает, что происходит при множественном наследовании, когда возникает конфликт имен между родительскими методами.

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

Перевод статьи «Intro to Multiple Inheritance & super()».