Класс декоратор не привязывает себя - PullRequest
0 голосов
/ 20 февраля 2020

Я определил класс, который определяет декоратор как метод этого класса. Сам декоратор создает вызываемый экземпляр второго класса, который заменяет декорированный метод. Поскольку декорированный метод теперь фактически является классом, я могу вызывать методы для него. В моем (вымышленном, минимальном) примере я хочу зарегистрировать обратные вызовы с настраиваемым максимальным количеством обратных вызовов на метод.

class CallbackAcceptor:

    def __init__(self, max_num_callbacks, func):
        self._func = func
        self._max_num_callbacks = max_num_callbacks
        self._callbacks = []

    def __call__(self, *args, **kwargs):
        # This ends up being called when the decorated method is called
        for callback in self._callbacks:
            print(f"Calling {callback.__name__}({args}, {kwargs})")
            callback(*args, **kwargs)
        return self._func(*args, **kwargs)  # this line is the problem, self is not bound

    def register_callback(self, func):
        # Here I can register another callback for the decorated function
        if len(self._callbacks) < self._max_num_callbacks:
            self._callbacks.append(func)
        else:
            raise RuntimeError(f"Can not register any more callbacks for {self._func.__name__}")
        return func


class MethodsWithCallbacksRegistry:

    def __init__(self):
        self.registry = {}  # Keep track of everything that accepts callbacks

    def accept_callbacks(self, max_num_callbacks: int = 1):

        def _make_accept_callbacks(func):
            # Convert func to an CallbackAcceptor instance so we can register callbacks on it
            if func.__name__ not in self.registry:
                self.registry[func.__name__] = CallbackAcceptor(max_num_callbacks=max_num_callbacks, func=func)
            return self.registry[func.__name__]

        return _make_accept_callbacks

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

registry = MethodsWithCallbacksRegistry()

@registry.accept_callbacks(max_num_callbacks=1)
def bar(i):
    return i * 10

@bar.register_callback
def bar_callback(*args, **kwargs):
    print("Bar Callback")

print(bar(i=10))  # Works fine, prints "Bar Callback" and then 100

Теперь, если я определю метод для приема обратных вызовов:

class Test:

    @registry.accept_callbacks(max_num_callbacks=1)
    def foo(self, i):
        return i * 2

@Test.foo.register_callback
def foo_callback(*args, **kwargs):
    print("Foo Callback")

Это работает, если я передаю себя явно, но не если Я просто предполагаю, что экземпляр привязан:

t = Test()
# Note that I pass the instance of t explicitly as self
Test.foo(t, i=5)  # Works, prints "Foo Callback" and then 10
t.foo(t, i=5)  # Works, prints "Foo Callback" and then 10

t.foo(i=5)  # Crashes, because self is not passed to foo

Это трассировка:

Traceback (most recent call last):
  File "/home/veith/.PyCharmCE2019.3/config/scratches/scratch_4.py", line 62, in <module>
    t.foo(i=5)
  File "/home/veith/.PyCharmCE2019.3/config/scratches/scratch_4.py", line 13, in __call__
    return self._func(*args, **kwargs)  # this line is the problem, self is not bound
TypeError: foo() missing 1 required positional argument: 'self'

Я всегда думал, что t.foo(i=5) в основном syntacti c сахар для Test.foo(t, i=5) через дескрипторы, но, похоже, я не прав. Итак, вот мои вопросы:

  1. В чем причина того, что это не работает должным образом?
  2. Что мне нужно сделать, чтобы это работало?

Спасибо!

PS: я использую python 3.8

1 Ответ

0 голосов
/ 20 февраля 2020

Если вы сделаете CallbackAcceptor дескриптор , он будет работать следующим образом:

class CallbackAcceptor:

    def __init__(self, max_num_callbacks, func):
        self._func = func
        self._max_num_callbacks = max_num_callbacks
        self._callbacks = []

    def __call__(self, *args, **kwargs):
        # This ends up being called when the decorated method is called
        for callback in self._callbacks:
            print(f"Calling {callback.__name__}({args}, {kwargs})")
            callback(*args, **kwargs)

        return self._func(*args, **kwargs)

    def register_callback(self, func):
        # Here I can register another callback for the decorated function
        if len(self._callbacks) < self._max_num_callbacks:
            self._callbacks.append(func)
        else:
            raise RuntimeError(f"Can not register any more callbacks for {self._func.__name__}")
        return func

    # Implementing __get__ makes this a descriptor
    def __get__(self, obj, objtype=None):
        if obj is not None:
            # the call is made on an instance, we can pass obj as the self of the function that will be called
            return functools.partial(self.__call__, obj)
        # Called on a class or a raw function, just return self so we can register more callbacks
        return self

Вызов теперь работает, как и ожидалось:

print(bar(i=10))
# Bar Callback
# 100

t = Test()
t.foo(i=5)
# Foo Callback
# 10

t.foo(t, i=5)
# TypeError: foo() got multiple values for argument 'i'

...