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

Создание универсальных методов в классах Python

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

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