.sweyla.com

Programming and coding

Python: Decorators

This is a guide to Python decorators, which is when you have the following syntax:

@ThisIsADecorator
class MyClass(object):
    pass

The @ sign is just syntactic candy and the above is equivalent to:

class MyClass(object):
    pass
MyClass = ThisIsADecorator(MyClass)

Which means MyClass runs through the callable object ThisIsADecorator. There are plenty of things this can be used for, but let's start looking at a simple example (notice that decorators can operate on both functions and classes).

def debug(f):
    def newf(*args, **kwargs):
        c = f(*args, **kwargs)
        print "Function returned %s" % c
        return c
    return newf
    
@debug
def test(x,y): 
    return x + y

test(10, 20)

Output

Function returned 30

In this example, a new function newf (a closure, to be exact) is created within debug, all the arguments are tunneled to make it work exactly like the original function. Then newf is returned instead of f, and every time we call the function test, we will see what the value returned.

Decorators with arguments

The next example shows how to add arguments to the decorator, let's first take a look at what this code will translate as:

@debug(arg)
def function():
    pass

Is equivalent to:

def function():
    pass
function = debug(arg)(function)

This can be achieved with another level of nested functions, but many prefer making debug a class that will first construct using one argument, then making the whole object callable, which is achieved by overriding the function __call__:

class debug(object):
    def __init__(self, arg):
        self.arg = arg

    def __call__(self, f):
        def newf(*args, **kwargs):
            c = f(*args, **kwargs)
            print "Function returned %s and used argument %s" % (c, self.arg)
            return c
        return newf

@debug(6)
def test(x, y):
    return x + y

test(10, 20)

Output

Function returned 30 and used argument 6

Now, the argument wasn't used for much, but it was there and you can do whatever you want with it. An example is to pass the file descriptor so that you can specify where to write the debug.

Several decorators

It's possible to apply more than one decorator. Adding two @debug in the above example is equivalent of running them through debug afterwards, but in the opposite order.

@debug(1)
@debug(2)
def test(x, y):
    return x + y

test(10, 20)

Output

Function returned 30 and used argument 1
Function returned 30 and used argument 2

Adding arguments to a function

The following example shows how to inject an argument into the function, of course this could cause obfuscated code, but I'll leave those decisions up to you.

def debuggable(f):
    def newf(*args, **kwargs):
        debugging = False
        if 'debug' in kwargs and kwargs['debug'] == True:
            del kwargs['debug']
            debugging = True
        c = f(*args, **kwargs)
        if debugging:
            print "Function returned %s" % c
        return c
    return newf
    
@debuggable
def test(x,y): 
    return x + y

test(100, 200)
test(10, 20, debug=True)

Output

Function returned 30

What we did was to check whether debug was in kwargs (which gives the keyword arguments as a dictionary) before calling the original function, then removing it so it won't complain about an unknown argument.

Changing back the meta data

Maybe you have noticed that the meta name of your class or function changes to newc or newf, or whatever you called it. This can be avoided by copying over the meta data from the original class, luckily there is already a decorator built into Python which does this.

from functools import wraps

def decorator(f):
    @wraps(f)
    def newf(*args, **kwargs):
        return f(*args, **kwargs)
    return newf

@decorator
def Test(): pass

print Test.__name__

Output

Test

More information

That was a quick introduction to Python decorators. More examples can be found at python.org. If you are using Python 3 and tried copy-pasting the examples, please note that you will have to change your print statements to conform to the new syntax.