Множественное наследование и миксины в Python

Spread the love

Перевод статьи: Leonardo Giordani — Multiple inheritance and mixin classes in Python

Недавно я пересмотрел три своих старых статьи о представлениях на основе классов Django (class-based views), которые написал для своего блога, обновив их до Django 3.0 (вы можете найти их здесь), и еще раз обнаружил, большое количество кода использующего классы mixin для улучшения повторного использования кода. По своему опыту я понял, что миксины не очень популярны в Python, поэтому решил изучить их лучше, тем самым освежив свои знания теории ООП.

Чтобы полностью оценить содержание поста, убедитесь, что вы усвоили два столпа ООП: делегирование, в частности, как оно реализуется посредством наследования, и полиморфизм. Этот пост подробнее расскажет о делегировании а этот пост о полиморфизме.

Множестенное наследование: благодать и проклятье

Общая концепция

Чтобы обсудить миксины, мы должны начать с одного из самых спорных вопросов в мире ООП: множественное наследование. Это естественное расширение концепции простого наследования, когда класс автоматически делегирует разрешенные метода и атрибуты другому классу (от родительского классу к дочернему).

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

Делегирование было введено в ООП как способ уменьшить дублирование кода. Когда объекту нужна определенная функция, он просто делегирует ее другому классу (явно или неявно), поэтому код пишется только один раз.

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

      assignable reviewable item
 (assign_to_user, ask_review_to_user)
                 ^
                 |
                 |
                 |
            pull request

которая позволяет нам помещать в pull request только определенный код, требуемый этим элементом. Вызовы методов и делегирование — это не что иное, как сообщения между объектами, поэтому иерархия делегирования — это простая сетевая система.

К сожалению, использование наследования вместо композиции часто приводит к системам, которые, как это ни парадоксально, увеличивают дублирование кода. Основная проблема заключается в том, что наследование может напрямую делегироваться только одному классу (родительскому классу), в отличие от композиции, где объект может делегировать любому количеству других классов. Это ограничение наследования означает, что у нас может быть класс, который наследует от другого, потому что ему нужны некоторые его функции, но при этом так же получает все другие функции, которые он не хочет или не должен иметь.

Давайте продолжим пример портала управления кодом и рассмотрим проблему, которая представляет собой элемент, который мы хотим сохранить в системе, но который пользователь не может просмотреть. Если мы создадим иерархию, как это

      assignable reviewable item
   (assign_to_user, ask_review_to_user)
                   ^
                   |
                   |
                   |
                   |
          +--------+--------+
          |                 |
          |                 |
          |                 |
        issue          pull request
   (not reviewable)

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

          assignable item
         (assign_to_user)
                 ^
                 |
                 |
                 |
                 |
          +------+--------------+
          |                     |
          |                     |
          |                     |
          |         reviewable assignable item
          |            (ask_review_to_user)
          |                     ^
          |                     |
          |                     |
          |                     |
        issue              pull request

Тем не менее, этот подход перестает быть жизнеспособным, как только объект должен наследовать от данного класса, но не от родителя этого класса. Например, элемент, который должен быть рецензируемым, но не назначаемым, например, best practice, которую мы хотим добавить на сайт. Если мы хотим продолжать использовать наследование, единственным решением на этом этапе является дублирование кода, который реализует рецензируемую природу элемента (или код, который реализует назначаемую функцию) и создание двух разных иерархий классов.

          assignable item              +-------->  reviewable item
         (assign_to_user)              |         (ask_review_to_user)
                 ^                     |                  ^
                 |                     |                  |
                 |                     |                  |
                 |             CODE DUPLICATION           |
                 |                     |                  |
          +------+--------------+      |                  |
          |                     |      |                  |
          |                     |      |                  |
          |                     |      V                  |
          |         reviewable assignable item            |
          |            (ask_review_to_user)               |
          |                     ^                         |
          |                     |                         |
          |                     |                         |
          |                     |                         |
        issue              pull request             best practice

Обратите внимание, что это даже не принимает во внимание, что новый reviewable item может нуждаться в атрибутах assignable item, что требует другого уровня глубины в иерархии, где мы выделяем эти функции в более общем классе. Так что, к сожалению, есть вероятность, что это только первый из многих компромиссов, которые нам придется принять, чтобы сохранить систему в стабильном состоянии, если мы не сможем изменить наш подход.

