Сообщить об ошибке.

Что такое миксины в Python и как их использовать

Mixin классы - это концепция в программировании, в которой класс предоставляет функциональные возможности, но не предназначен для самостоятельного использования. Основная цель миксинов - предоставить какие-то дополнительные методы.

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

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

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

Пример множественного наследования.

# test.py
from collections import Counter, OrderedDict

class OrderedCounter(Counter, OrderedDict):
     """Счетчик, который запоминает порядок, 
     в котором элементы встречаются впервые"""

     def __repr__(self):
         return f'{self.__class__.__name__}({OrderedDict(self)!r})'

     def __reduce__(self):
         return self.__class__, (OrderedDict(self),)

# запускаем 
# $ python3 -i test.py
>>> od = OrderedCounter()
>>> repr(od)
# 'OrderedCounter(OrderedDict())'

Он создает подкласс Counter и OrderedDict, которые импортируются из модуля collections.

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

Пример использования класса миксина/примеси.

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

В отличие от приведенного выше примера, класс миксина не предназначен для использования отдельно. Он предоставляет новые методы или переопределяет имеющиеся методы.

Например, в стандартной библиотеке Python, в модуле socketserver есть несколько миксинов. Выдержка из документации:

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

class ThreadingUDPServer(ThreadingMixIn, UDPServer):
    pass

Первым, идет класс миксина ThreadingMixIn, так как он переопределяет метод process_request() и server_close(), определенный в классе UDPServer(), к тому же, добавляет новую функциональность, чтобы обеспечить параллелизм, а именно - новый метод process_request_thread().

Простой пример для понимания поведения классов миксинов.

Классы миксинов в Python - это действительно отличная концепция, которая позволяет создавать классы в композиционном стиле.

# test.py
class Entity:
    def __init__(self, pos_x, pos_y):
        self.pos_x = pos_x
        self.pos_y = pos_y


class SquareMixin:
    def add_size(self, size_x):
        self.size_x = size_x
        self.size_y = size_x

    def perimeter(self):
        return self.size_x * 4

    def square(self):
        return self.size_x * self.size_x


class SquareEntity(SquareMixin, Entity):
    pass

# запускаем 
# $ python3 -i test.py
>>> square = SquareEntity(5, 4)
>>> square.add_size(500)
>>> square.size_x
# 500
>>> square.size_y
# 500
>>> square.square()
# 250000
>>> square.perimeter()
# 2000

Здесь итоговый класс SquareEntity() получает от класса миксина SquareMixin() методы добавления размера квадрата, а так же вычисления его периметра и площади. Данное поведение упрощает дерево наследования SquareEntity(), что позволяет использовать класс Entity() в качестве родителя для других фигур без необходимости наследовать методы, которые не нужны (например для круга).

Иногда молодые программисты не до конца понимают принцип MRO в Python и по этому в некоторых случаях не правильно используют классы миксинов.

Пример:

# test.py
class BaseClass:
    def test(self):
        print('BaseClass')

class Mixin:
    def test(self):
        print('Mixin') 

class MyClass(BaseClass, Mixin):
    pass

# запускаем 
# $ python3 -i test.py
>>> obj = MyClass()
# класс миксина не работает
>>> obj.test()
# BaseClass

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

class MyClass(Mixin, BaseClass):
    pass

>>> obj = MyClass()
# теперь все нормально
>>> obj.test()
# Mixin

Использование функции super() в миксинах.

Например, если стоит необходимость явной передачи параметра size при инициализации класса SquareEntity() из примера выше, то необходимо воспользоваться функцией super().

# test.py
class Entity:
    def __init__(self, pos_x, pos_y):
        self.pos_x = pos_x
        self.pos_y = pos_y


class SquareMixin:
    def __init__(self, size, **kwargs):
        super().__init__(**kwargs)
        self.size_x = size
        self.size_y = size

    def perimeter(self):
        return self.size_x * 4

    def square(self):
        return self.size_x * self.size_x


class SquareEntity(SquareMixin, Entity):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

# все аргументы функции работают 
# исключительно как ключевые
square = SquareEntity(pos_x=5, pos_y=4, size=500)

# запускаем 
# $ python3 -i test.py
>>> square.size_x
# 500
>>> square.size_y
# 500
>>> square.perimeter()
# 2000
>>> square.square()
# 250000