Search code examples
pythonmethodsdecorator

Why is 'self' not in args when using a class as a decorator?


The top answer for Class method decorator with self arguments? (direct link to answer: https://stackoverflow.com/a/11731208) describes how a decorator can access attributes of the object when decorating a method.

I tried to apply this to my own code, but realized it only works when using a function as the decorator. For some reason, when using a class as the decorator, the arguments tuple no longer contains the self object. Why does this happen, and is there some other way to access the wrapped method's self?

To demonstrate:

import functools


class DecoratorClass:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func                    
                        
    def __call__(self, *args, **kwargs):
        print("args:", args)            
        print("kwargs:", kwargs)
        print("Counting with:", args[0].name)
        self.func(*args, **kwargs)           
                                  

def decorator_func(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("args:", args)     
        print("kwargs:", kwargs)
        print("Counting with:", args[0].name)
        func(*args, **kwargs)                
    return wrapper           
                  

class Counter:
    def __init__(self, name, count):
        self.name = name            
        self.count = count
                          
    @decorator_func
    def count_a(self):
        for i in range(self.count):
            print(i)               
                    
    @DecoratorClass
    def count_b(self):
        for i in range(self.count):
            print(i)               
                    

c = Counter('my counter', 3)
                            
c.count_a()
print()    
c.count_b()

Output:

args: (<__main__.Counter object at 0x7f4491a21970>,)
kwargs: {}
Counting with: my counter
0
1
2

args: ()
kwargs: {}
Traceback (most recent call last):
  File "./test2.py", line 48, in <module>
    c.count_b()
  File "./test2.py", line 14, in __call__
    print("Counting with:", args[0].name)
IndexError: tuple index out of range

Solution

  • It's a known problem (see here). I actually ran into this same issue when implementing a class decorator in one of my projects a while back.

    To fix, I added the below method to my class - which you can also add to your DecoratorClass, and then it should all work without surprises.

    def __get__(self, instance, owner):
        """
        Fix: make our decorator class a decorator, so that it also works to
        decorate instance methods.
        https://stackoverflow.com/a/30105234/10237506
        """
        from functools import partial
        return partial(self.__call__, instance)
    

    Also, do see the linked SO answer for an example of why this is.