Затем в ООП было введено множественное наследование, поскольку было ясно, что объект может захотеть делегировать определенные действия данному классу, а другие действия другому, имитируя, объекты из реальной жизни, когда мы наследуем признаки от нескольких предков (родителей, бабушек и дедушек) , и т.д.).

Вышеупомянутая ситуация может быть затем решена с помощью pull request наследования как от класса, который предоставляет функции assign, так и от класса, который реализует функции reviewable.

          assignable item                          reviewable item
         (assign_to_user)                        (ask_review_to_user)
                 ^                                      ^  ^
                 |                                      |  |
                 |                                      |  |
                 |                                      |  |
                 |                                      |  |
          +------+-------------+ +----------------------+  |
          |                    | |                         |
          |                    | |                         |
          |                    | |                         |
          |                    | |                         |
          |                    | |                         |
          |                    | |                         |
          |                    | |                         |
          |                    | |                         |
          |                    | |                         |
        issue              pull request              best practice

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

Это всего лишь примеры, которые могут быть действительными или нет, в зависимости от конкретного случая, но они ясно показывают проблемы, которые могут возникнуть у нас даже при очень простой иерархии из 4 классов. Многие из этих проблем явно возникают из-за того, что мы хотели реализовать делегирование только посредством наследования, и я осмелюсь сказать, что 80% архитектурных ошибок в проектах ООП происходят из-за использования наследования вместо композиции и использования God объектов(божественных объектов), то есть классов, которые несут ответственность за слишком много различных частей системы. Всегда помните, что ООП родился с идеей взаимодействия небольших объектов посредством сообщений, поэтому соображения, которые мы учитываем для монолитных архитектур, верны и здесь.

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

Почему это так противоречиво?

Учитывая то, что я только что сказал, множественное наследование кажется благословением. Когда объект может наследоваться от нескольких родителей, мы можем легко распределять обязанности между различными классами и использовать только те, которые нам нужны, способствуя повторному использованию кода и избегая «божественных» объектов.

К сожалению, все не так просто. Прежде всего, мы сталкиваемся с проблемой, с которой сталкивается каждая микросервисно-ориентированная архитектура, то есть с риском перехода от «божественных» объектов (экстремальная монолитная архитектура) к почти пустым объектам (экстремально распределенный подход), обременяя программиста слишком тонким детальным контролем, который в конечном итоге приводит к созданию системы, в которой отношения между объектами настолько сложны, что становится невозможным понять влияние изменений в коде.

Однако в множественном наследовании существует более насущная проблема. Как это происходит с естественным наследованием, родители могут предоставить одну и ту же «генетическую черту» в двух разных вариантах, но у получающегося в результате этого человека будет только одна. Оставляя в стороне генетику (которая невероятно сложнее, чем программирование) и возвращаясь к ООП, мы сталкиваемся с проблемой, когда объект наследуется от двух других объектов, которые предоставляют тот же атрибут.

Итак, если ваш класс Child наследует от родителей Parent1 и Parent2, и оба предоставляют метод __init__, какой из них следует использовать вашему объекту?

class Parent1():
    def __init__(self):
        [...]


class Parent2():
    def __init__(self):
        [...]


class Child(Parent1, Parent2):
    # наследуется Parent1 и Parent2, какой __init__ будет использоваться?
    pass


Ситуация может ухудшиться, так как родители могут иметь разные входные параметры общего метода, например

class Parent1:
    def __init__(self, status):
        [...]


class Parent2:
    def __init__(self, name):
        [...]


class Child(Parent1, Parent2):
    # какой __init__ используется?
    pass


Проблема может быть расширена еще дальше, введя общего предка выше Parent1 и Parent2.

class Ancestor:
    def __init__(self):
        [...]


class Parent1(Ancestor):
    def __init__(self, status):
        [...]


class Parent2(Ancestor):
    def __init__(self, name):
        [...]


class Child(Parent1, Parent2):
    pass


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

      Ancestor
       ^   ^
      /     \
     /       \
Parent1     Parent2
    ^         ^
     \       /
      \     /
       Child

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

