Шаг 175.
Python: сборник рецептов. Метапрограммирование. Реализация множественной диспетчеризации с помощью аннотаций функций

    На этом шаге мы рассмотрим особенности решения указанной задачи.

Задача

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

Решение

    Этот рецепт базируется на простом наблюдении - поскольку Python позволяет аннотировать аргументы, то можно написать такой код:

class Spam:
    def bar(self, x:int, y:int): 
        print('Bar 1:', x, y) 

    def bar(self, s:str, n:int = 0): 
        print('Bar 2:', s, n)

s = Spam()
s.bar(2, 3)	# Выводит Bar 1: 2 3
s.bar('hello') # Выводит Bar 2: hello 0

    Вот начало решения, которое делает то же самое, но с использованием комбинации метакласов и дескрипторов:

>>> import inspect
>>> import types
>>> class MultiMethod:
    '''
    Представляет один мультиметод.
    '''
    def __init__(self, name):
        self._methods = {}
        self.__name__ = name

    def register(self, meth):
        '''
        Регистрирует новый метод как мультиметод
        '''
        sig = inspect.signature(meth)

        # Создание сигнатуры типа из аннотаций методов
        types = []
        for name, parm in sig.parameters.items():
            if name == 'self': 
                continue
            if parm.annotation is inspect.Parameter.empty:
                raise TypeError(
                    'Argument {} must be annotated with a type'.format(name)
                    )
            if not isinstance(parm.annotation, type):
                raise TypeError(
                    'Argument {} annotation must be a type'.format(name)
                    )
            if parm.default is not inspect.Parameter.empty:
                self._methods[tuple(types)] = meth
            types.append(parm.annotation)

        self._methods[tuple(types)] = meth

    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:
            raise TypeError('No matching method for types {}'.format(types))
        
    def __get__(self, instance, cls):
        '''
        Метод дескриптора, необходимый для работы вызовов в классе
        '''
        if instance is not None:
            return types.MethodType(self, instance)
        else:
            return self

>>> class MultiDict(dict):
    '''
    Специальный словарь для создания мультиметодов в метаклассе
    '''
    def __setitem__(self, key, value):
        if key in self:
            # Если ключ уже существует, он должен быть мультиметодом
            # или вызываемым объектом
            current_value = self[key]
            if isinstance(current_value, MultiMethod):
                current_value.register(value)
            else:
                mvalue = MultiMethod(key)
                mvalue.register(current_value)
                mvalue.register(value)
                super().__setitem__(key, mvalue)
        else:
            super().__setitem__(key, value)

>>> class MultipleMeta(type):
    '''
    Метакласс, который позволяет множественную диспетчеризацию методов
    '''
    def __new__(cls, clsname, bases, clsdict):
        return type.__new__(cls, clsname, bases, dict(clsdict))

    @classmethod
    def __prepare__(cls, clsname, bases):
        return MultiDict()

>>> 

    Чтобы использовать этот класс, напишите такой код:

>>> class Spam(metaclass=MultipleMeta):
    def bar(self, x:int, y:int):
        print('Bar 1:', x, y)
    def bar(self, s:str, n:int = 0):
        print('Bar 2:', s, n)

>>> # Пример: перегруженный __init__
>>> import time
>>> class Date(metaclass=MultipleMeta):
    def __init__(self, year: int, month:int, day:int):
        self.year = year
        self.month = month
        self.day = day

    def __init__(self):
        t = time.localtime()
        self.__init__(t.tm_year, t.tm_mon, t.tm_mday)

>>> 

    Вот интерактивный сеанс, в котором мы проверяем, что все работает:

>>> s = Spam()
>>> s.bar(2, 3)
Bar 1: 2 3
>>> s.bar('hello')
Bar 2: hello 0
>>> s.bar('hello', 5)
Bar 2: hello 5
>>> s.bar(2, 'hello')
Traceback (most recent call last):
  File "<pyshell#15>", line 1, in <module>
    s.bar(2, 'hello')
  File "<pyshell#2>", line 43, in __call__
    raise TypeError('No matching method for types {}'.format(types))
TypeError: No matching method for types (<class 'int'>, <class 'str'>)
>>> # Перегруженный __init__
>>> d = Date(2012, 12, 21)
>>> print(d.year, d.month, d.day)
2012 12 21
>>> # Получить сегодняшнюю дату
>>> e = Date()
>>> print(e.year, e.month, e.day)
2026 9 14
>>> 

    На следующем шаге мы закончим изучение этого вопроса.




Предыдущий шаг Содержание Следующий шаг