The decorator story

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, ...

Simple decorator (logging of calls)

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())


INFO:root:calling foo with () and {}
INFO:root:calling foo with ('hello world', 1024, 3.1415926535) and {'author': 'Agatha Christie'}
INFO:root:calling bar with () and {}
Return value of bar() is 42

Configurable decorator (logging calls)

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:

  • define a function that takes the decorator parameters
  • inside define the function decorator that takes the function to decorate and return it
  • inside of the function decorator define the decorator of the function parameters and return it.
  • also the inner function usually implements the decorator logic like here: logging of the call.

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())


DEBUG:root:calling foo with () and {}
DEBUG:root:calling foo with ('hello world', 1024, 3.1415926535) and {'author': 'Agatha Christie'}
INFO:root:calling bar with () and {}
Return value of bar() is 42

Cache and combined decorators

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)])


INFO:root:calling is_prime with (0,) and {}
INFO:root:calling is_prime with (1,) and {}
INFO:root:calling is_prime with (2,) and {}
INFO:root:calling is_prime with (3,) and {}
INFO:root:calling is_prime with (4,) and {}
INFO:root:calling is_prime with (5,) and {}
INFO:root:calling is_prime with (6,) and {}
INFO:root:calling is_prime with (7,) and {}
INFO:root:calling is_prime with (8,) and {}
INFO:root:calling is_prime with (9,) and {}
INFO:root:calling is_prime with (10,) and {}
INFO:root:calling is_prime with (11,) and {}
INFO:root:calling is_prime with (12,) and {}
INFO:root:calling is_prime with (13,) and {}
INFO:root:calling is_prime with (14,) and {}
INFO:root:calling is_prime with (15,) and {}
INFO:root:calling is_prime with (16,) and {}
INFO:root:calling is_prime with (17,) and {}
INFO:root:calling is_prime with (18,) and {}
INFO:root:calling is_prime with (19,) and {}
INFO:root:calling is_prime with (20,) and {}
[2, 3, 5, 7, 11, 13, 17, 19]
[2, 3, 5, 7, 11, 13, 17, 19]
[2, 3, 5, 7, 11, 13, 17, 19]

Decorating all methods of a class at once

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()


 ... calling Foo with () and {}
 ... calling test1 with (<__main__.Foo object at 0x7fc392116bd0>,) and {}
Foo.test1 called
 ... calling test2 with (<__main__.Foo object at 0x7fc392116bd0>,) and {}
Foo.test2 called

In [ ]: