Decorators appeared in Python as syntactic sugar starting from version 2.4 to facilitate working with higher-order functions—those that take or return other functions. The evolution of approaches to extending the functionality of functions led to a format of concise and expressive means—annotations via @decorator.
In large projects, it is often necessary to modify or wrap functions with some additional logic: logging, access checking, caching, measuring execution time. Without decorators, it was necessary to manually call wrapper functions, which bloated the code.
Decorators allow for extracting repeated aspects into separate wrappers, thereby increasing the readability and reusability of code. Using @decorator, functionality can be elegantly added to functions and methods:
Example code:
import time def timing_decorator(func): def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) print(f'Elapsed: {time.time() - start:.3f}s') return result return wrapper @timing_decorator def slow_function(): time.sleep(0.5) slow_function() # Will output how long it took to execute
Key features:
Can decorators change the signature of the wrapped function?
It is often erroneously believed that the signature of a function is not changed by a decorator. In fact, without using the functools.wraps module, the metadata disappears, which can lead to unexpected errors in auto-documentation or introspection.
Example code:
from functools import wraps def decorator(func): @wraps(func) def wrapper(*args): return func(*args) return wrapper
Can decorators be parameterized?
It is often answered that they cannot. In fact, it is possible to create a parameterized decorator through an additional level of nesting—a function that returns a decorator.
Example code:
def repeat(n): def decorator(func): def wrapper(*args, **kwargs): result = None for _ in range(n): result = func(*args, **kwargs) return result return wrapper return decorator @repeat(3) def hello(): print("Hello!")
Can multiple decorators be applied to one function? What will be the execution order?
It is often mistakenly believed that the order is unimportant or that it matches the order of placement of decorators in code. In fact, the lowest decorator is applied first, then the next one, and so on upwards.
Example code:
def dec1(f): def wrapper(*a, **k): print("dec1") return f(*a, **k) return wrapper def dec2(f): def wrapper(*a, **k): print("dec2") return f(*a, **k) return wrapper @dec1 @dec2 def f(): print("core") f() # dec1, dec2, core
Timing logging was added to 10 functions manually, by copy-pasting code.
Pros:
Cons:
All timing logic is extracted into a decorator; all functions needing this metric are wrapped with the @timing_decorator.
Pros:
Cons:
functools.wraps, if not careful;