A decorator is a function that takes a function as input and returns another function, adding functionalities or replacing the entire fuction itself.
It is applied when the function is defined.
from functools import wraps
def timethis(func):
''' this decorator reports execution time '''
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(func.__name__, end -start)
return result
return wrapper
According to the official documentation, @functools.wraps
is a convenience function for invoking update_wrapper()
as a function decorator when defining a wrapper function.
If you forget to use @wraps
, the decorated function will lose its name, docstring, annotations and calling signature.
Using @wraps
, we can inject code to be executed alongside a given function, without losing function metadata.
In the given example, we start counting time before executing the original function, then stopping when it returns and print the time interval.
from time import sleep
@timethis
def timer(seconds):
''' creat a timer'''
time.sleep(seconds)
Defining a more useful decorator taking arguments (mandatory or optional)
Let's code a logging decorator to our timer function
import logging
def logged(level, name = None, message = None):
''' add logging '''
def decorate(func):
logname = name if name else func.__module__
log = logging.getLogger(logname)
logmsg = message if message else func.__name__
@wraps(func)
def wrapper(*args,**kwargs):
log.log(level, logmsg)
return func(*args, **kwargs)
return wrapper
return decorate
Decorating the function
@logged(logging.CRITICAL, 'example')
def timer(seconds):
sleep(seconds)
In this example, we have a decorator taking our custom arguments and then returning a decorator. This decorator then is applied to the function we want to decorate.
def logged(func = None, *, level = logging.DEBUG, name = None, message = None):
''' add logging '''
if func is None:
return partial(logged, level=level, name=name, message=message)
def decorate(func):
logname = name if name else func.__module__
log = logging.getLogger(logname)
logmsg = message if message else func.__name__
@wraps(func)
def wrapper(*args,**kwargs):
log.log(level, logmsg)
return func(*args, **kwargs)
return wrapper
return decorate
To create a decorator that takes no arguments, and avoid the calling conventions between decorators without any arguments and decorators with arguments, we use partial to return a new partial object which when called behaves like func called with the positional arguments args and keyword.
Can a decorator be defined as a class?
Also, decorators can be defined as classes and can be applied to class and static methods.
In fact, a decorator can be defined as a class, like @property, with a get, a set and a del method.
There are a lot more about decorators.
To really understand how decorators work you need to understand the variable scoping, closures, mtaclasses and descriptors.
I strong recommend you to read Fluent Python and Python Cookbook.
Top comments (0)