Как мы вскоре увидим, есть решения этой проблемы, но этот дополнительный уровень сложности делает множественное наследование чем-то, что не очень легко вписывается в дизайн. Помните, что наследование — это механизм автоматического делегирования. По этим причинам множественное наследование часто изображается как пугающее и запутанное, и, как правило, ему уделяется время только в продвинутых курсах ООП, по крайней мере, в мире Python. Я считаю, что каждый программист на Python должен ознакомиться с ним и узнать, как им воспользоваться.

Множественное наследование: путь Python

Посмотрим, как можно решить алмазную проблему. В отличие от генетики, мы, программисты, не можем позволить себе какой-либо уровень неопределенности или случайности в наших процессах, поэтому при наличии возможной неоднозначности, как та, которая создается множественным наследованием, нам необходимо записать правило, которое будет строго соблюдаться в каждом случае. В Python это правило называется MRO (Method Resolution Order — Порядок разрешения методов), которое было введено в Python 2.3 и описано в этом документе Микеле Симионато.

Можно многое сказать о MRO и лежащем в основе алгоритме линеаризации C3, но для целей этого поста достаточно посмотреть, как он решает проблему алмазов. В случае множественного наследования Python следует обычным правилам наследования (автоматическое делегирование предку, если атрибут не присутствует локально), но порядок следования по дереву наследования теперь включает все классы, указанные в сигнатуре класса. В приведенном выше примере Python будет искать атрибуты в следующем порядке: Child, Parent1, Parent2, Ancestor.

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

class Ancestor:
    def rewind(self):
        [...]


class Parent1(Ancestor):
    def open(self):
        [...]


class Parent2(Ancestor):
    def open(self):
        [...]

    def close(self):
        [...]

    def flush(self):
        [...]


class Child(Parent1, Parent2):
    def flush(self):
        [...]


В этом случае экземпляр c Child обеспечит rewind, open, close и flush. Когда вызывается c.rewind, выполняется код в Ancestor, так как это первый класс в списке MRO, который предоставляет этот метод. Метод open предоставляется Parent1, а closeParent2. Если вызывается метод c.flush, код предоставляется самим классом Child, который переопределяет его, переопределяя класс, предоставленный Parent2.

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

Под капотом

Как множественное наследование работает внутри? Как Python создает список MRO?

У Python очень простой подход к ООП (более подробнее см. Здесь). Классы сами являются объектами, поэтому они содержат структуры данных, которые используются языком для предоставления функций, и делегирование не исключение. Когда мы запускаем метод для объекта, Python по умолчанию используется метод __getattribute__ (предоставляемый object), который использует __class__ для доступа к классу из экземпляра и __bases__ для поиска родительских классов. Последний, в частности, является кортежем, поэтому он упорядочен и содержит все классы, от которых наследуется текущий класс.

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

Наследование и интерфейсы

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

В Python, когда вы переопределяете метод, предоставленный классом-предком, вы должны решить, когда и как вызывать его начальную реализацию. Это дает программисту свободу решать, нужно ли им просто дополнить метод или полностью заменить его. Помните, что единственное, что делает Python, когда класс наследует от другого, — это автоматически делегирует методы, которые не были реализованы в наследнике.

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

class GraphicalEntity:
    def __init__(self, pos_x, pos_y, size_x, size_y):
        self.pos_x = pos_x
        self.pos_y = pos_y
        self.size_x = size_x
        self.size_y = size_y

    def move(self, pos_x, pos_y):
        self.pos_x = pos_x
        self.pos_y = pos_y

    def resize(self, size_x, size_y):
        self.size_x = size_x
        self.size_y = size_y


class Rectangle(GraphicalEntity):
    pass


class Square(GraphicalEntity):
    def __init__(self, pos_x, pos_y, size):
        super().__init__(pos_x, pos_y, size, size)

    def resize(self, size):
        super().resize(size, size)


Обратите внимание, что Square меняет подпись __init__ и resize. Теперь, когда мы создаем экземпляры этих классов, нам нужно помнить что в Square другая подпись __init__

r1 = Rectangle(100, 200, 15, 30)
r2 = Rectangle(150, 280, 23, 55)
q1 = Square(300, 400, 50)

Обычно мы признаем, что расширенная версия класса принимает больше параметров при инициализации, так как мы не ожидаем, что она будет полиморфной в __init__. Проблемы возникают, когда мы пытаемся использовать полиморфизм в других методах, например, изменяя размер всех объектов GraphicalEntity в списке

