Decorator can add additional functionality to existing functions. It takes a function and returns a new function:
def decor(F):
def new_F(a, b):
print("input", a, b)
return F(a, b)
return new_F
So the decorator decor
adds a print()
before the execution of the original function F
. To apply decoration, we can:
@decor
def sum(a, b):
return a + b
So now the function sum()
will print out the input. It passes the original sum()
function to the decorator and assign the return function to the name sum
.
However, the function metadata is lost by applying the decorator. For example, after applying @decor
, the function metadata becomes:
sum.__name__ # 'new_F'
countdown.__doc__ # None
countdown.__annotations__ # {}
Whenever you define a decorator, you should always remember to apply the @wraps
decorator from the functools
library to the underlying wrapper function.
from functools import wraps
def decor(F):
@wraps(F)
def new_F(a, b):
print("input", a, b)
return F(a, b)
return new_F
Work with Function Signatures
A common practice of representing a function with arbitrary signature is using *args, **kwargs
as parameters. For example, a time decorator that times the execution of a function:
import time
from functools import wraps
def timethis(func):
'''
Decorator that reports the 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
Decorators that Take Arguments
from functools import wraps
import logging
def logged(level, name=None, message=None):
'''
Add logging to a function. level is the logging
level, name is the logger name, and message is the
log message. If name and message aren't specified,
they default to the function's module and name.
'''
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
# Example use
@logged(logging.DEBUG)
def add(x, y):
return x + y
@logged(logging.CRITICAL, 'example')
def spam():
print('Spam!')
The three nested function works as follows. The outermost function logged()
accepts the desired arguments and simply makes them available to the inner functions of the decorator. The inner function decorate()
accepts a function and puts a wrapper around it as normal. The key part is that the wrapper is allowed to use the arguments passed to logged()
.
The decoration process evaluates as logged(logging.DEBUG)(x, y)
. The result of logged(logging.DEBUG)
must be a callable which, in turn, takes a function as input and wraps it.
Decorators Inside Classes
Defining a decorator inside a class is straightforward, but you first need to sort out the manner in which the decorator will be applied. Specifically, whether it is applied as an instance or a class method.
from functools import wraps
class A:
# Decorator as an instance method
def decorator1(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
print('Decorator 1')
return func(*args, **kwargs)
return wrapper
# Decorator as a class method
@classmethod
def decorator2(cls, func):
@wraps(func)
def wrapper(*args, **kwargs):
print('Decorator 2')
return func(*args, **kwargs)
return wrapper
A common confusion when writing decorators in classes is getting tripped up by the proper use of the extra self
or cls
arguments in the decorator code itself. Although the outermost decorator function, such as decorator1()
or decorator2()
, needs to provide a self
or cls
argument (since they’re part of a class), the wrapper function created inside doesn’t generally need to include an extra argument. This is why the wrapper()
function created in both decorators doesn’t include a self argument.