Decorators In Python Python

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.

Decorators

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!

Conclusion

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!

Designed, built and maintained by Kimserey Lam.