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