Как я могу использовать functools.singledispatch с методами экземпляра?

В Python 3.4 добавлена возможность определять перегрузку функций статическими методами. По сути, это пример из документации:

from functools import singledispatch


class TestClass(object):
    @singledispatch
    def test_method(arg, verbose=False):
        if verbose:
            print("Let me just say,", end=" ")

        print(arg)

    @test_method.register(int)
    def _(arg):
        print("Strength in numbers, eh?", end=" ")
        print(arg)

    @test_method.register(list)
    def _(arg):
        print("Enumerate this:")

        for i, elem in enumerate(arg):
            print(i, elem)

if __name__ == '__main__':
    TestClass.test_method(55555)
    TestClass.test_method([33, 22, 11])

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

У кого-нибудь есть совет, как использовать (или jerry-rig) эту функциональность, чтобы заставить ее работать с методами экземпляра?

3 ответа

Решение

Глядя на источник для singledispatchмы видим, что декоратор возвращает функцию wrapper(), который выбирает функцию для вызова из зарегистрированных на основе типа args[0]...

    def wrapper(*args, **kw):
        return dispatch(args[0].__class__)(*args, **kw)

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

Мы можем, однако, написать новый декоратор methdispatch, который опирается на singledispatch сделать тяжелую работу, но вместо этого возвращает функцию-оболочку, которая выбирает, какую зарегистрированную функцию вызывать в зависимости от типа args[1]:

from functools import singledispatch, update_wrapper

def methdispatch(func):
    dispatcher = singledispatch(func)
    def wrapper(*args, **kw):
        return dispatcher.dispatch(args[1].__class__)(*args, **kw)
    wrapper.register = dispatcher.register
    update_wrapper(wrapper, func)
    return wrapper

Вот простой пример использования декоратора:

class Patchwork(object):

    def __init__(self, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k, v)

    @methdispatch
    def get(self, arg):
        return getattr(self, arg, None)

    @get.register(list)
    def _(self, arg):
        return [self.get(x) for x in arg]

Обратите внимание, что оба украшены get() метод и метод зарегистрирован list иметь начальный self аргумент как обычно.

Тестирование Patchwork учебный класс:

>>> pw = Patchwork(a=1, b=2, c=3)
>>> pw.get("b")
2
>>> pw.get(["a", "c"])
[1, 3]

Декоратор - это, по сути, оболочка, которая принимает упакованную функцию в качестве аргумента и возвращает другую функцию.

Как указано в принятом ответе, singledispatch возвращает wrapper который принимает первый аргумент как зарегистрированный тип - self в экземплярах методов.

Как показано в этом ответе, в таких случаях вы можете написать другую обертку, чтобы обезьяна исправила декоратор. Но такие хакерские исправления не всегда являются лучшим вариантом.

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

from functools import singledispatch

class TestClass(object):

    def __init__(self):
        self.test_method = singledispatch(self.test_method)
        self.test_method.register(int, self._test_method_int)
        self.test_method.register(list, self._test_method_list)

    def test_method(self, arg, verbose=False):
        if verbose:
            print("Let me just say,", end=" ")

        print(arg)

    def _test_method_int(self, arg):
        print("Strength in numbers, eh?", end=" ")
        print(arg)

    def _test_method_list(self, arg):
        print("Enumerate this:")

        for i, elem in enumerate(arg):
            print(i, elem)


if __name__ == '__main__':
    test = TestClass()
    test.test_method(55555)
    test.test_method([33, 22, 11])

Есть еще один модуль, multipledispatch (не стандартный, но включенный в Anaconda и без каких-либо нестандартных зависимостей), что, как следует из названия, отличается singledispatch Позволяет мультиметоды.

В дополнение к Dispatcher объекты, с singledispatch совместимый синтаксис, он обеспечивает dispatch декоратор, который скрывает создание и манипулирование этими объектами от пользователя.

Декоратор диспетчеризации использует имя функции, чтобы выбрать соответствующий объект Dispatcher, к которому он добавляет новую сигнатуру / функцию. Когда он встречает новое имя функции, он создает новый объект Dispatcher и сохраняет пару имя / диспетчер в пространстве имен для использования в будущем.

Например:

from types import LambdaType
from multipledispatch import dispatch

class TestClass(object):

    @dispatch(object)
    def test_method(self, arg, verbose=False):
        if verbose:
            print("Let me just say,", end=" ")

        print(arg)

    @dispatch(int, float)
    def test_method(self, arg, arg2):
        print("Strength in numbers, eh?", end=" ")
        print(arg + arg2)

    @dispatch((list, tuple), LambdaType, type)
    def test_method(self, arg, arg2, arg3):
        print("Enumerate this:")

        for i, elem in enumerate(arg):
            print(i, arg3(arg2(elem)))


if __name__ == '__main__':

    test = TestClass()
    test.test_method(55555, 9.5)
    test.test_method([33, 22, 11], lambda x: x*2, float)

Functools Python 3.8 представили перегрузку функций для методов экземпляра с использованием нового декоратора.@singledispatchmethod.

Согласно документам;

отправка происходит по типу первого не-я или не-cls аргумента.

Следовательноtypeдля аргумента, который следует сразу послеselfэто тот, который запускает отправку метода. Что-то вроде ниже.

      from functools import singledispatchmethod


class Cooking:
    @singledispatchmethod
    def cook(self, arg):
        return f"I'm cooking {arg} eggs."

    @cook.register
    def _(self, arg: int):
        return f"I'm cooking {arg} eggs."

    @cook.register
    def _(self, arg: bool):
        return f"Am I cooking eggs? {arg}"

f = Cooking()
print(f.cook('many'))
# I'm cooking many eggs.
print(f.cook(50))
# I'm cooking 50 eggs.
print(f.cook(True))
# Am I cooking eggs? True

Другие вопросы по тегам