Decorators offer a wide range of functionality in Python. It's the possibility to extend a function, method or a class dynamically. Here some examples where it can be used: performance measurement, singleton, unit testing, ...
The next code example does print out every call for any decorated function or method with any arguments and return value. The basic flow is that when calling a function like foo you are calling the decorator function first and those one is reponsible for delegating the arguments to the real function and to provide the return value of the real function. Additionally the decorator can do the job what it is designed for; here its task is to log the call details which are the name of the function (or method) and its arguments.
In [18]:
import logging
def log_call(function):
""" the function to decorate. """
def decorator(*args, **kwargs):
"""
Decorator that excepts any arguments, returns the value of the decorated function.
Before calling the real function all details of the call are logged.
"""
logging.info("calling %s with %s and %s" % (function.__name__, args, kwargs))
return function(*args, **kwargs)
return decorator
@log_call
def foo(*args, **kwargs): pass # a function that does nothing
@log_call
def bar(): return 42 # a function that does return a value
logger = logging.getLogger()
logger.setLevel(logging.INFO)
foo()
foo("hello world", 1024, 3.1415926535, author="Agatha Christie")
print("Return value of bar() is %s" % bar())
The previous decorator is not flexible because it does always use the same logging function. If you want to use different log functions for different functions or methods then you need to provide parameters for the decorator. The next code allows defining the log function and we use the logging.info as default. Writing a decorator works like this:
From use perspective you have to notice that you now always have to use the decorator like a call with ().
In [43]:
def configurable_log_call(log_function=logging.info):
def decorator_function(function):
def decorator_arguments(*args, **kwargs):
log_function("calling %s with %s and %s" % (function.__name__, args, kwargs))
return function(*args, **kwargs)
return decorator_arguments
return decorator_function
@configurable_log_call(logging.debug)
def foo(*args, **kwargs): pass # a function that does nothing
@configurable_log_call()
def bar(): return 42 # a function that does return a value
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
foo()
foo("hello world", 1024, 3.1415926535, author="Agatha Christie")
print("Return value of bar() is %s" % bar())
The cache in this example is defined for a function that takes one argument and provide a value for it. Because the calculation might be expensive we store the result in a cache. Additionally we combine the cache with a logging which demonstrates that the final function is not called twice for the same value.
In [45]:
import math
def cache(function):
CACHE = {}
def decorator(value):
try: return CACHE[value]
except:
result = function(value)
CACHE[value] = result
return result
return decorator
@cache
@configurable_log_call()
def is_prime(n):
if n < 2: return False
if n % 2 == 0: return n == 2
limit = int(math.sqrt(n))
for d in xrange(3, limit+1, 2):
if n % d == 0: return False
return True
print([n for n in range(20+1) if is_prime(n)])
print([n for n in range(20+1) if is_prime(n)])
print([n for n in range(20+1) if is_prime(n)])
You also can decorate a class. Using the last logging decorator at a class itself it would log the c'tor only and you would have to add the decorator before each individual method. It can be done easier demonstrated by the next decorator which detects whether the decorated object is a class or a function; when the object is a class we iterate over all callable attributes and decorate them individually.
In [67]:
import types
def print_call():
def decorator_function(instance):
def decorator_arguments(*args, **kwargs):
print(" ... calling %s with %s and %s" % (instance.__name__, args, kwargs))
return instance(*args, **kwargs)
if isinstance(instance, (types.TypeType, types.ClassType)):
for attr in instance.__dict__:
if callable(getattr(instance, attr)):
setattr(instance, attr, decorator_function(getattr(instance, attr)))
return decorator_arguments
return decorator_function
@print_call()
class Foo(object):
def test1(self): print("Foo.test1 called")
def test2(self): print("Foo.test2 called")
foo = Foo()
foo.test1()
foo.test2()
In [ ]: