На этом шаге мы рассмотрим особенности решения указанной задачи.
Вы узнали об аннотациях аргументов функций и задумались, нельзя ли использовать их для реализации множественной диспетчеризации (перегрузки методов) на основе типов. Однако вы не уверены, что тут нужно использовать (и хорошая ли это идея в принципе).
Этот рецепт базируется на простом наблюдении - поскольку 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 >>>
На следующем шаге мы закончим изучение этого вопроса.