Decorating with Python

I recently decided to build a miniature framework to make one of my personal projects a bit easier. In the course of doing so, I decided to use decorators to solve one of my first problem. A decorator in Python is an object that takes a function and returns a function. A callable object can be used as a decorator, so a standard Python function works just fine. More detailed information on decorators in Python can be found in PEP 318.

def exampleDecorator(func):
    print(str(func))
    return func

This decorator prints the name of the function when it decorates the function. To invoke it, use @ symbol when declaring a function.

@exampleDecorator
def double(p):
    return 2*p

The function double returns twice whatever the parameter p is. But the declaration above, tells the interpreter to decorate it using exampleDecorator. It's important to realize that functions are decorated when they are declared, not when they are invoked. Putting the two together

def exampleDecorator(func):
   print(str(func))
   return func

@exampleDecorator
def double(p):
   return 2*p

print(double(5))

This gives an output of

<function double at 0x1b7ec40>
10

In this example, I declared the decorator then declared double as a decorated function. The interpreter defines double as it normally would. Then, exampleDecorator is called and passed double as its parameter. All the example decorator does is print a string about the actual argument and return it. When double is called, it functions exactly as normal. This is because the decorator didn't actually change anything about the function. If you want to do something everytime double is invoked, you need a slightly more complex decorator. Let's define a decorator that only allows a single argument to be passed to a function, and tries to change any argument to an integer

def acceptInt(func):
    def wrapper(arg):
        arg = int(arg)
        return func(arg)
    return wrapper

At first glance this looks strange, nesting function definitions this way. The function acceptInt takes a single function func as its argument. It then declares another function that takes just one argument, tries to cast it to an integer, and passes it to func. It also returns whatever func returns. Let's decorate the double function again

@acceptInt
def double(p):
    return 2*p

print(double(5))
print(double('6'))

This gives an ouput of

10
12

The first line of output is the same as in the original example. Rightfully so, as double is declared exactly the same. The second line of output is twice the value of six. But notice when double was called, the string literal '6' was passed to it. Normally, 2*'6' would result in the string '66'. But since double was decorated with acceptInt the wrapper function declared in it converts '6' to the integer 6. Then wrapper then calls double as usual and returns the value from it.

Let's recap what we've learned so far. Decorators in Python are any object that can take a function and return a function. Decorators are attached to function by placing it directly above the function declaration and prepending the name of the decorator with '@'. Whenever the interpreter gets to the function declaration, it interprets the function as normal and then passes it to the decorator as an argument. At this point, the decorator can do anything it wants with the function including replacing it completely. This is what makes decorators powerful.

A real example

Consider the following function and decorator.

import types

def fileOpener(func):
    def wrapper(file,*args,**kwargs):
        if type(file) == types.StringType:
            with open(file) as fin:
                return func(fin,*args,**kwargs)

        return func(file,*args,**kwargs)

    return wrapper

@fileOpener
def searchFile(file,searchFor):
    for line in file.xreadlines():
        if searchFor in line:
            return line

The function searchFile iterates over each line in a file and returns the first line that matches. The arguments are a file object and the string to search for. If no lines match, it returns None. It is decorated with fileOpener. Let's search for the string 'root' in /etc/passwd. This string should almost always be present in that file.

with open('/etc/passwd') as fin:
    print(searchFile(fin,'root'))

print(searchFile('/etc/passwd','root'))

Running this gives an output of

root:x:0:0:root:/root:/bin/bash
root:x:0:0:root:/root:/bin/bash

Calling searchFile twice gives the same output in both cases. But the difference is in the second case, a string literal is passed as the first argument. But when searchFile calls xreadlines on the object, it works fine. This is because the wrapper function declared within fileOpener checks the first argument passed to the function. If it is a string, it treats it as a file path and opens the file, passing it on to searchFile. If is not a string, searchFile is invoked without any changes. This powerful idiom allows us to attach behavior to functions without actually changing the definition of the function.

Going farther

Previously we defined the fileOpener decorator that allowed a function that normally accepted a file object to be passed a string that is a file path and work as expected. However, it has the limitation that the decorator always expects the first argument to be the string or file object. You'd need to customize the decorator for each case. You could define fileOpener0, fileOpener1, etc. but that is obviously not very effecient. Instead, you can pass the decorator arguments. Let's go back to the example of requiring an argument to be of a specific type as in the acceptInt decorator. But let's generalize it to any type and argument

import inspect
import types
class accept(object):
    def __init__(self,argName,requiredType):
        self.requiredType = requiredType
        self.argName = argName

    def __call__(self,func):
        args,varargs,keywords,defaults = inspect.getargspec(func)
        argIndex = args.index(self.argName)
        def wrapper(*args):
            if type(args[argIndex]) != self.requiredType:
                args = list(args)
                args[argIndex] = self.requiredType(args[argIndex])

            return func(*args)
        return wrapper

@accept('searchFor',types.StringType)
def searchFile(f,searchFor):
    for line in f.xreadlines():
        if searchFor in line:
            return line

with open('/etc/passwd') as fin:
    print(searchFile(fin,'0'))

with open('/etc/passwd') as fin:    
    print(searchFile(fin,0))

This gives an output of

root:x:0:0:root:/root:/bin/bash

root:x:0:0:root:/root:/bin/bash

The searchFile function is the same as before. But the accept decorator is new and is declared as a class. This is fine because __call__ is also declared, and Python just expects a decorator to be callable. For more on what is callable in Python take a look here. The decorator also takes two arguments: the name of the argument and type it is required to be. When searchFile in invoked the first time a string is passed and matches a line. In the second call, the literal 0 is passed which is an int. But the same line in the file is matched. This works because the accept decorator converts it to a string.

