Python Decorator Usage and Best Practices
How to Use Function Decorators
A common implementation of Fibonacci:
def fibonacci(n):
if n <= 1:
return 1
return fibonacci(n - 1) + fibonacci(n - 2)
This approach, typical of recursion in C, leads to massive redundant calculations. For example, computing fibonacci(10) requires fibonacci(8) and fibonacci(9), and computing fibonacci(9) requires fibonacci(7) and fibonacci(8).
An improved version with caching:
def fibonacci(n, cache=None):
if cache is None:
cache = {}
if n in cache:
return cache[n]
if n <= 1:
return 1
cache[n] = fibonacci(n - 1, cache) + fibonacci(n - 2, cache)
return cache[n]
if __name__ == '__main__':
print(fibonacci(50))
The result is computed instantly.
Using a decorator:
def memo(func):
cache = {}
def wrap(n):
if n not in cache:
cache[n] = func(n)
return cache[n]
return wrap
@memo
def fibonacci(n):
if n <= 1:
return 1
return fibonacci(n - 1) + fibonacci(n - 2)
if __name__ == '__main__':
print(fibonacci(50))
How to Preserve Function Metadata After Decoration
Functions store metadata such as:
f.__name__: the function namef.__doc__: the docstringf.__module__: the module namef.__dict__: attribute dictionaryf.__defaults__: default parameter tuple
After applying a decorator, accessing these attributes reveals the wrapper function's metadata, causing the original function's metadata to be lost.
Solution:
- Use
update_wrapper - Use
wraps
from functools import wraps
def mydecorator(func):
@wraps(func)
def wrapper(*args, **kargs):
"""wrapper function"""
print('In wrapper')
func(*args, **kargs)
return wrapper
@mydecorator
def example():
"""example function"""
print('In example')
if __name__ == '__main__':
example()
print(example.__name__)
How to Define Decorators with Arguments
Practical Case:
- Create a decorator that validates the types of the decorated function's parameters.
- The decorator accepts arguments specifying parameter types. If a mismatch is detected at call time, an expection is raised.
@type_assert(str, int, int)<br></br>def f(a, b, c):<br></br> ...<br></br><br></br>@type_assert(y=list)<br></br>def g(x, y):<br></br> ...
Solution:
- Extract the function signature:
inspect.signature() - A decorator with arguments is essentially a factory that produces a specific decorator. Each call to
type_assertreturns a custom decorator to apply to other functions.
import inspect
def type_assert(*ty_args, **ty_kwargs):
def decorator(func):
func_sig = inspect.signature(func)
bind_type = func_sig.bind_partial(*ty_args, **ty_kwargs).arguments
def wrap(*args, **kwargs):
for name, obj in func_sig.bind(*args, **kwargs).arguments.items():
type_ = bind_type.get(name)
if type_:
if not isinstance(obj, type_):
raise TypeError('%s must be %s' % (name, type_))
return func(*args, **kwargs)
return wrap
return decorator
@type_assert(c=str)
def f(a, b, c):
pass
if __name__ == '__main__':
f(5, 10, 's') # passes validation
f(5, 10, 1) # fails validation: 1 is not a string
How to Create a Decorator with Mutable Attributes
Practical Case:
In a project with performance issues, implement a decorator with a timeout parameter to analyze function execution times:
- Log the execution time of each function call.
- If the time exceeds the
timeoutvalue, log the call details. - The
timeoutvalue should be modifiable at runtime.
@warn_timeout(1.5)<br></br>def func(a, b):<br></br> ...
Solution:
- Add a function to the wrapper to modify the free variable used in the closure. In Python 3, use
nonlocalto reference variables in the enclosing scope.
import time
import logging
def warn_timeout(timeout):
def decorator(func):
def wrap(*args, **kwargs):
t0 = time.time()
res = func(*args, **kwargs)
used = time.time() - t0
if used > timeout:
logging.warning('%s: %s > %s', func.__name__, used, timeout)
return res
def set_timeout(new_timeout):
nonlocal timeout
timeout = new_timeout
wrap.set_timeout = set_timeout
return wrap
return decorator
import random
@warn_timeout(1.5)
def f(i):
print('in f [%s]' % i)
while random.randint(0, 1):
time.sleep(0.6)
for i in range(3):
f(i)
f.set_timeout(1)
for i in range(3):
f(i)
How to Define Decorators Within a Class
Practical Case:
- Implement a decorator that logs function call details to a file.
- Record the call time, execution time, and call count for each function.
- Group decorated functions to log to different files.
- Allow dynamic modification of parameters, such as log format.
- Enable toggling log output on and off.
Solution:
- Use an instence method of a class as a decorator. The wraper function can then hold a reference to the instance, making it easier to modify attributes and extend functionality.
import time
import logging
DEFAULT_FORMAT = '%(func_name)s -> %(call_time)s\t%(used_time)s\t%(call_n)s'
class CallInfo:
def __init__(self, log_path, format_=DEFAULT_FORMAT, on_off=True):
self.log = logging.getLogger(log_path)
self.log.addHandler(logging.FileHandler(log_path))
self.log.setLevel(logging.INFO)
self.format = format_
self.is_on = on_off
def info(self, func):
_call_n = 0
def wrap(*args, **kwargs):
func_name = func.__name__
call_time = time.strftime('%x %X', time.localtime())
t0 = time.time()
res = func(*args, **kwargs)
used_time = time.time() - t0
nonlocal _call_n
_call_n += 1
call_n = _call_n
if self.is_on:
self.log.info(self.format % locals())
return res
return wrap
def set_format(self, format_):
self.format = format_
def turn_on_off(self, on_off):
self.is_on = on_off
import random
ci1 = CallInfo('mylog1.log')
ci2 = CallInfo('mylog2.log')
@ci1.info
def f():
sleep_time = random.randint(0, 6) * 0.1
time.sleep(sleep_time)
@ci1.info
def g():
sleep_time = random.randint(0, 8) * 0.1
time.sleep(sleep_time)
@ci2.info
def h():
sleep_time = random.randint(0, 7) * 0.1
time.sleep(sleep_time)
for _ in range(3):
random.choice([f, g, h])()
ci1.set_format('%(func_name)s -> %(call_time)s\t%(call_n)s')
for _ in range(3):
random.choice([f, g])()