На этом шаге мы рассмотрим особенности реализации такого расширения.
У вас есть набор полезных методов, которые вы хотели бы сделать доступными в других классах, чтобы расширить их функциональность. Однако классы, в
которые добавляются методы, не всегда связаны друг с другом через наследование. Поэтому вы не можете просто прикрепить методы к общему базовому классу (суперклассу).
Проблема, решаемая в этом рецепте, часто возникает в программах, где необходима кастомизация классов. Например, библиотека может предоставлять базовый набор классов вместе с набором необязательных кастомизаций, которые могут быть применены пользователями при желании.
Чтобы проиллюстрировать это, предположим, что вы заинтересованы в добавлении различных кастомизаций к объектам (например, логирования, запрета повторного присваивания, проверки типов и т. п.). Вот набор классов-миксин (mixin), которые это делают:
>>> class LoggedMappingMixin: ''' Добавляет логирование для операций get/set/delete в целях отладки. ''' __slots__ = () def __getitem__(self, key): print('Getting ' + str(key)) return super().__getitem__(key) def __setitem__(self, key, value): print('Setting {} = {!r}'.format(key, value)) return super().__setitem__(key, value) def __delitem__(self, key): print('Deleting ' + str(key)) return super().__delitem__(key) >>> class SetOnceMappingMixin: ''' Позволяет устанавливать ключ только один раз. ''' __slots__ = () def __setitem__(self, key, value): if key in self: raise KeyError(str(key) + ' already set') return super().__setitem__(key, value) >>> class StringKeysMappingMixin: ''' Запрещает ключам быть чем-то, кроме строк. ''' __slots__ = () def __setitem__(self, key, value): if not isinstance(key, str): raise TypeError('keys must be strings') return super().__setitem__(key, value) >>>
Сами по себе эти классы бесполезны. Если вы попытаетесь создать их экземпляры, ничего полезного не получится, разве что вы полюбуетесь на исключения. На самом деле они должны быть подмешаны к другим классам через множественное наследование. Например:
>>> class LoggedDict(LoggedMappingMixin, dict): pass >>> d = LoggedDict() >>> d['x'] = 23 Setting x = 23 >>> d['x'] Getting x 23 >>> del d['x'] Deleting x >>> from collections import defaultdict >>> class SetOnceDefaultDict(SetOnceMappingMixin, defaultdict): pass >>> d = SetOnceDefaultDict(list) >>> d['x'].append(2) >>> d['y'].append(3) >>> d['x'].append(10) >>> try: d['x'] = 23 except KeyError as e: print(e) 'x already set' >>> from collections import OrderedDict >>> class StringOrderedDict(StringKeysMappingMixin, SetOnceMappingMixin, OrderedDict): pass >>> d = StringOrderedDict() >>> d['x'] = 23 >>> try: d[42] = 10 except TypeError as e: print(e) keys must be strings >>> try: d['x'] = 42 except KeyError as e: print(e) 'x already set' >>>
В этом примере вы заметите, что миксины объединены с другими существующими классами (dict, defaultdict, OrderedDict) и даже
друг с другом. При объединении классы работают вместе, предоставляя желаемую функциональность.
Классы-миксины используются в различных местах стандартной библиотеки, в основном для расширения функциональности других классов. Также они являются одной из основных причин применять множественное наследование. Например, если вы пишете сетевой код, вы часто можете использовать ThreadingMixIn из модуля socketserver, чтобы добавить поддержку потоков в другие связанные с сетью классы. Например, вот многопоточный XML-RPC-сервер:
from xmlrpc.server import SimpleXMLRPCServer from socketserver import ThreadingMixIn class ThreadedXMLRPCServer(ThreadingMixIn, SimpleXMLRPCServer): pass
Также часто можно встретить миксины в крупных библиотеках и фреймворках - опять же, в основном для расширения функциональности существующих классов дополнительными возможностями.
У теории классов-миксин богатая история. Однако вместо погружения во все детали стоит держать в голове несколько важных моментов реализации.
Во-первых, классы-миксины никогда не предназначались для прямого создания экземпляров. Например, ни один из приведенных в этом рецепте классов сам по себе не работает. Они должны быть подмешаны к другому классу, который реализует требуемую функциональность. Похожим образом ThreadingMixIn из модуля socketserver должен быть подмешан к подходящему классу-серверу - он не может быть использован сам по себе.
Во-вторых, классы-миксины обычно не имеют собственного состояния. Это значит, что у них нет метода __init__() и переменных экземпляра. В этом рецепте определение __slots__ = () предназначено для строгого указания на тот факт, что классы-миксины не имеют собственных данных экземпляра.
Если вы подумываете определить класс-миксин, у которого будет метод __init__() и переменные экземпляра, то обратите внимание, что существует серьезная опасность, связанная с тем фактом, что класс ничего не знает о других классах, с которыми он будет смешиваться. Поэтому все переменные экземпляра должны иметь такие имена, которые позволят избежать конфликтов имен. Также метод __init__() должен быть запрограммирован правильно вызывать метод __init__() других классов, к которым подмешивается миксин. В общем случае это трудно реализовать, поскольку вы ничего не знаете о сигнатурах аргументов других классов. По крайней мере, вы должны реализовать нечто очень общее, используя *args и **kwargs. Если __init__() класса-миксина принимает какие-либо аргументы, эти аргументы должны быть определены только как именованные - и иметь такие имена, чтобы избежать конфликтов с другими аргументами. Вот возможная реализация миксина, определяющего __init__() и принимающего именованный аргумент:
>>> class RestrictKeysMixin: def __init__(self, *args, _restrict_key_type, **kwargs): self.__restrict_key_type = _restrict_key_type super().__init__(*args, **kwargs) def __setitem__(self, key, value): if not isinstance(key, self.__restrict_key_type): raise TypeError('Keys must be ' + str(self.__restrict_key_type)) super().__setitem__(key, value) >>> class RDict(RestrictKeysMixin, dict): pass >>>
Вот пример использования этого класса:
>>> d = RDict(_restrict_key_type=str) >>> e = RDict([('name', 'Dave'), ('n', 37)], _restrict_key_type=str) >>> f = RDict(name='Dave', n=37, _restrict_key_type=str) >>> f {'name': 'Dave', 'n': 37} >>> f[42] = 10 Traceback (most recent call last): File "<pyshell#36>", line 1, in <module> f[42] = 10 File "<pyshell#29>", line 8, in __setitem__ raise TypeError('Keys must be ' + str(self.__restrict_key_type)) TypeError: Keys must be <class 'str'> >>>
В этом примере вы можете заметить, что инициализация RDict() все еще принимает аргументы, которые понимает функция dict(). Однако есть и дополнительный именованный аргумент restrict_key_type, который предоставляется классу-миксину.
И последнее: функция super() - необходимая и критически важная часть написания классов-миксин. В вышеприведенном решении классы переопределяют некоторые критически важные методы, такие как __getitem__() и __setitem__(). Однако им также нужно вызвать изначальные реализации этих методов. Использование super() делегируется следующему классу в порядке разрешения методов (ПРМ, MRO - method resolution order). Этот аспект рецепта, однако, неочевиден для новичков, потому что super() используется в классах, у которых нет родителей (на первый взгляд это может выглядеть как ошибка). Однако в подобном определении класса:
class LoggedDict(LoggedMappingMixin, dict): pass
Альтернативная реализация миксин подразумевает использование декораторов классов. Например, рассмотрите такой код:
>>> def LoggedMapping(cls): cls_getitem = cls.__getitem__ cls_setitem = cls.__setitem__ cls_delitem = cls.__delitem__ def __getitem__(self, key): print('Getting %s' % key) return cls_getitem(self, key) def __setitem__(self, key, value): print('Setting %s = %r' % (key, value)) return cls_setitem(self, key, value) def __delitem__(self, key): print('Deleting %s' % key) return cls_delitem(self, key) cls.__getitem__ = __getitem__ cls.__setitem__ = __setitem__ cls.__delitem__ = __delitem__ return cls >>>
Эта функция применяется к определению класса как декоратор. Например:
>>> @LoggedMapping class LoggedDict(dict): pass >>>
Если вы попробуете это в работе, то получите такое же поведение, как и ранее, но без использования множественного наследования.
>>> d = LoggedDict() >>> d['x'] = 23 Setting x = 23 >>> d['x'] Getting x 23 >>> del d['x'] Deleting x >>>
Вместо него декоратор просто выполняет небольшую хирургическую операцию на определении класса для замены некоторых методов.
См. также 142 шаг, где приведен продвинутый способ, использующий миксины и декораторы классов одновременно.
На следующем шаге мы рассмотрим реализацию объектов с состоянием или конечных автоматов.