A Dive Into Python Decorators

Understand logged, time, nested, and other built-in Python decorators

Senthil E
Better Programming

--

Nice living room
Photo by Spacejoy on Unsplash

Introduction

A Python decorator is a function that modifies another function and returns a function. The Python decorator concept is a little hard to understand. Let us go through in detail to understand Python decorators.

Contents

1. What is a Python Decorator?
2. Create a Simple Python Decorator
3. Debugging-Fix the Function Name and Docstring
4. Nested Decorators
5. Passing Arguments to the Decorator
6. Passing Arguments to the Function
7. Logged Decorator
8. Time Decorator
9. Functools.lru_cache(Memoize)
10. Uses of Decorators
11. Built-in Python Decorators
12. Conclusion

Python Decorator

  • Decorators were introduced in Python 2.4.
  • The Python decorator function is a function that modifies another function and returns a function.
  • It takes a function as its argument.
  • It returns a closure. A closure in Python is simply a function that is returned by another function.
  • There is a wrapper function inside the decorator function.
  • It adds some additional functionality to the existing function without changing the code of the existing function. This can be achieved by decorators.
  • A decorator allows you to execute code before and after the function; they decorate without modifying the function itself.
  • In Python, a decorator starts with the @ symbol followed by the name of the decorator function.
  • Decorators slow down the function call.
Image by the author

Create a Simple Python Decorator

Let's code and see how it works.

  1. I have a simple function below which gives the product of three numbers. Just call it the original function.

2. Now I want to modify the functionality of the original function without modifying the code in the original function. This can be done by using the Python decorator.

Now I have the following function:

  • I have the decorator function called decor_func. Inside the decorator function, I have another function called wrapper_func which accepts any number of arguments — *args and **kwargs.
  • The decor_func will return the wrapper_func.
  • The orig_func is decorated by the decor_func.

Let's see how it works.

orig_func (7,9,11)

The output is as follows:

This is the product: 693

Now, I want to add some functionality without changing the above function. So I am using a decorator function that contains a wrapper function that has some code and is executed with the original function.

  • Functions are objects.
  • Functions can be defined in a function.
  • Functions can be assigned to a variable.

You can call the decorator function by

orig_func=decor_func(orig_func)

or

@decor_func
Image by the author

The orig_func=decor_func(orig_func) or the @decor_func runs the wrapper_func in the decor_func.

When I execute the above code the output is as follows:

Execute this code before the main function is executed.
This is the product: 693

Now you can understand how Python decorators work.

Image by the author

Now you know how a basic decorator works. We can explore some additional concepts of decorators.

Debugging-Fix the Function Name and Docstring

If you do the help on the original function, it looks like this:

It gives the following:

Help on function wrapper_func in module __main__: 
wrapper_func(*args, **kwargs)
This is the wrapper function

This shows the docstring of the wrapper function. It should show the original function name and the docstring.

We can achieve this with the following workaround:

dir(orig_func)

The following attributes are there in the Python function:

[‘__annotations__’, ‘__call__’, ‘__class__’, ‘__closure__’, ‘__code__’, ‘__defaults__’, ‘__delattr__’, ‘__dict__’, ‘__dir__’, ‘__doc__’, ‘__eq__’, ‘__format__’, ‘__ge__’, ‘__get__’, ‘__getattribute__’, ‘__globals__’, ‘__gt__’, ‘__hash__’, ‘__init__’, ‘__init_subclass__’, ‘__kwdefaults__’, ‘__le__’, ‘__lt__’, ‘__module__’, ‘__name__’, ‘__ne__’, ‘__new__’, ‘__qualname__’, ‘__reduce__’, ‘__reduce_ex__’, ‘__repr__’, ‘__setattr__’, ‘__sizeof__’, ‘__str__’, ‘__subclasshook__’]

You can assign the function to a variable and display the name and docstring.

orig_func 
returns the product of a,b,c

or

We can use Wraps from the functools module. It is a function decorator which applies update_wrapper() to the decorated function. Applying functools.wraps to the wrapper returned by the decorator carries over the docstring and other metadata of the input function which is orig_func.The functools module was introduced in Python 2.5. It includes the function, which copies the name, module, and docstring of the decorated function to its wrapper. We can change the code as follows:

Nested Decorator

We can also have more than one decorator like the following:

This is the first decorator
This is the second decorator
Nested Decorators
Image by the author

Passing Arguments to the Decorator

You can also pass parameters to the decorator to be used in the code.

