На этом шаге мы закончим изучение этого вопроса.
Если честно, то в этом рецепте слишком много магии, чтобы применять его в реальном мире. Однако он позволяет погрузиться во внутреннюю работу метаклассов и дескрипторов, улучшая ваше понимание этих концепций. Так что если вы и не будете применять данный рецепт напрямую, многие его идеи могут повлиять на другие приемы программирования, использующие метаклассы, дескрипторы и аннотации функций.
Основная идея этой реализации относительно проста. Метакласс MultipleMeta использует свой метод __prepare__(), чтобы предоставить кастомный словарь класса в качестве экземпляра MultiDict. В отличие от обычного словаря, MultiDict во время присваивания значений проверяет, существуют ли уже эти записи. Если они уже существуют, дублированные записи сливаются вместе внутри экземпляра MultiMethod.
Экземпляры MultiMethod собирают методы путем построения отображения из сигнатур типов в функции. Во время создания аннотации функции используются для сбора этих сигнатур и построения отображения. Это происходит в методе MultiMethod.register(). Важнейший аспект данного рецепта заключается в том, что для мультиметодов типы должны быть определены на всех аргументах, иначе возникнет ошибка.
Чтобы заставить экземпляры MultiMethod эмулировать поведение вызываемого объекта, в них реализован метод __call_(). Этот метод строит кортеж типов из всех аргументов, за исключением self, ищет метод во внутреннем отображении и вызывает подходящий метод. Метод __get__() нужен, чтобы заставить экземпляры MultiMethod правильно работать внутри определений классов. В данной реализации он был использован для создания правильных связанных методов. Например:
>>> b = s.bar >>> b <bound method bar of <__main__.Spam object at 0x0000025D8A038790>> >>> b.__self__ <__main__.Spam object at 0x0000025D8A038790> >>> b.__func__ <__main__.MultiMethod object at 0x0000025D8A0384C0> >>> b(2, 3) Bar 1: 2 3 >>> b('hello') Bar 2: hello 0 >>>
Будьте уверены, в этом рецепте много "движущихся частей". Однако это не спасает от большого количества ограничений. Например, это решение не работает с именованными аргументами:
>>> s.bar(x=2, y=3) Traceback (most recent call last): File "<pyshell#31>", line 1, in <module> s.bar(x=2, y=3) TypeError: __call__() got an unexpected keyword argument 'x' >>> s.bar(s='hello') Traceback (most recent call last): File "<pyshell#32>", line 1, in <module> s.bar(s='hello') TypeError: __call__() got an unexpected keyword argument 's' >>>
Способ добавить поддержку, возможно, нашелся бы, но он потребовал бы абсолютно другого подхода к отображению методов. Проблема в том, что именованные аргументы не поставляются в каком-то конкретном порядке. При смешивании с позиционными аргументами вы получите беспорядочный клубок, который вы как-то должны будете распутать в методе __call__().
Этот рецепт также серьезно ограничен в том, что касается поддержки наследования. Например, что-то вроде такого работать не будет:
class A: pass class B(A): pass class C: pass class Spam(metaclass=MultipleMeta): def foo(self, x:A): print('Foo 1:', x) def foo(self, x:C): print('Foo 2:', x)
Причина, по которой все ломается, такова: аннотация x:A не совпадает с экземплярами, которые являются подклассами (такими как экземпляры B). Например:
>>> s = Spam() >>> a = A() >>> s.foo(a) Foo 1: <__main__.A object at 0x1006a5310> >>> c = C() >>> s.foo(c) Foo 2: <__main__.C object at 0x1007a1910> >>> b = B() >>> s.foo(b) Traceback (most recent call last): File "<stdin>", line 1, in <module> . . . raise TypeError('No matching method for types {}'.format(types)) TypeError: No matching method for types (<class '__main__.B'>,) >>>
В качестве альтернативы использованию метаклассов и аннотаций можно реализовать похожий рецепт, использующий декораторы. Например:
>>> import types >>> class multimethod: def __init__(self, func): self._methods = {} self.__name__ = func.__name__ self._default = func def match(self, *types): def register(func): ndefaults = len(func.__defaults__) if func.__defaults__ else 0 for n in range(ndefaults+1): self._methods[types[:len(types) - n]] = func return self return register def __call__(self, *args): types = tuple(type(arg) for arg in args[1:]) meth = self._methods.get(types, None) if meth: return meth(*args) else: return self._default(*args) def __get__(self, instance, cls): if instance is not None: return types.MethodType(self, instance) else: return self >>>
Чтобы использовать версию на базе декораторов, напишите такой код:
>>> class Spam: @multimethod def bar(self, *args): # Если нет совпадений, вызывается дефолтный метод raise TypeError('No matching method for bar') @bar.match(int, int) def bar(self, x, y): print('Bar 1:', x, y) @bar.match(str, int) def bar(self, s, n=0): print('Bar 2:', s, n) >>>
Решение на базе декораторов страдает от тех же ограничений, что и предыдущая реализация (от отсутствия поддержки именованных аргументов и сломанного наследования).
При прочих равных, вероятно, лучше держаться подальше от множественной диспетчеризации в коде общего назначения. Существуют особые ситуации, где это может иметь смысл, например в программах, где диспетчеризация методов базируется на каком-то сопоставлении с образцом (pattern matching). К примеру, описанный на 150 шаге паттерн проектирования "Посетитель" может быть преобразован в класс, который неким образом использует множественную диспетчеризацию. Однако никогда не будет дурной идеей остановиться на более простом подходе (просто использовать методы с разными именами).
На следующем шаге мы рассмотрим избегание повторяющихся методов свойств.