The structure of the accept decorator may look a little out of place. Python actually passes the decorator arguments to the decorator in the constructor of the object. After that, it calls the constructed object and passes it just one argument: the function to be wrapped. It is expected that the decorator returns the wrapped function from the __call__ method. That's probably a little confusing, so here is the same function searchFile declared without the @ decorator syntax.

def searchFile(f,searchFor):
    for line in f.xreadlines():
        if searchFor in line:
            return line
searchFile = accept('searchFor',types.StringType)(searchFile)

The decorator is constructed once per use, immediately called, and then discarded. All the transformations on the wrapped function have to happen at that time.

Getting back to the way the accept decorator works, the __call__ method declares the function wrapper just like the acceptInt decorator. The inspect module is used to get the position of the argument specified in the decorator's constructor in args. The wrapper function checks the specified argument and converts it if the type is non-conformant. It's necessary to convert args to a list if the argument needs to be cast. When called, args is always a tuple which is immutable.

Readers may notice this example will not work with functions called using keyword arguments. The example could be extended but that is not the goal.

Multiple decorations

It is worth mentioning here that the same function can be decorated more than once. Decorators are called in ascending order. This is slightly counter-intuitive because we're used to read source code in a start to finish manner. Here is a contrived example to demonstrate this.

def deco0(func):
    print 'A'
    return func

def deco1(func):
    print 'B'
    return func

def deco2(func):
    print 'C'
    return func

@deco0    
@deco1
@deco2
def double(x):
    return 2*x

This gives an output of

C
B
A

As you can see, deco2 is called first. While we're at it, it is worth mentioning you can decorate the same function with the same decorator more than once. A use case hasn't been identified for this, however.

def deco(func):
    print 'A'
    return func

@deco   
@deco
@deco
def double(x):
    return 2*x

This gives an output of

A
A
A

Decorating instance methods

Being an object-oriented language, it's not unusual to want to decorate an instance method of an object. This requires understanding another idiosyncrasy of Python's: descriptors. Descriptors are a way of controlling the binding behavior of an object. To learn more about descriptor, start here. To decorate an instance method, a non-data descriptor is needed. This type of descriptor is an object implementing the __get__ method. The interpreter invokes this method to bind an object to a specific instance.

import copy
import types
import sys

class fileWriter(object):
    def __init__(self,default):
        self.default = default
    def __call__(self,func):
        return FileWriter(func,self.default)

class FileWriter(object):
    def __init__(self,wrapped,default):
        self.default = default
        self.wrapped = wrapped

    def __get__(self,instance,clazz):
        ret = copy.copy(self)
        ret.instance = instance
        return ret

    def __call__(self,*args):
        if len(args) == 0:
            args = [self.default]
        f = args[0]
        if type(f) == types.StringType:
            with open(f,'w') as fout:
                return self.wrapped(self.instance,fout)
        return self.wrapped(self.instance, f)

class BankAccount(object):
    def __init__(self,startingBalance):
        self.balance = startingBalance
        self.transactions = []

    def applyTransaction(self,amount):
        self.balance += amount

        if amount < 0:
            self.transactions.append(('withdrawal',amount,self.balance))
        else:
            self.transactions.append(('deposit',amount,self.balance))

    @fileWriter('default')
    def writeLog(self,fout):
        for action,amount,bal in self.transactions:
            fout.write(action + ' ' + str(amount)  + ' ' + str(bal) + '\n')

myAccount = BankAccount(1.0)
myAccount.applyTransaction(100.0)
myAccount.applyTransaction(-6.38)
myAccount.applyTransaction(-45.0)
myAccount.applyTransaction(350.0)

myAccount.writeLog(sys.stdout)
myAccount.writeLog('tmp')
myAccount.writeLog()

This gets written to standard output. It also gets written to the files 'tmp' and 'default'.

deposit 100.0 101.0
withdrawal -6.38 94.62
withdrawal -45.0 49.62
deposit 350.0 399.62

Let's understand what is going on with fileWriter and FileWriter. fileWriter is a decorator that is used to decorate BankAccount.writeLog. It doesn't do much, but does construct an instance of FileWriter. It returns that instance, replacing the BankAccount.writeLog in the BankAccount class. This is easy to see in the interactive interpreter

>>> print BankAccount.writeLog
<__main__.FileWriter object at 0x28cfe60>

The writeLog member of BankAccount is actually an instance of FileWriter even though it was declared as a function. It should be obvious to the reader that fileWriter and FileWriter work closely together. The naming is strictly a choice of the writer and is not convention.

What exactly is FileWriter? It's a non data descriptor. The Python interpreter calls the __get__ method when it wants to bind it to a specific instance. It passes in the instance and the class to the method. The class is safely ignored here. The __get__ method returns a copy of the FileWriter with the instance member filled in. This allows each instance of BankAccount to have it's own copy of FileWriter. Whenever writeLog is called on the myAccount instance, it's actually a copy of the FileWriter instance that is called. So the FileWriter.__call__ method is called. At this point, what it is doing should be obvious so I won't re-hash it.

As you can see, decorating an instance method is two-step ordeal involving created a decorator and an associtaed non-data descriptor. If you don't do this, you can still decorate methods of a class, but they wind up getting treated as non-instance methods even if you intended to declare an instance method.


Copyright Eric Urban 2013, or the respective entity where indicated