Подтвердить что ты не робот

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

4b9b3361

Ответ 1

Update: As of Python 3.8, functools.singledispatchmethod allows single dispatch on methods, classmethods, abstractmethods, и staticmethods.

Для более старых версий Python см. остальную часть этого ответа.

Глядя на источник для 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]

Ответ 2

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

Как указано в принятом ответе, 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 -compatible он предоставляет декоратор 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)