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

Перегрузка функций в 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) для улавливания любыми неспецифическими типами аргументов.

Перегрузка операторов в Python.

Кроме того, в Python уже существует перегрузка для операторов Python (<, >, = и др.) и встроенных функций (len(), str(), repr() и др.), использующих так называемые dunder или magic методы, и это возможность языка используется довольно часто. Например, перегрузка операторов используется для описания логики в случаях нестандартного использования операторов в пользовательских объектах. Но злоупотреблять перегрузкой также не стоит - не нужно делать перегрузку просто так, "чтобы было".

Дополнительно смотрите модуль стандартной библиотеки Python operator, экспортирует набор эффективных функций, соответствующих встроенным операторам Python. Например, operator.add(x, y) эквивалентен выражению x+y. Многие имена функций используются для специальных методов без двойных подчеркиваний.

Например магический метод object.__str__() перегружает функцию str(), а так-же вызывается встроенными функциями format() и print() для вычисления неформального или красиво строкового представления объекта.

class Order:
    def __init__(self, price, num):
        self.price = price
        self.num = num
    
    def __getattr__(self, attrname):
        if attrname == "total":
            return self.price * self.num

    # неформальное строковое представление
    def __str__(self):
        return f'price={self.price}, num={self.num}, total={self.total}'

>>> order = Order(10, 15)
>>> str(order)
# 'price=10, num=15, total=150'

Что-бы было более понятно для чего нужна перегрузка операторов и где ее использовать, смотрите синтетический пример ниже, с пояснениями в коде. Например есть класс Rope() (веревка). У веревки есть только одно свойство - длина. К веревке можно привязать другую веревку, в следствии чего ее длина увеличится (длину входящую в узел учитывать не будем).

# создайте файл `test.py`
class Rope:
    
    def __init__(self, length):
        self._length = length

    def __len__(self):
        # Переопределяем оператор `__len__`
        return self._length

    def __add__(self, other):
        # Переопределяем оператор сложения `+`
        if isinstance(other, self.__class__):
            # Если второй объект того же класса, то
            # возвращаем новый объект `Rope` с новой длиной
            return Rope(self._length + other._length)  
        else:
            raise TypeError

    def __iadd__(self, other):
        # Переопределяем оператор 
        # присваивания на месте `+=`
        if isinstance(other, self.__class__):
            # Если второй объект того же класса, то
            # изменяем длину существующего объекта
            self._length += other._length
            return self
        else:
            raise TypeError


# запустим python3 в интерактивном режиме
# $ python3 -i test.py
# новая веревка длиной 10
>>> rope = Rope(10)
# прибавляем объект веревки Rope(15), с длиной 15
>>> rope1 = rope + Rope(15)
# длина исходной веревки не изменилась
>>> len(rope)
# 10

# длина объекта связанной веревки 
>>> len(rope1)
# 25

>>> rope += Rope(5)
>>> len(rope)
# 15

# объект `Rope()` можно складывать 
# только с объектом `Rope()`
>>> rope += 10
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
#   File "test.py", line 28, in __iadd__
#     raise TypeError
# TypeError

