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!