What Are Decorators in Python?
If you've been writing Python for more than a week, you've probably seen the @ symbol above a function. This is a decorator — a powerful and clear programming tool.
Decorators in Python allow you to "wrap" one function around another. They modify or extend the behavior of a function without changing its source code. Decorators are used everywhere: in Flask, Django, FastAPI, pytest, and even in the Python standard library.
How Decorators Work
Decorator Basics
A decorator is a function that takes another function and returns a new function or a modified version of the old one.
def decorator(func):
def wrapper():
print("Before call")
func()
print("After call")
return wrapper
@decorator
def say_hello():
print("Hello!")
say_hello()
Execution result:
Before call
Hello!
After call
The @decorator symbol is syntactic sugar for:
say_hello = decorator(say_hello)
Inner Workings of Decorators
To understand decorators in Python, you need to know the key concepts:
- In Python, functions are first-class objects.
- Functions can be passed as arguments to other functions.
- Other functions can be defined inside functions.
- There is a closure mechanism.
Universal Decorator Template
The minimum decorator template looks like this:
def decorator(func):
def wrapper(*args, **kwargs):
# Actions before function execution
result = func(*args, **kwargs)
# Actions after function execution
return result
return wrapper
Practical Examples of Decorators
Execution Time Measurement Decorator
import time
def timing(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
execution_time = time.time() - start
print(f"{func.__name__} took {execution_time:.4f} sec")
return result
return wrapper
@timing
def slow_func():
time.sleep(1)
return "Operation completed"
slow_func()
This decorator is useful for profiling function performance and optimizing code.
Authorization Check Decorator
def require_admin(func):
def wrapper(user, *args, **kwargs):
if user != "admin":
raise PermissionError("Access denied")
return func(user, *args, **kwargs)
return wrapper
@require_admin
def delete_database(user):
print("Database deleted!")
return True
delete_database("admin") # Successful execution
This decorator ensures application security by controlling access to critical functions.
Logging Decorator
import logging
from functools import wraps
def log_calls(func):
@wraps(func)
def wrapper(*args, **kwargs):
logging.info(f"Calling function {func.__name__} with arguments {args}, {kwargs}")
try:
result = func(*args, **kwargs)
logging.info(f"Function {func.__name__} executed successfully")
return result
except Exception as e:
logging.error(f"Error in function {func.__name__}: {e}")
raise
return wrapper
Decorators with Arguments
Creating a Decorator Factory
When a decorator needs to accept parameters, a decorator factory is created:
def repeat(n):
def decorator(func):
def wrapper(*args, **kwargs):
results = []
for _ in range(n):
result = func(*args, **kwargs)
results.append(result)
return results
return wrapper
return decorator
@repeat(3)
def greet():
print("Hello!")
return "Greeting"
greet()
Decorator with Conditional Parameters
def cache_result(ttl_seconds=300):
def decorator(func):
cache = {}
def wrapper(*args, **kwargs):
key = str(args) + str(kwargs)
if key in cache:
cached_time, cached_result = cache[key]
if time.time() - cached_time < ttl_seconds:
return cached_result
result = func(*args, **kwargs)
cache[key] = (time.time(), result)
return result
return wrapper
return decorator
Built-in Python Decorators
Main Built-in Decorators
Python provides several built-in decorators for working with classes and functions:
@staticmethod— creates a static class method that does not require an instance.@classmethod— creates a class method that takes the class as the first argumentcls.@property— converts a method into an attribute with getter, setter, and deleter capabilities.@functools.lru_cache— caches function results to improve performance.
Example of Using @property
class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature cannot be below absolute zero")
self._celsius = value
@property
def fahrenheit(self):
return (self._celsius * 9/5) + 32
Saving Function Metadata
Using functools.wraps
To preserve the function name, documentation, and other attributes of the original function, you need to use the @functools.wraps decorator:
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling function: {func.__name__}")
return func(*args, **kwargs)
return wrapper
@my_decorator
def important_function():
"""This is an important function."""
return "Result"
print(important_function.__name__) # important_function
print(important_function.__doc__) # This is an important function.
Without @wraps, the function name would become wrapper, and the documentation would be lost.
Applying Decorators in Popular Frameworks
Django
In Django, decorators are widely used to control access and handle HTTP requests:
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods
@login_required
@require_http_methods(["GET", "POST"])
def dashboard(request):
return render(request, 'dashboard.html')
Flask
Flask uses decorators for routing and handling requests:
from flask import Flask
app = Flask(__name__)
@app.route('/home')
@app.route('/dashboard')
def home():
return "Welcome!"
@app.before_request
def before_request():
print("Executed before each request")
pytest
In pytest, decorators are used to parametrize tests and configure the test environment:
import pytest
@pytest.mark.parametrize("a,b,expected", [
(1, 2, 3),
(2, 3, 5),
(10, 5, 15)
])
def test_sum(a, b, expected):
assert a + b == expected
@pytest.fixture
def sample_data():
return {"key": "value"}
Using Multiple Decorators
Decorator Application Order
When using multiple decorators, they are applied from bottom to top:
@decorator_a
@decorator_b
@decorator_c
def func():
pass
Equivalent to the notation:
func = decorator_a(decorator_b(decorator_c(func)))
Practical Example of Multiple Decorators
def bold(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return f"<b>{result}</b>"
return wrapper
def italic(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return f"<i>{result}</i>"
return wrapper
@bold
@italic
def say_hello():
return "Hello!"
print(say_hello()) # <b><i>Hello!</i></b>
Recommendations for Using Decorators
Best Practices
When working with decorators, follow these principles:
- Always use
@functools.wrapsto preserve function metadata. - Apply
*argsand**kwargsfor decorator universality. - Avoid creating overly complex nested decorators.
- Don't overuse decorators with side effects.
- Remember that decorators are applied during module import, not when the function is called.
Testing Functions with Decorators
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Decorator worked")
return func(*args, **kwargs)
return wrapper
@my_decorator
def calculate(x, y):
return x + y
# To test the original function
original_calculate = calculate.__wrapped__
assert original_calculate(2, 3) == 5
Common Mistakes and Solutions
Main Errors When Working with Decorators
- Problem of losing function metadata.
- Solution: use
@functools.wraps.
- Solution: use
- Incorrect handling of arguments.
- Solution: use
*argsand**kwargsin the wrapper function.
- Solution: use
- Errors in decorators with arguments.
- Solution: create the correct decorator factory with three levels of nesting.
- Performance issues.
- Solution: cache results and avoid redundant calculations in the decorator.
Debugging Decorated Functions
import functools
def debug_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"DEBUG: Calling {func.__name__} with args={args}, kwargs={kwargs}")
try:
result = func(*args, **kwargs)
print(f"DEBUG: {func.__name__} returned {result}")
return result
except Exception as e:
print(f"DEBUG: {func.__name__} raised exception {e}")
raise
return wrapper
Conclusion
Decorators in Python are a powerful tool for extending functionality without changing the source code of functions. They are widely used in modern development for solving problems of logging, caching, authorization, performance measurement, and many others.
Proper use of decorators makes the code cleaner, more modular, and reusable. By mastering the principles of creating and applying decorators, the developer gains access to elegant solutions for many everyday programming tasks.
Frequently Asked Questions
- What is a decorator in Python? A decorator is a function that takes another function as an argument and returns a modified version of that function, extending or changing its behavior.
- How does the
@symbol work in Python? The@symbol represents syntactic sugar for applying a decorator. The@decoratornotation is equivalent tofunc = decorator(func). - Can I pass arguments to a decorator? Yes, to do this, a decorator factory is created — a function that takes arguments and returns a decorator:
@decorator(arg). - Why do I need
@functools.wraps? The@functools.wrapsdecorator preserves the name, documentation, and other attributes of the original function, preventing them from being lost during decoration. - Do decorators belong to functional programming? Yes, decorators are part of the functional programming style in Python, as they work with functions as first-class objects.
The Future of AI in Mathematics and Everyday Life: How Intelligent Agents Are Already Changing the Game
Experts warned about the risks of fake charity with AI
In Russia, universal AI-agent for robots and industrial processes was developed