Модуль multipledispatch
реализует шаблона программирования множественной диспетчеризации (перегрузки методов и функций) в Python, выполняя статический анализ во избежание конфликтов и обеспечивает дополнительную поддержку пространства имен.
(int, float)
.Iterator
, Number
,...multipledispatch
в виртуальное окружение:# создаем виртуальное окружение, если нет $ python3 -m venv .venv --prompt VirtualEnv # активируем виртуальное окружение $ source .venv/bin/activate # ставим модуль multipledispatch (VirtualEnv):~$ python3 -m pip install -U multipledispatch
multipledispatch
.multipledispatch
.Пример перегрузки функций:
from multipledispatch import dispatch @dispatch(object, object) # базовая реализация функции `add()` def add(x, y): return f"{x} + {y}" @dispatch(int, int) # реализация функции `add()` для целых чисел def add(x, y): return x + y >>> add(1, 2) # 3 >>> add(1, 'hello') # '1 + hello'
Пример перегрузки методов у класса:
from multipledispatch import dispatch class Concate(): @dispatch(object, object) # базовая реализация метода `.add()` def add(x, y): return f"{x} + {y}" @dispatch(int, int) # реализация функции `add()` для целых чисел def add(x, y): return x + y >>> c = Concate() >>> c.add(1, 2) # 3 >>> c.add(1, 'hello') # '1 + hello'
Множественная диспетчеризация/отправка выбирает метод/функцию, на основе анализа типов входящих аргументов.
from multipledispatch import dispatch @dispatch(int) # реализация функции `f()` для целого числа def f(x): # увеличит целое число return x + 1 @dispatch(float) # реализация функции `f()` для вещественного числа def f(x): # уменьшит вещественное число return x - 1 >>> f(1) # 2 >>> f(1.0) # 0.0
Аналогично встроенной функции isinstance()
, в декораторе @dispatch()
указываются несколько допустимых типов с помощью кортежа. Приведенная ниже реализация f()
для (list, tuple)
применит реализацию f()
для целого числа к каждому элементу в списке или кортеже, переданному в качестве аргумента.
@dispatch((list, tuple)) # реализация функции `f()` для типов `list` и `tuple` def f(x): """ Применит `f(y: int)` к каждому элементу в списке или кортеже """ return [f(y) for y in x] >>> f([1, 2, 3]) # [2, 3, 4] >>> f((1, 2, 3)) # [2, 3, 4]
В декораторе @dispatch()
можно также использовать абстрактные классы, такие как Iterable
и Number
, вместо типов объединения, таких как (list, tuple)
или (int, float)
соответственно.
from collections.abc import Iterable # @dispatch((list, tuple)) @dispatch(Iterable) def f(x): """ Применит `f(y: int)` к каждому элементу в итерационном """ return [f(y) for y in x]
Если существует несколько допустимых реализаций, то используется наиболее конкретная. В следующем примере создается функция для "сглаживания" вложенных итераций.
from multipledispatch import dispatch from collections.abc import Iterable @dispatch(Iterable) def flatten(L): return sum([flatten(x) for x in L], []) @dispatch(object) def flatten(x): return [x] >>> flatten([1, 2, 3]) # [1, 2, 3] >>> flatten([1, [2], 3]) # [1, 2, 3] >>> flatten([1, 2, (3, 4), [[5]], [(6, 7), (8, 9)]]) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
Так как строки итерируемы, то они тоже будут "сглажены":
>>> flatten([1, 'hello', 3]) # [1, 'h', 'e', 'l', 'l', 'o', 3]
Нужно избегать этого, конкретизируя реализацию flatten()
до типа str
. Поскольку тип str
более конкретен, чем Iterable
, то следующая реализация имеет приоритет для типа str
.
@dispatch(str) def flatten(s): return s >>> flatten([1, 'hello', 3]) # [1, 'hello', 3]
Модуль multipledispatch
зависит от механизма работы функции issubclass()
, который определяет, какие типы более специфичны, чем другие.
Все эти правила применяются, когда метод/функция принимает несколько аргументов в качестве входных данных.
from multipledispatch import dispatch @dispatch(object, object) def f(x, y): return x + y @dispatch(object, float) def f(x, y): """ Квадрат второго аргумента, если это `float` """ return x + y**2 >>> f(1, 10) # 11 >>> f(1.0, 10.0) # 101.0
Модуль multipledispatch
поддерживает множественную отправку (включая поддержку типов объединения) в качестве последнего набора аргументов, переданных в функцию.
Вариативные сигнатуры задаются одноэлементным списком, содержащим тип аргументов, которые принимает функция.
Пример функции, которая принимает число с плавающей запятой, за которым следует любое число (включая 0) либо int
, либо str
:
from multipledispatch import dispatch @dispatch(float, [(int, str)]) def float_then_int_or_str(x, *args): return x + sum(map(int, args)) >>> f(1.0, '2', '3', 4) # 10.0 >>> f(2.0, '4', 6, 8) # 20.0
Однако неоднозначности возникают, когда разные реализации функции одинаково допустимы.
from multipledispatch import dispatch @dispatch(object, object) def f(x, y): return x + y @dispatch(object, float) def f(x, y): """ Квадрат второго аргумента, если он `float` """ return x + y**2 @dispatch(float, object) def f(x, y): """ Квадрат первого аргумента, если он `float` """ return x**2 + y >>> f(2.0, 10.0) # ?
Какого результата ждать: 2,0 ** 2 + 10,0
или 2,0 + 10,0 ** 2
? Типы входных данных удовлетворяют трем различным реализациям, две из которых имеют одинаковую силу.
input types: float, float Вариант 1: object, object Вариант 2: object, float Вариант 3: float, object
Вариант 1 - строго менее конкретен, чем варианты 2 или 3, поэтому он отбрасывается. Но варианты 2 и 3 одинаково специфичны, поэтому неясно, какой из них использовать.
Чтобы решить такие проблемы, как эта, множественная отправка проверяет предоставленные ей сигнатуры типов и ищет неоднозначности. Затем появляется предупреждение, подобное следующему:
multipledispatch/dispatcher.py:27: AmbiguityWarning: Ambiguities exist in dispatched function f The following signatures may result in ambiguous behavior: [object, float], [float, object] Consider making the following additions: @dispatch(float, float) def f(...) warn(warning_text(dispatcher.name, ambiguities), AmbiguityWarning) 14.0
Это предупреждение появляется, когда в перегруженном методе/функции есть двусмысленность, а так же помогает создать конкретную реализацию. В этом случае функция с сигнатурой (float, float)
более конкретна, чем варианты 2 или 3, и таким образом решает проблему. Чтобы избежать этого предупреждения, необходимо создать эту реализацию раньше других.
@dispatch(float, float) def f(x, y): ... @dispatch(float, object) def f(x, y): ... @dispatch(object, float) def f(x, y): ...
Если не устранить двусмысленность, то одна из конкурирующих функций будет выбрана псевдослучайно. По умолчанию, выбор зависит от хэша, поэтому он будет согласован во время сеанса интерпретатора, но может меняться от сеанса к сеансу.