Sonny Sonny - 1 month ago 14
Python Question

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

Answer

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