Список операторов, которые часто перегружаются.

  • __new__(cls[, ...]) - управляет созданием экземпляра. В качестве обязательного аргумента принимает класс (не путать с экземпляром). Должен возвращать экземпляр класса для его последующей передачи методу __init__.__init__(self[, ...]) - конструктор класса.
  • __del__(self) - вызывается при удалении объекта сборщиком мусора.
  • __repr__(self) - вызывается встроенной функцией repr(), использующиеся для внутреннего представления в Python.
  • __str__(self) - вызывается функциями str(), print() и format(). Возвращает строковое представление объекта.
  • __bytes__(self) - вызывается функцией bytes() при преобразовании к байтам.
  • __format__(self, format_spec) - используется функцией format, а также методом строки str.format().
  • __lt__(self, other) - x < y вызывает x.__lt__(y).
  • __le__(self, other) - x ≤ y вызывает x.__le__(y).
  • __eq__(self, other) - x == y вызывает x.__eq__(y).
  • __ne__(self, other) - x != y вызывает x.__ne__(y).
  • __gt__(self, other) - x > y вызывает x.__gt__(y).
  • __ge__(self, other) - x ≥ y вызывает x.__ge__(y).
  • __hash__(self) - получение хэш-суммы объекта, например, для добавления в словарь.
  • __bool__(self) - вызывается при проверке истинности. Если этот метод не определён, то вызывается метод __len__ (объекты, имеющие ненулевую длину, считаются истинными).
  • __call__(self[, args...]) - вызов экземпляра класса как функции.
  • __len__(self) - длина объекта.
  • __getitem__(self, key) - доступ по индексу или ключу.
  • __setitem__(self, key, value) - назначение элемента по индексу.
  • __delitem__(self, key) - удаление элемента по индексу.
  • __reversed__(self) - итератор из элементов, следующих в обратном порядке.
  • __contains__(self, item) - проверка на принадлежность элемента контейнеру.
  • __concat__(self, other) - a + b для последовательностей.
  • __int__(self) - приведение к типу int.
  • __float__(self) - приведение к типу float.
  • __round__(self[, n]) - округление.
  • __abs__(self) - по модулю, функция abs().
  • __neg__(self) - унарный -.
  • __pos__(self) - унарный +.
  • __invert__(self) - инверсия ~.

Часто перегружаемые арифметические операторы:

  • __add__(self, other) - сложение x + y вызывает x.__add__(y).
  • __sub__(self, other) - вычитание x - y.
  • __mul__(self, other) - умножение x * y.
  • __truediv__(self, other) - деление x / y.
  • __floordiv__(self, other) - целочисленное деление x // y.
  • __mod__(self, other) - остаток от деления x % y.
  • __divmod__(self, other) - частное и остаток divmod(x, y).
  • __pow__(self, other[, modulo]) - возведение в степень x ** y и pow(x, y[, modulo]).
  • __matmul__(self, other) - матричное умножение x @ y.
  • __lshift__(self, other) - битовый сдвиг влево x << y.
  • __rshift__(self, other) - битовый сдвиг вправо x >> y.
  • __and__(self, other) - битовое И x & y.
  • __xor__(self, other) - битовое ИСКЛЮЧАЮЩЕЕ ИЛИ x ^ y.
  • __or__(self, other) - битовое ИЛИ x | y.

Далее идут методы, которые делают то же самое, что и арифметические операторы, но для аргументов, находящихся справа, и только в случае, если для левого операнда не определён соответствующий метод. Например, операция x + y будет сначала пытаться вызвать x.__add__(y), и только в том случае, если это не получилось, будет пытаться вызвать y.__radd__(x):

  • __radd__(self, other),
  • __rsub__(self, other),
  • __rmul__(self, other),
  • __rtruediv__(self, other),
  • __rfloordiv__(self, other),
  • __rmod__(self, other),
  • __rdivmod__(self, other),
  • __rpow__(self, other),
  • __rlshift__(self, other),
  • __rrshift__(self, other),
  • __rand__(self, other),
  • __rxor__(self, other),
  • __ror__(self, other).

Операторы действий "на месте":

  • __iadd__(self, other) - эквивалентно x += y.
  • __isub__(self, other) - эквивалентно x -= y.
  • __imul__(self, other) - эквивалентно x *= y.
  • __itruediv__(self, other) - эквивалентно x /= y.
  • __ifloordiv__(self, other) - эквивалентно x //= y.
  • __imod__(self, other) - эквивалентно x %= y.
  • __ipow__(self, other[, modulo]) - эквивалентно x **= y.
  • __ilshift__(self, other) - эквивалентно x <<= y.
  • __irshift__(self, other) - эквивалентно x >>= y.
  • __iand__(self, other) - эквивалентно x &= y.
  • __ixor__(self, other) - эквивалентно x ^= y.
  • __ior__(self, other) - эквивалентно x |= y.
  • __imatmul__(self, other) - эквивалентно x @= y.