Search code examples
pythonpython-2.7python-decorators

Python chaining decorators overwrite attributes


I have two decorators. Each decorator gets a function as an argument. Each decorator sets an attribute to the function. After chaining the decorators on a single function, I would expect to see 2 new attributes. However, the top decorator t2 "overwrites" the attributes t1 sets. In otherwise, t1 no longer exists after everything is resolved. Can anyone explain why and how to fix it?

def t1(function):
 def wrapper(*args, **kwargs):
  setattr(wrapper, "t1", True)
  return function(*args, **kwargs)
 setattr(wrapper, "t1", False)
 return wrapper

def t2(function):
 def wrapper(*args, **kwargs):
  setattr(wrapper, "t2", True)
  return function(*args, **kwargs)
 setattr(wrapper, "t2", False)
 return wrapper

@t2
@t1
def test():
 pass

Solution

  • It happens, because your decorators set attributes on wrappers. When the first decorated sets the attribute on its wrapper, it passes the wrapper to the second decorater, that adds another wrapper on top of the first one and sets the attribute on the second wrapper. So you end up with the second wrapper.

    In [3]: def decorator_a(fn):
       ...:     def wrapper(*args, **kwargs):
       ...:         return fn(*args, **kwargs)
       ...:     print("I'm setting the attribute on function {}".format(id(wrapper)))
       ...:     setattr(wrapper, "attr1", True)
       ...:     return wrapper
       ...: 
    
    In [4]: def decorator_b(fn):
       ...:     def wrapper(*args, **kwargs):
       ...:         return fn(*args, **kwargs)
       ...:     print("I'm setting the attribute on function {}".format(id(wrapper)))
       ...:     setattr(wrapper, "attr2", True)
       ...:     return wrapper
       ...: 
    
    In [5]: first_time_decorated = decorator_a(lambda x: x)
    I'm setting the attribute on function 4361847536
    
    In [6]: second_time_decorated = decorator_b(first_time_decorated)
    I'm setting the attribute on function 4361441064
    

    You can solve this by setting all attributes of a function being decorated on the wrapper

    In [14]: def decorator_a(fn):
        ...:     def wrapper(*args, **kwargs):
        ...:         return fn(*args, **kwargs)
        ...:     setattr(wrapper, "attr1", True)
        ...:     for attribute in set(dir(fn)) - set(dir(wrapper)):
        ...:         setattr(wrapper, attribute, getattr(fn, attribute))
        ...:     return wrapper
        ...: 
    
    In [15]: def decorator_b(fn):
        ...:     def wrapper(*args, **kwargs):
        ...:         return fn(*args, **kwargs)
        ...:     setattr(wrapper, "attr2", True)
        ...:     for attribute in set(dir(fn)) - set(dir(wrapper)):
        ...:         setattr(wrapper, attribute, getattr(fn, attribute))
        ...:     return wrapper
        ...: 
    
    In [16]: first_time_decorated = decorator_a(lambda x: x)
    
    In [17]: second_time_decorated = decorator_b(first_time_decorated)
    
    In [18]: second_time_decorated.attr1
    Out[18]: True
    
    In [19]: second_time_decorated.attr2
    Out[19]: True