When we decorate a Python function we must take some care to ensure that the decorated function can present itself in the same way as before its decoration. Let's explore what is needed.
Decorating a Python function
Say we have an undecorated function f(). It features a docstring (line 2) and annotated arguments:
def f(a:str):
"an undecorated function"
return a
print(f.__name__,f.__annotations__,f.__doc__,f('foo'))
The final line in the previous snippet prints the name of the function, its annotation dictionary and the docstring. The output looks like this:
f {'a': <class 'str'>} an undecorated function foo
Decorating a Python function
Now say we have a function do_something() and we would like to have a decorator that would arrange that a function would call do_something() first. We might set this up like this:
def do_something():
print('oink')
def decorator(f):
def g(*args,**kwargs):
do_something()
return f(*args,**kwargs)
return g
@decorator
def f(a:str):
"a naively decorated function"
return a
print(f.__name__,f.__annotations__,f.__doc__,f('foo'))
The final line would produce the following output:
oink
g {} None foo
It is clear that do_something() is executed as it does print oink but although our decorated function can be called as f(), its name, annototations and doctring are different.
Preserving attributes when decorating a Python function
To resolve these matters we can rewrite our decorator as follows:
def decorator(f):
def g(*args,**kwargs):
do_something()
return f(*args,**kwargs)
g.__name__ = f.__name__
g.__doc__ = f.__doc__
g.__annotations__ = f.__annotations__
return g
@decorator
def f(a:str):
"a less naively decorated function"
return a
print(f.__name__,f.__annotations__,f.__doc__,f('foo'))
We now simply copy the relevant attributes to the newly created function and if we now examine the output produced by the final line we get what we expect:
oink
f {'a': <class 'str'>} a less naively decorated function foo