Decorators in Python are a powerful feature that allows you to modify or enhance functions or methods in a clean and readable way. They are built upon fundamental Python concepts like namespaces, nested functions, and closures.
A decorator essentially acts as a callable tool that wraps another function or method. Its core principle is adherence to the Open/Closed Principle: it should be open for extension but closed for modification. This means you can add new functionality to existing code without altering its original source.
Let's explore this with an example.
Imagine a function that simulates a game event:
import time
def game_event(faction, delay):
print('Welcome to the arena!')
print(f'Your faction: {faction}')
print(f'Enemy arrives in {delay} seconds')
time.sleep(delay)
print('All units deployed!')
game_event('Red', 30)
If we want to measure the execution time of game_event without changing its source code, we could manually add timing logic:
import time
def game_event(faction, delay):
start_time = time.time()
print('Welcome to the arena!')
print(f'Your faction: {faction}')
print(f'Enemy arrives in {delay} seconds')
time.sleep(delay)
print('All units deployed!')
end_time = time.time()
print(f'Execution time: {end_time - start_time:.4f} seconds')
game_event('Red', 30)
This approach modifeis the original function's source, which is not ideal. Alternatively, we could call the timing logic separately, but this also changes how game_event is invoked:
import time
def game_event(faction, delay):
print('Welcome to the arena!')
print(f'Your faction: {faction}')
print(f'Enemy arrives in {delay} seconds')
time.sleep(delay)
print('All units deployed!')
def execute_with_timing(func, *args, **kwargs):
start_time = time.time()
func(*args, **kwargs)
end_time = time.time()
print(f'Execution time: {end_time - start_time:.4f} seconds')
execute_with_timing(game_event, 'Blue', 30)
This still requires a wrapper function and modifies the call site. A true decorator allows us to apply this timing functionality more elegant.
Consider a decorator functon log_execution_time:
import time
def log_execution_time(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs) # Call the original function
end_time = time.time()
print(f'{func.__name__} executed in {end_time - start_time:.4f} seconds')
return result
return wrapper
@log_execution_time
def game_event(faction, delay):
print('Welcome to the arena!')
print(f'Your faction: {faction}')
print(f'Enemy arrives in {delay} seconds')
time.sleep(delay)
print('All units deployed!')
game_event('Red', 10)
The @log_execution_time syntax is Python's syntactic sugar for applying the decorator. It is equivalent to game_event = log_execution_time(game_event) after the function definition.
Decorators can also accept arguments. For instance, a decorator to simulate battery charging:
import time
import functools
def simulate_charging(min_charge):
def decorator(func):
@functools.wraps(func) # Preserves original function metadata
def wrapper(*args, **kwargs):
print(f'Starting charge from {min_charge}%...')
for i in range(min_charge, 101):
time.sleep(0.02)
print(f'\rCharging: {i}%', end='')
print('\nCharge complete.')
return func(*args, **kwargs)
return wrapper
return decorator
@simulate_charging(50)
def power_up(level):
print(f'Power level at {level} established.')
power_up(99)
This example demonstrates a decorator factory (simulate_charging) that takes arguments. functools.wraps is crucial here to preserve the original function's name, docstring, and other metadata, which is important for introspection and debugging.
Decorators are often used for cross-cutting concerns like logging, access control, instrumentation, and timing, promoting modular and reusable code.