Sep 24th, 2021 - written by Kimserey with .
In Python, we can add extra functionalities to existing functions with decorators but this comes with small gotchhas. The functools
module comes with a special wraps
decorator which addresses those issues. In today’s post we will look at how to use wraps
with example.
A decorators in Python is defined as function taking the wrapped function as argument and returning the wrapper function.
1
2
3
4
5
6
7
In [4]: def say_hi(func):
...: """Say hi"""
...: def wrapper(*args, **kwargs):
...: print("Hi")
...: return func()
...: return wrapper
...:
For example here we define a say_hi
decorator which we can use as such:
1
2
3
4
5
6
7
8
In [6]: @say_hi
...: def my_function():
...: print("hello")
...:
In [7]: my_function()
Hi
hello
The notation @
will result in the decorator being called with the function it decorates as argument.
Calling the decorator manually can also give us insight on how it actually works. If we didn’t decorate my_function
, we could have created it as followed:
1
2
3
4
5
In [3]: x = say_hi(my_function)
In [4]: x()
Hi
hello
Following that way of calling, we can derive how to provide arguments to the decorator by trying to achieve the following:
1
In [5]: x = say_hi("my message")(my_function)
In order to accept arguments in decorators, we can wrap the decorater in another function which will return the decorator itself:
1
2
3
4
5
6
7
8
9
In [29]: def say_hi(message):
...: """Say hi"""
...: def wrapper(func):
...: def _wrapper(*args, **kwargs):
...: print("Hi", message)
...: return func()
...: return _wrapper
...: return wrapper
...:
This then allow us to call say_hi
providing it an argument which then can be used in the underlying decorator:
1
2
3
4
5
6
7
8
In [30]: @say_hi("hehe")
...: def my_function():
...: """my function"""
...: print("hello")
In [34]: my_function()
Hi hehe
hello
Here we see that we successfully passed the argument from say_hi
and it was apply to the resulting decorator.
wrap
Although decorators functionalities work without surprise, the resulting function would be the wrapper
function. Therefore trying to access any property on the function will return the wrapper properties.
1
2
In [17]: my_function
Out[17]: <function __main__.say_hi.<locals>.wrapper(*args, **kwargs)>
We can see that my_function
is showing say_hi.wrapper
.
To cater for this, functools
provides us wraps
, a decorator which we can use to decorate the wrapper in order to keep all the references to the wrapped function:
1
2
3
4
5
6
7
8
9
In [18]: from functools import wraps
In [19]: def say_hi(func):
...: """Say hi"""
...: @wraps(func)
...: def wrapper(*args, **kwargs):
...: print("Hi")
...: return func()
...: return wrapper
The only change is to use @wraps(func)
on our wrapper
which will forward all metadat from func
to the wrapper.
1
2
3
4
5
6
7
8
In [20]: my_function
Out[20]: <function __main__.my_function()>
In [21]: my_function.__name__
Out[21]: 'my_function'
In [22]: my_function.__doc__
Out[22]: 'my function'
We can then see that my_function
kept its metadata. And that concludes today’s post!
In today’s post we looked at Python decorators. We looked at how we could use decorators and how we could provide arguments. We then moved on to see how we could keep the metadata on decorated functions. I hope you liked this post and I see you on the next one!