for shape in [r1, r2, q1]:
    size_x = shape.size_x
    size_y = shape.size_y
    shape.resize(size_x*2, size_y*2)


Поскольку r1, r2 и q1 — все объекты, которые наследуются от GraphicalEntity, мы ожидаем, что они предоставят интерфейс, предоставленный этим классом, но это не удастся, потому что Square изменил сигнатуру resize. То же самое произойдет, если мы создадим их в цикле for из списка классов, но, как я уже сказал, общепринято, что дочерние классы изменяют сигнатуру метода __init__. Это не так, например, в системе на основе плагинов, где все плагины должны быть инициализированы одинаково.

Это классическая проблема в ООП. Хотя мы, как люди, воспринимаем квадрат просто как немного особенный прямоугольник, с точки зрения интерфейса эти два класса различны, и поэтому мы не должны находиться в одном и том же дереве наследования, когда имеем дело с измерениями. Это важное соображение: Rectangle и Square полиморфны в методе move, но не в __init__ и resize. Итак, вопрос в том, можем ли мы как-то разделить две природы movable и resizable.

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

Миксины

MRO — это хорошее решение, которое предотвращает двусмысленность, но оставляет программистам ответственность за создание разумных деревьев наследования. Алгоритм помогает разрешать сложные ситуации, но это не значит, что мы должны их создавать. Итак, как мы можем использовать множественное наследование, не создавая системы, которые слишком сложны для понимания? Кроме того, возможно ли использовать множественное наследование для решения проблемы управления двойной (или множественной) природой объекта, как в предыдущем примере с movable и resizable формой?

Решение исходит из смешанных классов: это небольшие классы, которые предоставляют атрибуты, но не включены в стандартное дерево наследования, работая скорее как «дополнения» к текущему классу, чем как настоящие предки. Миксины происходят от языка программирования LISP, а именно от того, что можно считать первой версией Common Lisp Object System, расширением Flavors. Современные языки ООП реализуют миксины разными способами: например, в Scala есть функции, называемые traits, которые живут в своем собственном пространстве с определенной иерархией, и которые не мешают правильному наследованию классов.

Классы миксинов в Python

Python не поддерживает миксины с какой-либо выделенной функцией языка, поэтому мы используем множественное наследование для их реализации. Это явно требует большой дисциплины от программиста, поскольку это нарушает одно из основных допущений для миксинов: их ортогональность к дереву наследования. В Python так называемые миксины — это классы, которые живут в обычном дереве наследования, но они остаются небольшими, чтобы избежать создания иерархий, которые слишком сложны для понимания программистом. В частности, миксины не должны иметь общих предков, кроме объекта, с другими родительскими классами.

Давайте посмотрим на простой пример

class GraphicalEntity:
    def __init__(self, pos_x, pos_y, size_x, size_y):
        self.pos_x = pos_x
        self.pos_y = pos_y
        self.size_x = size_x
        self.size_y = size_y


class ResizableMixin:
    def resize(self, size_x, size_y):
        self.size_x = size_x
        self.size_y = size_y


class ResizableGraphicalEntity(GraphicalEntity, ResizableMixin):
    pass


Здесь класс ResizableMixin наследуется не от GraphicalEntity, а непосредственно от object, поэтому ResizableGraphicalEntity получает от него только метод resize. Как мы уже говорили, это упрощает дерево наследования ResizableGraphicalEntity и помогает снизить риск проблемы алмаза. Это позволяет нам свободно использовать GraphicalEntity в качестве родителя для других классов без необходимости наследовать методы, которые нам не нужны. Пожалуйста, помните, что это происходит потому, что классы предназначены для того, чтобы этого избежать, а не из-за особенностей языка: алгоритм MRO просто гарантирует, что всегда будет однозначный выбор в случае нескольких предков.

Mixins обычно не могут быть слишком общими. В конце концов, они предназначены для добавления определенных функций в классы, но эти новые функции часто взаимодействуют с другими уже существующими функциями расширенного класса. В этом случае метод resize взаимодействует с атрибутами size_x и size_y, которые должны присутствовать в объекте. Очевидно, что есть примеры чистых mixins, но, поскольку они не требуют инициализации, их область действия определенно ограничена.

