Understanding Python Decorators with Practical Examples
Table of Contents
- What are Python Decorators?
- How to Define and Use Decorators
- Decorators with Arguments
- Common Use Cases of Decorators
- Best Practices for Using Decorators
- Conclusion
- References
What are Python Decorators?
A Python decorator is a function that takes another function as an argument, adds some functionality to it, and then returns the modified function. In other words, a decorator is a wrapper around a function. The original function’s behavior is extended without modifying its source code.
Here is a simple example to illustrate the concept:
def my_decorator(func):
def wrapper():
print("Before the function is called.")
func()
print("After the function is called.")
return wrapper
def say_hello():
print("Hello!")
# Apply the decorator to the say_hello function
decorated_say_hello = my_decorator(say_hello)
# Call the decorated function
decorated_say_hello()
In this example, my_decorator is a decorator function that takes say_hello as an argument. It defines a new function wrapper inside it, which adds some print statements before and after calling the original function func. Finally, it returns the wrapper function. When we call decorated_say_hello, it executes the wrapper function, which in turn calls the original say_hello function.
How to Define and Use Decorators
Python provides a more concise syntax for applying decorators using the @ symbol. We can rewrite the previous example as follows:
def my_decorator(func):
def wrapper():
print("Before the function is called.")
func()
print("After the function is called.")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
# Call the decorated function
say_hello()
The @my_decorator syntax is equivalent to say_hello = my_decorator(say_hello). It makes the code more readable and easier to understand.
Decorators with Arguments
Sometimes, you may want to pass arguments to a decorator. To achieve this, you need to define a decorator factory function that returns a decorator. Here is an example:
def repeat(n):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def say_hello():
print("Hello!")
say_hello()
In this example, repeat is a decorator factory function that takes an argument n. It returns a decorator function decorator. The decorator function takes the original function func as an argument and defines a wrapper function inside it. The wrapper function calls the original function n times. Finally, it returns the wrapper function.
Common Use Cases of Decorators
Logging
Decorators can be used to add logging functionality to functions. Here is an example:
import logging
def log_function_call(func):
def wrapper(*args, **kwargs):
logging.basicConfig(level=logging.INFO)
logging.info(f"Calling function {func.__name__} with args: {args} and kwargs: {kwargs}")
result = func(*args, **kwargs)
logging.info(f"Function {func.__name__} returned: {result}")
return result
return wrapper
@log_function_call
def add(a, b):
return a + b
result = add(2, 3)
print(result)
In this example, the log_function_call decorator adds logging statements before and after calling the original function. It logs the function name, the arguments passed to the function, and the return value.
Authentication
Decorators can be used to implement authentication in web applications. Here is a simple example using Flask:
from flask import Flask, request, abort
app = Flask(__name__)
def authenticate(func):
def wrapper(*args, **kwargs):
if 'Authorization' not in request.headers:
abort(401)
return func(*args, **kwargs)
return wrapper
@app.route('/protected')
@authenticate
def protected_route():
return "This is a protected route."
if __name__ == '__main__':
app.run()
In this example, the authenticate decorator checks if the Authorization header is present in the request. If not, it returns a 401 Unauthorized error. Otherwise, it calls the original function.
Best Practices for Using Decorators
- Keep Decorators Simple: Decorators should be as simple as possible. Avoid adding too much complex logic inside a decorator.
- Use Descriptive Names: Use descriptive names for decorators to make the code more readable.
- Preserve Function Metadata: When using a decorator, make sure to preserve the original function’s metadata, such as its name and docstring. You can use the
functools.wrapsdecorator to achieve this. Here is an example:
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper():
print("Before the function is called.")
func()
print("After the function is called.")
return wrapper
@my_decorator
def say_hello():
"""This function says hello."""
print("Hello!")
print(say_hello.__name__)
print(say_hello.__doc__)
In this example, the functools.wraps decorator is used to preserve the original function’s metadata.
Conclusion
Python decorators are a powerful and flexible feature that allows you to modify the behavior of functions or classes without changing their source code. They are widely used in Python libraries and frameworks for various purposes, such as logging, authentication, and caching. By understanding the fundamental concepts, usage methods, common practices, and best practices of decorators, you can write more modular and reusable code.
References
- Python official documentation: https://docs.python.org/3/glossary.html#term-decorator
- “Python Tricks: A Buffet of Awesome Python Features” by Dan Bader