Table of Contents
- Introduction
- What are Decorators?
- Why Use Decorators?
- First-Class Functions in Python
- How to Write a Simple Decorator from Scratch
- Using
@
Syntax for Decorators - Handling Arguments with Decorators
- Returning Values from Decorators
- Preserving Metadata with
functools.wraps
- Practical Use Cases for Decorators
- Common Mistakes and How to Avoid Them
- Conclusion
Introduction
Decorators are a cornerstone of advanced Python programming. They provide a clean and powerful way to modify or extend the behavior of functions or classes without changing their code directly. Decorators are widely used in frameworks like Flask, Django, and many others for tasks such as authentication, logging, performance measurement, and more.
In this article, we’ll build decorators from scratch, understand their inner workings, and explore practical use cases to solidify your understanding.
What are Decorators?
In Python, a decorator is simply a function that takes another function as input and returns a new function that enhances or modifies the behavior of the original function.
In essence:
- Input: A function.
- Output: A new function.
Why Use Decorators?
- Code Reusability: Apply common functionality (like logging, timing, or checking permissions) across multiple functions.
- Separation of Concerns: Keep core logic separate from auxiliary functionality.
- DRY Principle: Avoid code repetition.
- Enhance Readability: Cleanly attach behavior to functions.
First-Class Functions in Python
In Python, functions are first-class objects, which means:
- You can assign them to variables.
- Pass them as arguments.
- Return them from other functions.
This flexibility is what makes decorators possible.
Example:
def greet(name):
return f"Hello, {name}"
say_hello = greet
print(say_hello("Alice"))
How to Write a Simple Decorator from Scratch
Let’s create a basic decorator that prints a message before and after calling a function.
def simple_decorator(func):
def wrapper():
print("Before calling the function.")
func()
print("After calling the function.")
return wrapper
def say_hello():
print("Hello!")
# Decorating manually
decorated_function = simple_decorator(say_hello)
decorated_function()
Output:
Before calling the function.
Hello!
After calling the function.
Using @
Syntax for Decorators
Instead of manually decorating, Python offers a cleaner syntax using @decorator_name
:
@simple_decorator
def say_hello():
print("Hello!")
say_hello()
When Python sees the @simple_decorator
, it is equivalent to:
say_hello = simple_decorator(say_hello)
Handling Arguments with Decorators
Most real-world functions accept arguments. To handle this, modify the wrapper to accept *args
and **kwargs
.
def decorator_with_args(func):
def wrapper(*args, **kwargs):
print(f"Arguments received: args={args}, kwargs={kwargs}")
return func(*args, **kwargs)
return wrapper
@decorator_with_args
def greet(name, age=None):
print(f"Hello, {name}! Age: {age}")
greet("Bob", age=30)
Output:
Arguments received: args=('Bob',), kwargs={'age': 30}
Hello, Bob! Age: 30
Returning Values from Decorators
If your decorated function returns something, ensure your wrapper also returns that value.
def decorator_with_return(func):
def wrapper(*args, **kwargs):
print("Function is being called.")
result = func(*args, **kwargs)
print("Function call finished.")
return result
return wrapper
@decorator_with_return
def add(a, b):
return a + b
result = add(3, 4)
print(f"Result: {result}")
Output:
Function is being called.
Function call finished.
Result: 7
Preserving Metadata with functools.wraps
When you decorate a function, it loses important metadata like its name and docstring unless you explicitly preserve it.
Python provides the functools.wraps
decorator to help with this.
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
"""Wrapper function"""
return func(*args, **kwargs)
return wrapper
@my_decorator
def original_function():
"""This is the original function."""
print("Original function called.")
print(original_function.__name__) # original_function
print(original_function.__doc__) # This is the original function.
Without @wraps
, __name__
and __doc__
would point to the wrapper, not the original function.
Practical Use Cases for Decorators
1. Logging
Automatically log every time a function is called.
def logger(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Logging: {func.__name__} was called with args={args}, kwargs={kwargs}")
return func(*args, **kwargs)
return wrapper
@logger
def process_data(data):
print(f"Processing {data}")
process_data("data.txt")
2. Authentication Check
Check user authentication before allowing function execution.
def requires_authentication(func):
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
if not user.get('authenticated'):
raise PermissionError("User not authenticated!")
return func(user, *args, **kwargs)
return wrapper
@requires_authentication
def view_account(user):
print(f"Access granted to {user['name']}.")
user = {'name': 'John', 'authenticated': True}
view_account(user)
3. Timing Function Execution
Measure how long a function takes to run.
import time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.4f} seconds.")
return result
return wrapper
@timer
def slow_function():
time.sleep(2)
slow_function()
Common Mistakes and How to Avoid Them
Mistake | How to Avoid |
---|---|
Forgetting to use functools.wraps | Always decorate the wrapper with @functools.wraps(func) . |
Not returning the result of func | Ensure the wrapper returns func(*args, **kwargs) . |
Mismanaging arguments | Always use *args and **kwargs in the wrapper unless you have a specific reason. |
Conclusion
Decorators are a fundamental part of writing clean, scalable, and Pythonic code. They allow you to abstract repetitive logic, manage cross-cutting concerns like logging and authentication, and keep your core codebase elegant.
By learning to build decorators from scratch, you gain deep insight into how Python handles functions, closures, and execution flow. The more you practice, the more natural decorators will feel.
Keep experimenting by creating your own decorators for caching, validation, error handling, and beyond!