Использование миксинов для взламывания наследования

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

class GraphicalEntity:
    def __init__(self, pos_x, pos_y, size_x, size_y):
        self.pos_x = pos_x
        self.pos_y = pos_y
        self.size_x = size_x
        self.size_y = size_y

class Button(GraphicalEntity):
    def __init__(self, pos_x, pos_y, size_x, size_y):
        super().__init__(pos_x, pos_y, size_x, size_y)
        self.status = False

    def toggle(self):
        self.status = not self.status

b = Button(10, 20, 200, 100)


Как вы можете видеть, класс Button расширяет класс GraphicalEntity классическим способом, используя super для вызова родительского метода __init__ перед добавлением нового атрибута состояния status. Теперь, если я хочу создать класс SquareButton, у меня есть два варианта.

Я мог бы просто переопределить __init__ в новом классе

class GraphicalEntity:
    def __init__(self, pos_x, pos_y, size_x, size_y):
        self.pos_x = pos_x
        self.pos_y = pos_y
        self.size_x = size_x
        self.size_y = size_y


class Button(GraphicalEntity):
    def __init__(self, pos_x, pos_y, size_x, size_y):
        super().__init__(pos_x, pos_y, size_x, size_y)
        self.status = False

    def toggle(self):
        self.status = not self.status


class SquareButton(Button):
    def __init__(self, pos_x, pos_y, size):
        super().__init__(pos_x, pos_y, size, size)

b = SquareButton(10, 20, 200)


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

Второй вариант — это выделение объектов, связанных с наличием одного измерения в классе mixin, и добавление его в качестве родительского для нового класса.

class GraphicalEntity:
    def __init__(self, pos_x, pos_y, size_x, size_y):
        self.pos_x = pos_x
        self.pos_y = pos_y
        self.size_x = size_x
        self.size_y = size_y


class Button(GraphicalEntity):
    def __init__(self, pos_x, pos_y, size_x, size_y):
        super().__init__(pos_x, pos_y, size_x, size_y)
        self.status = False

    def toggle(self):
        self.status = not self.status


class SingleDimensionMixin:
    def __init__(self, pos_x, pos_y, size):
        super().__init__(pos_x, pos_y, size, size)


class SquareButton(SingleDimensionMixin, Button):
    pass

b = SquareButton(10, 20, 200)


Второе решение дает тот же конечный результат, но способствует повторному использованию кода, поскольку теперь класс SingleDimensionMixin можно применять к другим классам, производным от GraphicalEntity, и заставлять их принимать только один размер, тогда как в первом решении эта функция была тесно связана с предком Button класс.

Обратите внимание, что позиция миксина важна. Поскольку super следует MRO, вызываемый метод отправляется в ближайший класс линеаризации. Если вы поместите SingleDimensionMixin после Button в определении SquareButton, Python выдаст ошибку. В этом случае вызов b = SquareButton (10, 20, 200) и сигнатура метода __init__ (self, pos_x, pos_y, size_x, size_y) не будут совпадать.

Тем не менее, миксины используются не только тогда, когда вы хотите изменить интерфейс объекта. Используя super, мы можем достичь интересных конструкций, таких как

class GraphicalEntity:
    def __init__(self, pos_x, pos_y, size_x, size_y):
        self.pos_x = pos_x
        self.pos_y = pos_y
        self.size_x = size_x
        self.size_y = size_y


class Button(GraphicalEntity):
    def __init__(self, pos_x, pos_y, size_x, size_y):
        super().__init__(pos_x, pos_y, size_x, size_y)
        self.status = False

    def toggle(self):
        self.status = not self.status


class LimitSizeMixin:
    def __init__(self, pos_x, pos_y, size):
        size_x = min(size_x, 500)
        size_y = min(size_y, 400)
        super().__init__(pos_x, pos_y, size_x, size_y)


class LimitSizeButton(Button, LimitSizeMixin):
    pass

b = LimitSizeButton(10, 20, 200, 100)


Здесь LimitSizeButton вызывает __init__ своего первого родителя, который является Button. Это, однако, делегирует вызов следующему классу в MRO перед инициализацией self.status, поэтому вызов отправляется LimitSizeMixin, который сначала выполняет некоторые изменения и в конечном итоге отправляет его исходному получателю, GraphicalEntity.

