Python FAQ: Top Questions
51. What are Python decorators? How do you create and use them?
Python decorators are a powerful and elegant way to modify or enhance functions or methods without permanently changing their actual code. They essentially "wrap" another function, allowing you to execute code before, after, or even around the wrapped function's execution. Decorators are often used for tasks like logging, access control, timing, caching, and more.
Syntactically, a decorator is applied using the
@decorator_name
syntax placed directly above the function definition you want to decorate.
How Decorators Work:
At its core, a decorator is just a function that takes another function as an argument, adds some functionality, and returns a new function (or modifies the original one).
When you write:
@my_decorator
def my_function():
pass
This is just syntactic sugar for:
def my_function():
pass
my_function = my_decorator(my_function)
So,
my_decorator
is called with
my_function
as its argument, and whatever
my_decorator
returns
replaces
the original
my_function
.
Creating Decorators:
Decorators can be created as either functions or classes.
1. Function-Based Decorators:
These are the most common. A function-based decorator typically follows this pattern:
import functools
def my_decorator(func):
@functools.wraps(func) # Important for preserving original function metadata
def wrapper(*args, **kwargs):
# Code to run BEFORE the decorated function
print(f"Calling function: {func.__name__}")
result = func(*args, **kwargs) # Call the original function
# Code to run AFTER the decorated function
print(f"Function {func.__name__} finished.")
return result
return wrapper # Return the new wrapper function
-
my_decorator
takesfunc
(the function to be decorated) as an argument. -
It defines an inner function,
wrapper
, which is what will actually be called when the decorated function is invoked. -
wrapper
contains the extra logic (e.g., printing messages) and calls the originalfunc
. -
functools.wraps(func)
is crucial. It copies the__name__
,__doc__
, and other attributes from the original function (func
) to thewrapper
function. Without it, debugging would be harder as the decorated function would appear to be namedwrapper
. -
my_decorator
returns thiswrapper
function.
2. Class-Based Decorators:
A class can also act as a decorator if it implements the
__init__
and
__call__
special methods.
-
__init__
receives the function to be decorated. -
__call__
defines what happens when the decorated function (which is now an instance of the decorator class) is invoked.
import functools
class MyDecoratorClass:
def __init__(self, func):
self.func = func
functools.wraps(func)(self) # Preserve metadata
print(f"MyDecoratorClass initialized for function: {func.__name__}")
def __call__(self, *args, **kwargs):
# Code to run BEFORE the decorated function
print(f"Calling function from class decorator: {self.func.__name__}")
result = self.func(*args, **kwargs) # Call the original function
# Code to run AFTER the decorated function
print(f"Function {self.func.__name__} finished (class decorator).")
return result
Using Decorators:
Once defined, you apply them with the
@
syntax:
# Using the function-based decorator
@my_decorator
def greet(name):
return f"Hello, {name}!"
# Using the class-based decorator
@MyDecoratorClass
def farewell(name):
return f"Goodbye, {name}!"
print(greet("Alice"))
print(farewell("Bob"))
Why Use Decorators?
- Code Reusability: Apply the same functionality to multiple functions without repeating code.
- Separation of Concerns: Keep core business logic separate from auxiliary concerns (like logging, authentication).
-
Readability:
The
@
syntax is concise and clearly indicates that a function's behavior is being enhanced. - Frameworks and Libraries: Widely used in web frameworks (e.g., Flask, Django for routing, authentication), testing frameworks, and more.
Decorators are a powerful and common pattern in Python, allowing for clean, modular, and maintainable code by neatly packaging reusable code enhancements.
Example: Implementing and Using Decorators
import time
import functools
# --- 1. Function-based Decorator: Timer ---
print("--- Function-based Decorator: Timer ---")
def timer(func):
"""A decorator that measures the execution time of a function."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
result = func(*args, **kwargs)
end_time = time.perf_counter()
run_time = end_time - start_time
print(f"'{func.__name__}' took {run_time:.4f} seconds to execute.")
return result
return wrapper
@timer
def long_running_task(n):
"""Performs a calculation that takes some time."""
total = 0
for i in range(n):
total += i * i
return total
@timer
def simulate_api_call(delay):
"""Simulates a network API call with a given delay."""
time.sleep(delay)
return "API call completed"
print(f"Result of long_running_task: {long_running_task(1_000_000)}")
print(f"Result of simulate_api_call: {simulate_api_call(0.5)}")
# Verify metadata is preserved
print(f"Name of long_running_task: {long_running_task.__name__}")
print(f"Docstring of long_running_task: {long_running_task.__doc__}")
# --- 2. Class-based Decorator: Logger ---
print("\n--- Class-based Decorator: Logger ---")
class Logger:
"""A class-based decorator that logs function calls."""
def __init__(self, func):
self.func = func
functools.wraps(func)(self) # Essential for preserving metadata
print(f"Logger initialized for '{func.__name__}'")
def __call__(self, *args, **kwargs):
args_repr = [repr(a) for a in args]
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
signature = ", ".join(args_repr + kwargs_repr)
print(f"LOG: Calling '{self.func.__name__}' with args ({signature})")
result = self.func(*args, **kwargs)
print(f"LOG: '{self.func.__name__}' returned: {result!r}")
return result
@Logger
def add_numbers(a, b):
return a + b
@Logger
def greet_person(name, title="Mr/Ms"):
return f"Hello, {title}. {name}!"
print(f"\nResult of add_numbers: {add_numbers(10, 20)}")
print(f"Result of greet_person: {greet_person('Alice', title='Dr')}")
# Verify metadata is preserved
print(f"Name of add_numbers: {add_numbers.__name__}")
print(f"Docstring of add_numbers: {add_numbers.__doc__}")
# --- 3. Decorator with Arguments: Permissions Checker ---
print("\n--- Decorator with Arguments: Permissions Checker ---")
def requires_permission(permission_level):
"""
A decorator factory that returns a decorator to check user permissions.
This is a decorator that takes arguments, so it's a "decorator factory".
"""
def decorator(func):
@functools.wraps(func)
def wrapper(user_role, *args, **kwargs):
# Assume a simple permission check for demonstration
if user_role == permission_level or user_role == "admin":
print(f"PERMISSION CHECK: User '{user_role}' has '{permission_level}' access. Running '{func.__name__}'.")
return func(user_role, *args, **kwargs)
else:
print(f"PERMISSION DENIED: User '{user_role}' does not have '{permission_level}' access for '{func.__name__}'.")
return "ACCESS DENIED"
return wrapper
return decorator
@requires_permission("user")
def view_dashboard(user_role):
return f"Dashboard data for {user_role}."
@requires_permission("admin")
def delete_critical_data(user_role, item_id):
return f"CRITICAL: Deleting item {item_id} by {user_role}."
print(f"\nUser 'user' trying to view dashboard: {view_dashboard('user')}")
print(f"User 'guest' trying to view dashboard: {view_dashboard('guest')}")
print(f"\nUser 'user' trying to delete data: {delete_critical_data('user', 123)}")
print(f"User 'admin' trying to delete data: {delete_critical_data('admin', 456)}")
Explanation of the Example Code:
-
Function-based Decorator:
timer
-
The
timer
function takes another function (func
) as input. -
It defines a
wrapper
function inside, which wraps the originalfunc
. -
The
wrapper
calculates the execution time offunc
and prints it, then returns the result offunc
. -
@functools.wraps(func)
is used to ensure that the decorated function (long_running_task
orsimulate_api_call
) retains its original name, docstring, and other metadata, which is vital for introspection and debugging.
-
The
-
Class-based Decorator:
Logger
-
The
Logger
class is initialized with the function to be decorated (func
). -
The
__call__
method of theLogger
instance is executed whenever the decorated function is called. - It logs the arguments passed to the function before execution and the return value after execution.
-
This demonstrates how a class can act as a decorator by implementing
__init__
and__call__
. Again,functools.wraps
is important.
-
The
-
Decorator with Arguments:
requires_permission
-
Sometimes you need a decorator to take arguments (e.g.,
permission_level
). To achieve this, you create a "decorator factory" – a function (requires_permission
) that takes the arguments for the decorator and returns the actual decorator function. -
The inner
decorator
function then takes thefunc
to be decorated, and it, in turn, returns thewrapper
function. -
The
wrapper
function contains the logic to check permissions based on thepermission_level
captured from the outer scope, before deciding whether to execute the originalfunc
. This allows you to apply the same permission logic with different levels.
-
Sometimes you need a decorator to take arguments (e.g.,