For example,

orig_func(7,9,11)

The output of the above is as follows:

Flag is False 
This is the product: 693

Let's go through how this works:

  • The decor_factory function is not a decorator function. Instead, it returns a decorator when called.
  • Any arguments passed to decor_factory can be referenced (as free variables) inside the function decor.
  • The decor_factory function is a decorator factory function. It is a function that creates a new decorator each time it is called.
  • In this case, the decor_factory returns the decor function. The deco function is the real decorator which takes the function(fn) as its argument.
Image by the author

Passing Arguments to the Function

We can pass the arguments to the decorator wrapper function, as shown:

Arguments are passed: San Jose , California
My city is San Jose
My State is California

If you’re making a general-purpose decorator — one you’ll apply to any function or method — then just use *args, **kwargs

@Logged Decorator

Try to create a log like when the function is called using the function name, etc.

Image by the author

The output is as shown:

The product of a*b*c: 90 
product: called 2022–01–16 04:41:50.483630+00:00

@Time Decorator

Create a decorator to calculate how much time the function took to execute.

The output will be the following:

The product of a*b*c: 90 
product ran for 0.001004s

You can also call the log and time together.

The output will be the following:

The product of a*b*c: 90 
product ran for 0.000092s
product: called 2022–01–16 04:59:14.443023+00:00

@functools.lru_cache(Memoize)

  • @lru_cache decorator, which gives you the ability to cache the result of your functions using the Least Recently Used (LRU) strategy. It uses the catching technique.lru stands for “least recently used.” For more information, please check the following link.
  • The performance can be increased using the @lru_cache decorator.
  • Catching can be implemented by using a dictionary.
  • Memoization is a form of cache. We cache the previously calculated Factorial numbers so that we don’t have to calculate them again.

Here are some examples:

Without Cache

Here we created a factorial function:

The output is as shown:

Factor5!
Factor4!
Factor3!
Factor2!
Factor1!
120

If I try factor 7, then the output is

factor(7)

And the detailed output is the following:

Factor7!
Factor6!
Factor5!
Factor4!
Factor3!
Factor2!
Factor1!
5040

With Cache

Now we are using the @lru_cache.Functools.cache is available from Python 3.9. Before 3.9, use @lru_cache.

@lru_cache has a maxsize parameter, which has a default value of 128 — which means the cache will hold at most 128 entries at any time. The acronym LRU stands for “Least Recently Used,” meaning that older entries that have not been read for a while are discarded to make room for new ones.

The output for factor(5) is the following:

Factor5!
Factor4!
Factor3!
Factor2!
Factor1!
120

If you run the function for factor 6factor(6), then the output will be

Factor6!
720

As you can see, with cache it uses the factor 1 to factor 5 from the cache and only calculates the cache 6.

If you try the factor for 4, then the output is

24

In this case, all are selected from the cache.

So @lru_cacheimproves the performance. You can also use the @time decorator to capture the time-end-start.

Uses of Decorators

  • Access control and validation.
  • Logging.
  • Timing.
  • Caching.
  • Don’t want to change the source function.
  • Trying to reuse a function that might fail.

Built-in Python Decorators

Here are a few important Python built-in decorators:

  • @functools.wraps This is a convenience function for invoking update_wrapper() as a function decorator when defining a wrapper function.
  • @functools.lru_cache Decorator to wrap a function with a memoizing callable that saves up to the maxsize most recent calls. It can save time when an expensive or I/O bound function, and it is periodically called with the same arguments.
  • @atexit.register Register func as a function to be executed at termination.
  • @classmethod Return a class method for function.
  • @property Return a property attribute.

Conclusion

As you see, Python decorators are more powerful and can be used in a lot of scenarios. Maybe you can create your own decorator and use the above concepts.

References

  1. https://gist.github.com/Zearin/2f40b7b9cfc51132851a
  2. https://github.com/chiphuyen/python-is-cool
  3. https://github.com/lord63/awesome-python-decorator
  4. https://www.python.org/dev/peps/pep-0318/
  5. https://www.python.org/dev/peps/pep-3129/
  6. https://wiki.python.org/moin/PythonDecorators
  7. https://wiki.python.org/moin/PythonDecoratorLibrary
Want to Connect?Please feel free to connect with me on LinkedIn

--

--

ML/DS - Certified GCP Professional Machine Learning Engineer, Certified AWS Professional Machine learning Speciality,Certified GCP Professional Data Engineer .