Python decorators offer a robust syntactic tool for modifying the behavior of functions or methods. By wrapping a target function, developers can inject auxiliary logic—such as logging, timing, or validation—without altering the original function's source code or its invocation signature. This technique relies heavily on nested functions and closures, enabling a clean separation of concerns and significantly enhancing code reusability.
The Mechanics of Closure and Nesting
At its core, a decorator is a function that accepts another function as an argument and returns a replacement function, usually a wrapper. To understand this, one must grasp the concept of closures in Python. A closure occurs when a nested function remembers and accesses variables from its enclosing scope even after the outer function has finished execution.
Consider the following example of a nested function structure that forms the basis of a decorator:
def outer_scope(external_val):
# The value 'external_val' is enclosed within this scope
def inner_scope(local_val):
print(f"Sum: {external_val + local_val}")
return inner_scope
# Returning the inner function object, not the result
my_func = outer_scope(10)
my_func(5) # Output: Sum: 15
When applied to decorators, the external_val becomes the original function being decorated, and inner_scope becomes the wrapper that adds new logic before or after calling the original function.
Implementing a Basic Decorator
A common use case is measuring the execution time of a routine. Instead of manually adding timing logic to every function, a generic decorator can be applied.
import time
def profile_performance(target_func):
def wrapper():
start_tick = time.perf_counter()
target_func()
end_tick = time.perf_counter()
duration = end_tick - start_tick
print(f"Execution time: {duration:.4f} seconds")
return wrapper
@profile_performance
def process_data():
time.sleep(1)
print("Data processed.")
if __name__ == "__main__":
process_data()
In this scenario, process_data is passed to profile_performance. The wrapper function executes the timing logic around the original target_func.
Handling Arguments with Generic Decorators
The previous example fails if the decorated function accepts arguments. To create a universal decorator capable of wrapping functions with any signature, variadic arguments (*args and **kwargs) must be used.
def log_execution(target_func):
def wrapper(*pos_args, **kw_args):
print(f"Running {target_func.__name__}")
return target_func(*pos_args, **kw_args)
return wrapper
@log_execution
def calculate_sum(a, b):
return a + b
result = calculate_sum(10, 20)
print(f"Result: {result}")
Preserving Metadata with functools.wraps
When a function is decorated, its metadata (such as __name__ and __doc__) is overwritten by the wrapper function. This can break introspection tools that rely on the original function's identity. The functools module provides the wraps utility to copy metadata from the original function to the wrapper.
from functools import wraps
def log_execution(target_func):
@wraps(target_func)
def wrapper(*pos_args, **kw_args):
print(f"Running {target_func.__name__}")
return target_func(*pos_args, **kw_args)
return wrapper
@log_execution
def legacy_function():
"""This is a legacy function."""
pass
print(legacy_function.__name__) # Output: legacy_function
print(legacy_function.__doc__) # Output: This is a legacy function.
Advanced Decorators with Arguments
Sometimes the decorator itself requires parameters to configure its behavior. This requires an additional level of nesting: a function that accepts configuration parameters and returns the actual decorator.
from functools import wraps
def repeat_execution(times=1):
def actual_decorator(target_func):
@wraps(target_func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = target_func(*args, **kwargs)
return result
return wrapper
return actual_decorator
@repeat_execution(times=3)
def greet():
print("Hello!")
greet()
# Output:
# Hello!
# Hello!
# Hello!
This pattern allows for highly customizable behavior modification, passing runtime configuration directly to the decorator syntax.