Decorators in Python are a feature that allow developers to modify or extend the behavior of functions or methods without altering the source code. Represented by the @
symbol, decorators are widely used for logging, validation, caching, and other enhancements.
In this article, we will explore Python’s @
decorator, dive into real-world examples, and demonstrate how to use it.
What is a Decorator?
A decorator is a higher-order function that takes another function as input, adds functionality to it, and returns the modified function. Essentially, it “wraps” a function, altering its behavior without changing its core logic.
The basic syntax of a decorator:
@decorator_name
def function_to_decorate():
pass
The @decorator_name
is equivalent to writing:
function_to_decorate = decorator_name(function_to_decorate)
Setting Up the Environment
First, set up a Python virtual environment to isolate dependencies.
For Windows:
python -m venv venv
venv\Scripts\activate
For Linux/Mac:
python3 -m venv venv
source venv/bin/activate
Example 1: Logging Function Calls
Logging is a common use case for decorators. Let’s create a simple decorator that logs when a function is called.
File Structure:
project/
├── log_decorator.py
├── app.py
log_decorator.py:
import datetime
def log_function_call(func):
def wrapper(*args, **kwargs):
print(f"{datetime.datetime.now()}: Calling {func.__name__} with {args} and {kwargs}")
result = func(*args, **kwargs)
print(f"{datetime.datetime.now()}: {func.__name__} returned {result}")
return result
return wrapper
app.py:
from log_decorator import log_function_call
@log_function_call
def add_numbers(a, b):
return a + b
if __name__ == "__main__":
print(add_numbers(5, 10))
Output when running app.py
:
2024-12-12 14:30:15.123456: Calling add_numbers with (5, 10) and {}
2024-12-12 14:30:15.123456: add_numbers returned 15
15
Explanation of Example 1
@log_function_call
def add_numbers(a, b):
return a + b
What the Decorator Does:
@log_function_call
applies thelog_function_call
decorator to theadd_numbers
function.
2. When the Python interpreter sees the @log_function_call
, it automatically replaces the add_numbers
function with the result of log_function_call(add_numbers)
.
3. Inside log_function_call
, a new wrapper
function is created. This wrapper
:
- Logs the call to
add_numbers
, printing the function name and its arguments. - Executes the original
add_numbers
function and stores its result. - Logs the result of the function call before returning it.
4. The add_numbers
function is now “wrapped” by the wrapper
, so whenever add_numbers
is called, it first goes through the log_function_call
logic.
Example 2: Authentication for a Web Application
In a real-world application, decorators can enforce user authentication. Let’s demonstrate this for a simple Flask web app.
Setup Flask:
pip install flask
File Structure:
project/
├── app/
│ ├── __init__.py
│ ├── decorators.py
│ ├── routes.py
app/decorators.py:
from flask import request, jsonify
def require_api_key(func):
def wrapper(*args, **kwargs):
api_key = request.headers.get("x-api-key")
if api_key != "mysecurekey123":
return jsonify({"error": "Unauthorized"}), 401
return func(*args, **kwargs)
return wrapper
app/routes.py:
from flask import Flask, jsonify
from .decorators import require_api_key
app = Flask(__name__)
@app.route("/secure-data", methods=["GET"])
@require_api_key
def secure_data():
return jsonify({"data": "This is secure data!"})
if __name__ == "__main__":
app.run(debug=True)
Run the application with
python -m app.routes
Send a request with the proper API key using a tool like curl
:
curl -H "x-api-key: mysecurekey123" http://127.0.0.1:5000/secure-data
Explanation of Example 2:
@require_api_key
def secure_data():
return jsonify({"data": "This is secure data!"})
What the Decorator Does:
@require_api_key
applies therequire_api_key
decorator to thesecure_data
function.
2. The require_api_key
decorator wraps the secure_data
function with additional logic. Specifically:
- It checks whether an
x-api-key
header is present in the HTTP request and validates it. - If the key is invalid or missing, the wrapper function immediately returns an unauthorized response (
401
). - If the key is valid, the wrapper proceeds to call the original
secure_data
function, which returns the secure data as JSON.
3. The decorator enforces a precondition (authentication) without altering the secure_data
function itself. This separation of concerns ensures cleaner, more modular code.
Example 3: Caching Results
Caching is useful when you want to store the results of expensive function calls for reuse.
File Structure:
project/
├── cache_decorator.py
├── app.py
cache_decorator.py:
import functools
def cache(func):
memo = {}
@functools.wraps(func)
def wrapper(*args):
if args in memo:
print("Returning cached result")
return memo[args]
print("Calculating result")
result = func(*args)
memo[args] = result
return result
return wrapper
app.py:
from cache_decorator import cache
@cache
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
if __name__ == "__main__":
print(fibonacci(10))
print(fibonacci(10)) # This will return the cached result
Explanation of Example 3
@cache
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
What the Decorator Does:
@cache
applies thecache
decorator to thefibonacci
function.
2. The cache
decorator creates a wrapper
function that:
- Checks if the arguments passed to
fibonacci
have already been cached in thememo
dictionary. - If the result is cached, it retrieves the value and skips recalculating it, improving performance.
- If the result is not cached, it calculates the result by calling the original
fibonacci
function, stores the result in thememo
dictionary, and then returns the result.
3. The decorator transparently manages caching, so you can use fibonacci
without worrying about caching logic in the main code. This is particularly useful for computationally expensive recursive functions.
Common Theme Across All Examples
In all three examples, the @
decorator modifies the behavior of the target function by wrapping it inside another function (the wrapper). This wrapper can:
- Add new functionality (e.g., logging, authentication).
- Modify inputs or outputs.
- Enforce preconditions (e.g., checking API keys).
- Improve performance (e.g., caching).
At its core, the decorator acts as a middleman between the caller and the original function, enhancing or altering the behavior without changing the function’s source code. This approach keeps code clean, modular, and easier to maintain.
Thank you for reading this article. I hope you found it helpful and informative. If you have any questions, or if you would like to suggest new Python code examples or topics for future tutorials, please feel free to reach out. Your feedback and suggestions are always welcome!
Happy coding!
C. C. Python Programming
You can also find this article at Medium.com