3

I'm trying to code a method from a class that uses a decorator from another class. The problem is that I need information stored in the Class that contains the decorator (ClassWithDecorator.decorator_param). To achieve that I'm using partial, injecting self as the first argument, but when I do that the self, from the class that uses the decorator " gets lost" somehow and I end up getting an error. Note that this does not happen if I remove partial() from my_decorator() and "self" will be correctly stored inside *args.

See the code sample:

from functools import partial


class ClassWithDecorator:
    def __init__(self):
        self.decorator_param = "PARAM"

    def my_decorator(self, decorated_func):
        def my_callable(ClassWithDecorator_instance, *args, **kwargs):
            # Do something with decorator_param
            print(ClassWithDecorator_instance.decorator_param)
            return decorated_func(*args, **kwargs)

        return partial(my_callable, self)


decorator_instance = ClassWithDecorator()


class WillCallDecorator:
    def __init__(self):
        self.other_param = "WillCallDecorator variable"

    @decorator_instance.my_decorator
    def decorated_method(self):
        pass


WillCallDecorator().decorated_method()

I get

PARAM
Traceback (most recent call last):
  File "****/decorator.py", line 32, in <module>
    WillCallDecorator().decorated_method()
  File "****/decorator.py", line 12, in my_callable
    return decorated_func(*args, **kwargs)
TypeError: decorated_method() missing 1 required positional argument: 'self'

How can I pass the self corresponding to WillCallDecorator() into decorated_method() but at the same time pass information from its own class to my_callable() ?

Gonzalo Hernandez
  • 727
  • 2
  • 9
  • 25

2 Answers2

5

It seems that you may want to use partialmethod instead of partial:

From the docs:

class functools.partialmethod(func, /, *args, **keywords)

When func is a non-descriptor callable, an appropriate bound method is created dynamically. This behaves like a normal Python function when used as a method: the self argument will be inserted as the first positional argument, even before the args and keywords supplied to the partialmethod constructor.

Energya
  • 2,623
  • 2
  • 19
  • 24
2

So much simpler just to use the self variable you already have. There is absolutely no reason to be using partial or partialmethod here at all:

from functools import partial


class ClassWithDecorator:
    def __init__(self):
        self.decorator_param = "PARAM"

    def my_decorator(self, decorated_func):
        def my_callable(*args, **kwargs):
            # Do something with decorator_param
            print(self.decorator_param)
            return decorated_func(*args, **kwargs)

        return my_callable


decorator_instance = ClassWithDecorator()


class WillCallDecorator:
    def __init__(self):
        self.other_param = "WillCallDecorator variable"

    @decorator_instance.my_decorator
    def decorated_method(self):
        pass


WillCallDecorator().decorated_method()

Also, to answer your question about why your code didn't work, when you access something.decorated_method() the code checks whether decorated_method is a function and if so turns it internally into a call WillCallDecorator.decorated_method(something). But the value returned from partial is a functools.partial object, not a function. So the class lookup binding won't happen here.

In more detail, something.method(arg) is equivalent to SomethingClass.method.__get__(something, arg) when something doesn't have an attribute method and its type SomethingClass does have the attribute and the attribute has a method __get__ but the full set of steps for attribute lookup is quite complicated.

Duncan
  • 92,073
  • 11
  • 122
  • 156