
Last Updated on 6 June 2024
Introduction
Python decorators are a powerful feature of the language that allows you to modify the behavior of functions or classes without changing their source code. Decorators provide a way to add functionality to existing code in a clean and modular way, making it easy to reuse code across different parts of your program.
A decorator is a function that takes another function as its argument and returns a new function that wraps the original function, adding some extra behavior or functionality. Decorators are used extensively in Python’s standard library, as well as in many popular third-party libraries and frameworks.
Define a decorator
To define a decorator, you simply define a function that takes another function as an argument and returns a new function that performs some additional processing before or after the original function is called. Below is an illustration of a basic decorator that introduces a “Hello, World!” message before a function is called:
def hello_decorator(func):
def wrapper():
print("Hello, World!")
func()
return wrapper
@hello_decorator
def my_function():
print("This is my function.")
In this example, the ‘hello_decorator‘ function takes another function (func) as an argument and returns a new function (wrapper) that first prints “Hello, World!“, and then calls the original function (func). The ‘@hello_decorator‘ syntax is used to apply the decorator to the my_function function.
When we call ‘my_function()‘, it will first print “Hello, World!“, and then print “This is my function.” This is because the ‘my_function‘ function has been modified by the ‘hello_decorator‘ decorator to add additional functionality.
Decorators with arguments
Decorators can also take arguments, which can be used to modify their behavior. Here’s an example of a decorator that takes an argument:
def repeat_decorator(num_repeats):
def decorator(func):
def wrapper():
for i in range(num_repeats):
func()
return wrapper
return decorator
@repeat_decorator(num_repeats=3)
def my_function():
print("This is my function.")
In this example, the ‘repeat_decorator‘ function takes an argument (num_repeats) and returns a decorator function that takes another function (func) as an argument and returns a new function (wrapper) that calls the original function multiple times (num_repeats times).
When we apply this decorator to the my_function function with ‘@repeat_decorator(num_repeats=3)‘, calling ‘my_function()‘ will print “This is my function.” three times.
Example 1
One of the most common uses of decorators is to add additional functionality to functions, such as logging or caching. For example, let’s say you have a function that performs some expensive computation and you want to cache its results to avoid repeating the computation. You could use a decorator to wrap the function and cache its results:
import functools
def cache(func):
cache = {}
@functools.wraps(func)
def wrapper(*args, **kwargs):
key = (args, frozenset(kwargs.items()))
if key not in cache:
cache[key] = func(*args, **kwargs)
return cache[key]
return wrapper
@cache
def expensive_computation(x, y):
# do some expensive computation here
return result
In this example, the ‘cache‘ decorator takes a function as an argument and returns a new function that wraps the original function in a caching mechanism. The decorator creates a dictionary called ‘cache‘ to store the results of each function call, keyed by the function arguments. If a given set of arguments has been seen before, the cached result is returned immediately, without re-computing the function. Otherwise, the original function is called to compute the result, which is then cached for future calls.
Example 2
Another common use of decorators is to add additional behavior to classes. For example, let’s say you have a class that represents a database connection, and you want to ensure that the connection is closed automatically when the class is no longer needed. You could use a decorator to add a context manager to the class:
def auto_close(cls):
class Wrapper:
def __init__(self, *args, **kwargs):
self._wrapped = cls(*args, **kwargs)
def __getattr__(self, name):
return getattr(self._wrapped, name)
def __enter__(self):
return self._wrapped.__enter__()
def __exit__(self, exc_type, exc_val, exc_tb):
self._wrapped.__exit__(exc_type, exc_val, exc_tb)
self._wrapped.close()
return Wrapper
@auto_close
class DatabaseConnection:
def __init__(self, host, port, username, password):
self._conn = connect(host, port, username, password)
def query(self, sql):
# execute a SQL query and return the results
pass
def close(self):
self._conn.close()
In this example, the ‘auto_close‘ decorator takes a class as an argument and returns a new class that wraps the original class in a context manager. The wrapper class forwards all attribute access and method calls to the wrapped class, but adds an ‘__enter__‘ and ‘__exit__‘ method that automatically closes the connection when the context manager exits.
Summary
Decorators are versatile and potent characteristics of Python that enable you to alter the functionality of functions and classes without making modifications to their original source code.
Decorators provide a way to add functionality to existing code in a clean and modular way, making it easy to reuse code across different parts of your program. If you’re not already using decorators in your Python code, you should definitely give them a try!
