Перегрузка методов в классе (по другому - универсальные методы) - это распространенный шаблон программирования (известен как множественная диспетчеризация/отправка), который, наверное, зарезервирован только для статически типизированных, компилируемых языков.
Программисты, пришедшие в Python из строго типизированных языков, например, таких как Java, часто спрашивают:
– "Всю жизнь писал на Java, начал изучать Python в связи с чем появилось много вопросов. Например в Java можно перегружать методы класса следующим образом:"
public class test { public static void main(String[] args) { ... } private void testrunner() { ... } private void testrunner(int i) { ... } private void testrunner(String s) { ... } }
– "Как можно перегружать методы класса на языке Python?"
Python является динамически типизированным языком и следовательно, перегрузка методов здесь невозможна, тем не менее, есть простой способ реализовать такое поведение в Python.
Для перегрузки методов требуется, чтобы язык программирования мог различать типы во время компиляции. Интерпретатору Python, для этого нужно как-то определять типы аргументов, передаваемых методу во время его вызова, и на их основе динамически выбирать, какую из нескольких реализаций конкретного метода использовать/отправить.
Без множественной диспетчеризации (отправки) очевидный способ сделать это - использовать проверку типов с isinstance()
. Это очень хрупкое решение, которое закрыто для расширения кода, а некоторые программисты называют его "костылем" или анти-паттерном.
Например:
class Test(object): def testrunner(self, t=None): if isinstance(t, str): print('str: ', t) elif isinstance(t, int): print('int: ', t) else: print(type(t), t) >>> t = Test() >>> t.testrunner(10) # int: 10 >>> t.testrunner('A') # str: A >>> t.testrunner(10.1) # <class 'float'> 10.1
В примере, на основе типа передаваемого аргумента t
принимается решение о его строковом представлении. А что будет с кодом метода, если для получения результата определенного типа необходима длинная логика? Придется использовать фабрику, но проще и понятней использовать перегрузку метода.
В Python нет функции или класса, который поддерживает множественную отправку, но доступна одиночная отправка. Фактическое различие между множественной и одиночной отправкой - это количество аргументов, которые можно перегрузить. Функция (и декоратор), предоставляющая эту возможность, называется @singledispatchmethod
и расположена в модуле functools
.
Эту концепцию лучше всего объяснить на примере.
from functools import singledispatchmethod from datetime import date, time class Formatter: @singledispatchmethod # базовая реализация метода `format()` def format(self, arg): raise NotImplementedError(f"Cannot format value of type {type(arg)}") @format.register # реализация метода `format()` для типа `date` def _(self, arg: date): return arg.strftime("%Y-%m-%d") @format.register(time) # реализация метода `format()` для типа `time` def _(self, arg): return arg.strftime("%H:%M:%S") >>> f = Formatter() >>> f.format(date(2021, 7, 28)) # '2021-07-28' >>> f.format(time(17, 25, 10)) # '17:25:10'
Чтобы дополнительные реализации метода (определенные в коде как def _()
) различали типы, есть два варианта - можно использовать аннотации типов, как показано в первом случае @format.register
, или явно передать тип в декоратор @format.register()
.
В некоторых случаях имеет смысл использовать одну и ту же реализацию метода для нескольких типов, например, для int
и float
. Для таких ситуаций допускается перечисление нескольких декораторов @format.register(type)
, чтобы связать функцию со всеми допустимыми типами.
@name_method.register(int) @name_method.register(float) def _(num): return 10 ** num
Декоратор @singledispatchmethod
поддерживает вложение с другими декораторами, такими как @classmethod
. Обратите внимание, что для использования @dispatcher.register
, декоратор @singledispatchmethod
должен быть внешним. Вот класс Negator
с методом Negator.neg()
, привязанным к классу:
class Negator: @singledispatchmethod @classmethod def neg(cls, arg): raise NotImplementedError("Cannot negate a") @neg.register @classmethod def _(cls, arg: int): return -arg @neg.register @classmethod def _(cls, arg: bool): return not arg
Часто одиночной отправки будет недостаточно, и может понадобиться надлежащая функциональность множественной отправки. Она доступна в стороннем модуле multipledispatch
.
Модуль multipledispatch
и его декоратор @dispatch
ведут себя очень похоже на @singledispatchmethod
. Единственная разница заключается в том что он может принимать в качестве аргументов несколько типов:
from multipledispatch import dispatch class Foo(): @dispatch(list, str) def concatenate(a, b): a.append(b) return a @dispatch(str, str) def concatenate(a, b): return a + b @dispatch(str, int) def concatenate(a, b): return a + str(b) >>> f = Foo() >>> f.concatenate(["a", "b"], "c") # ['a', 'b', 'c'] >>> f.concatenate("Hello", "World") # HelloWorld >>> f.concatenate("a", 1) # a1
Приведенный выше фрагмент показывает, как можно использовать декоратор @dispatch
для перегрузки методов с несколькими аргументами, например для реализации конкатенации различных типов. Со сторонним модулем multipledispatch
не нужно определять и регистрировать базовую функцию, а просто создать несколько функций с одним и тем же именем. Если необходимо предоставить базовую реализацию, то можно использовать @dispatch(object, object)
для улавливания любыми неспецифическими типами аргументов.