Сообщить об ошибке.

Перегрузка функций в Python

Создание универсальных функций в Python

Перегрузка функций, по другому универсальные функции - это распространенный шаблон программирования, называемый множественной диспетчеризацией/отправкой, который, наверное, зарезервирован только для статически типизированных, компилируемых языков. Python является динамически типизированным языком и следовательно, перегрузка функций здесь невозможна, тем не менее, есть простой способ реализовать такое поведение в Python.

Дополнительно смотрите материал "Перегрузка методов в классе Python".

Содержание:

Общие моменты.

И так, как же можно реализовать перегрузку функции в Python? Ведь для перегрузки функции требуется, чтобы язык программирования мог различать типы во время компиляции. Для этого, интерпретатору Python необходимо различить несколько реализаций функции во время выполнения, на основе динамически определенных типов. Другими словами, Python нужно как-то использовать типы аргументов, передаваемых функции во время ее вызова, и на их основе динамически выбирать, какую из нескольких реализаций функции использовать.

Кто не стакивался со строго типизированными языками может задаться вопросом: "А оно нам нужно? Если такое поведение не может быть реализовано нормально, то и не стоит его использовать...". Да, это так, но есть веские причины реализовать поведение перегрузки функций в Python. Это мощный инструмент, который может сделать код более кратким, читаемым и минимизировать его сложность.

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

Например:

from datetime import date, datetime, time

def date_to_string(label):
     if isinstance(label, datetime):
         return label.strftime("%Y-%m-%d %H:%M:%S")
     elif isinstance(label, date):
         return label.strftime("%Y-%m-%d")
     elif isinstance(label, time):
         return label.strftime("%H:%M:%S")
     return str(label)

>>> l = time(19, 22, 15)
>>> date_to_string(l)
'19:22:15'

В примере, на основе типа, передаваемого аргумента label, принимается решение о строковом представлении даты и/или времени. Представьте что будет с кодом этой функции, если для получения результата определенного типа необходима длинная логика? Придется использовать фабрику функций, но проще и понятней использовать перегрузку.

Перегрузка одного аргумента (одиночная отправка).

В Python нет функции или класса стандартной библиотеки, которая поддерживает множественную отправку, но доступна одиночная отправка. Фактическое различие между множественной и одиночной отправкой - это количество аргументов, которые можно перегрузить. Функция (и декоратор), предоставляющая эту возможность, называется @singledispatch и расположена в модуле functools.

Эту концепцию лучше всего объяснить на нескольких примерах. Пример использования декоратора @singledispatch при форматировании datetime.datetime, datetime.date и datetime.time:

from functools import singledispatch
from datetime import date, datetime, time

@singledispatch
def format(label):
    return label

@format.register
def _(label: date):
    return label.strftime("%Y-%m-%d")

@format.register(datetime)
def _(label):
    return label.strftime("%Y-%m-%d %H:%M:%S")

@format.register(time)
def _(label):
    return label.strftime("%H:%M:%S")

>>> format("today")
# 'today'
>>> format(date(2021, 7, 28))
# '2021-05-26'
>>> format(datetime(2021, 7, 28, 17, 25, 10))
# '2021-07-28 17:25:10'
>>> format(time(17, 25, 10))
# '17:25:10'

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

В примере определяется базовая функция format(), которая будет перегружена. Эта функция декорируется @singledispatch и обеспечивает базовую реализацию, которая используется, если нет подходящего типа. Затем определяются отдельные дочерние функции для каждого типа, которые необходимо перегрузить - в данном случае это datetime.date, datetime.datetime и datetime.time - каждый из них имеет имя _ (подчеркивание), т.к. все они будут вызваны (отправлены) через функцию format(). Каждая из дочерних функций также декорируется @format.register(), что прикрепляет их к ранее упомянутой функции format(). Чтобы дочерние функции различали типы, есть два варианта - можно использовать аннотации типов, как показано в первом случае, или явно передать тип в декоратор.

В некоторых случаях имеет смысл использовать одну и ту же реализацию для нескольких типов, например, для int и float. Для таких ситуаций допускается перечисление нескольких декораторов @format.register(type), чтобы связать функцию со всеми допустимыми типами.

@name_func.register(int)
@name_func.register(float)
def _(num):
    return 10 ** num

Перегрузка нескольких аргументов (множественная отправка).

Часто одиночной отправки будет недостаточно, и может понадобиться надлежащая функциональность множественной отправки. Поведение множественной отправки доступно из стороннего модуля multipledispatch.

Модуль multipledispatch и его декоратор @dispatch ведут себя очень похоже на @singledispatch. Единственная разница заключается в том что он может принимать в качестве аргументов несколько типов:

from multipledispatch import dispatch

@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)


>>> concatenate(["a", "b"], "c")
# ['a', 'b', 'c']
>>> concatenate("Hello", "World")
# HelloWorld
>>> concatenate("a", 1)
# a1

Приведенный выше фрагмент показывает, как можно использовать декоратор @dispatch для перегрузки функции с несколькими аргументами, например для реализации конкатенации различных типов. Со сторонним модулем multipledispatch не нужно определять и регистрировать базовую функцию, а просто создать несколько функций с одним и тем же именем. Если необходимо предоставить базовую реализацию, то можно использовать @dispatch(object, object) для улавливания любыми неспецифическими типами аргументов.