Помните, что в Python вы никогда не будете вынуждены вызывать родительскую реализацию метода, поэтому миксин здесь также может остановить механизм диспетчеризации, если это является требованием бизнес-логики нового объекта.

Реальный пример: представления на основе классов Django

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

Выбранный мной пример можно найти в коде универсальных представлений(generic views), в частности в двух классах: TemplateResponseMixin и TemplateView.

Как вы, возможно, знаете, класс Django View является прародителем всех представлений на основе классов и предоставляет метод диспетчеризации dispatch, который преобразует методы HTTP-запросов в вызовы функций Python (CODE). Теперь TemplateView — это представление, которое отвечает на запрос GET, отображающий шаблон с данными, поступающими из контекста, передаваемого при вызове представления. Учитывая механизм, лежащий в основе представлений Django, TemplateView должен реализовать метод get и вернуть содержимое HTTP-ответа. Код класса

class TemplateView(TemplateResponseMixin, ContextMixin, View):
    """
    Render a template. Pass keyword arguments from the URLconf to the context.
    """
    def get(self, request, *args, **kwargs):
        context = self.get_context_data(**kwargs)
        return self.render_to_response(context)


Как вы можете видеть, TemplateView — это View, но он использует два миксина для добавления функций. Давайте посмотрим на TemplateResponseMixin

class TemplateResponseMixin:
    [...]

    def render_to_response(self, context, **response_kwargs):
        [...]

    def get_template_names(self):
        [...]

[Я удалил код класса, так как он не имеет решающего значения для настоящего обсуждения, вы можете увидеть полный класс здесь]

Понятно, что TemplateResponseMixin просто добавляет к любому классу два метода get_template_names и render_to_response. Последний вызывается в методе get TemplateView для создания ответа. Давайте посмотрим на упрощенную схему вызовов:

GET request --> TemplateView.dispatch --> View.dispatch --> TemplateView.get --> TemplateResponseMixin.render_to_response

Это может показаться сложным, но попробуйте следовать коду пару раз, и вся картина начнет обретать смысл. Важно подчеркнуть, что код в TemplateResponseMixin доступен для любого класса, который хочет иметь функцию рендеринга шаблона, например DetailView (CODE), который получает возможность показа деталей одного объекта с помощью SingleObjectTemplateResponseMixin. , который наследуется от TemplateResponseMixin, переопределяя его метод get_template_names (CODE).

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

Выводы

  • Наследование предназначено для повторного использования кода, но может привести к противоположному результату.
  • Множественное наследование позволяет нам сохранять дерево наследования простым
  • Множественное наследование приводит к возможным проблемам, которые решаются в Python через MRO
  • Интерфейсы (неявные или явные) должны быть частью вашего дизайна
  • Смешанные классы используются для добавления простых изменений в классы.
  • Миксины реализованы в Python с использованием множественного наследования: они обладают большой выразительной силой, но требуют тщательного проектирования.

Заключение

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

Обратная связь

Не стесняйтесь связаться со мной в Twitter, если у вас есть вопросы. Страница вопросов GitHub — лучшее место для отправки исправлений.

Была ли вам полезна эта статья?
[8 / 5]

Spread the love
Подписаться
Уведомление о
guest
2 Комментарий
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Kirill Pletnev
Kirill Pletnev
4 лет назад

Привет.
Спасибо за очередную отличную статью.

Хочу уточнить момент касательно LimitSizeButton: я не вижу, чтобы в данном примере Mixin вообще как-то использовался.
In [14]: LimitSizeButton.mro()
Out[14]:
[__main__.LimitSizeButton,
__main__.Button,
__main__.GraphicalEntity,
__main__.LimitSizeMixin,
object]

В результате __init__ Mixin’а не работает

In [15]: b = LimitSizeButton(10, 20, 800, 800)

In [16]: b.size_x
Out[16]: 800

In [17]: b.size_y
Out[17]: 800

Max Lerman
Max Lerman
4 лет назад
class LimitSizeMixin:
    def __init__(self, pos_x, pos_y, size_x, size_y):
        size_x = min(size_x, 500)
        size_y = min(size_y, 400)
        super().__init__(pos_x, pos_y, size_x, size_y)

В этом блоке ошибка. В init написан аргумент size, а должен быть size_x, size_y