0

I thought the following would work as a decorator

class D:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        return self.func(*args, **kwargs)


class A:
    @D
    def f(self, x):
        pass

a=A()
a.f(1)

but I get TypeError: f() missing 1 required positional argument: 'x'

What's going on and is there a way a can use a class as a decorator like this?

Gere
  • 12,075
  • 18
  • 62
  • 94
  • 1
    Related: https://stackoverflow.com/questions/51542063/decorators-for-instance-methods The problem is that you are trying to decorate an instance method, not that you are implementing the decorator with a class – DeepSpace Feb 05 '21 at 13:08
  • The exact problem is that `self.func(*args, **kwargs)` does not pass the `self` argument to `self.func` (which is `A.f`). If you add `self` (`self.func(self, *args, **kwargs)`) you will have another problem: `self` here refers to a `D` instance, not an `A` instance. As a matter of fact, I'm not sure it is possible to decorate an instance method with a decorator that is implemented as a class. You may need to implement it with a function – DeepSpace Feb 05 '21 at 13:09
  • 3
    Actually, it is possible if `D` is a descriptor. See the duplicate that I marked – DeepSpace Feb 05 '21 at 13:12
  • the "duplicate" is not. There may be other question dealing with this, but the one ou marked is about decorating a method with a plain function - and does not mention the adjustments needed when using a class at all. – jsbueno Feb 05 '21 at 15:03
  • `D`, as a class, is callable, but it's not the decorator you defined. That's `D()` (aside from the descriptor issues raised by decorating a function to be used as a method). – chepner Feb 05 '21 at 15:04

1 Answers1

1

The thing is that besides the decorator mechanism, there is the mechanism that Python uses so that functions inside class bodies behave as instance methods: it is the "descriptor protocol". That is actually simple: all function objects have a __get__ method (but not __set__ or __del__) method, which make of them "non data descriptors". When Python retrieves the attribute from an instance, __get__ is called with the instance as a parameter - the __get__ method them have to return a callable that will work as the method, and has to know which was the instance called:


# example only - DO NOT DO THIS but for learning purposes,
#  due to concurrency problems:

class D:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        
        return self.func(self.instance, *args, **kwargs)

    def __get__(self, instance, owner):
        self.instance = instance
        return self

class A:
    @D
    def f(self, x):
        print(self, x)

a=A()
a.f(1)

This will print "<main.A object at 0x...> 1"

However, as it is easily perceivable this only allows the decorated method to be called in a single instance at once - even non parallel code that owns more than an instance of "A" could have the method called with the wrong instance in mind. That is, this sequence:


In [127]: a1 = A()                

In [128]: a2 = A()                

In [129]: f1 =  a1.f              

In [130]: f2 = a2.f               

In [131]: f1() 

will end up calling "a2.f()" not "a1.f()"

To avoid this, what you have to return is a callable from __get__ that won't need to retrieve the instance as a class attribute. One way to do that is to create a partial callable and include that - however, note that since this is a necessary step, there is no need for the decorator class itself to have the "run wrapper + original code" function in the __call__ method - it could have any name:

from functools import partial

class D:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, _instance=None, **kwargs):
        if _instance:
            return self.func(_instance, *args, **kwargs)
        else: 
            return self.func(*args, **kwargs)
        

    def __get__(self, instance, owner):
        return partial(self.__call__, _instance=instance)
      

class A:
    @D
    def f(self, x):
        print(self, x)

a=A()
a.f(1)
jsbueno
  • 99,910
  • 10
  • 151
  • 209