ProgrammingBits

Exploring Python's awesomeness. By Ariel Ortiz.

More On Function Decorators

aortiz | 15 February, 2009 15:00

In my previous post, I presented a couple of examples on how to use function decorators in Python. Those examples were illustrative, yet fairly restricted. First of all, they assumed that the function being decorated only receives one positional argument. What can I do if I want to decorate a function that takes two or more positional arguments, or one or more keyword arguments? Secondly, they didn't allow the decorator to be configured in any special way. So, how do I send input arguments to the decorator itself so that I can vary its behavior?

I will now elaborate in a slightly more complex example that will give us a better insight on how to define much more general decorators, without any of the restrictions I just mentioned.

NOTE: All Python code examples presented here are based in Python 3.0. Full source code: more_function_decorators.py

Suppose we want a decorator function that is able to "swallow" one or more kinds of exceptions that might be raised during the execution of the decorated function1. If a particular exception is actually produced, we want to be able to specify a default value to be returned instead of allowing the exception to propagate through the execution stack and possibly causing the program to terminate. A client of our decorator would basically be able to avoid the hassle of writing an explicit try statement.

To demonstrate how this could work, let's assume we have a function like this one:

1
2
def divide(dividend=0, divisor=1):
    return dividend / divisor

When calling this function, there are at least two possible exceptions that might get raised: ZeroDivisionError and TypeError. The first one is produced when the divisor parameter is zero. The second exception occurs when any of the two arguments sent are not of a numerical type, when more than two arguments are actually sent, or when you try to use a nonexistent keyword argument. Some examples:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
>>> divide(1, 2)
0.5
 
>>> divide(1, 0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in divide
ZeroDivisionError: int division or modulo by zero
 
>>> divide("hello")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in divide
TypeError: unsupported operand type(s) for /: 'str' 
and 'int' 
 
>>> divide(whatever=1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: divide() got an unexpected keyword 
argument 'whatever'

Our swallow decorator will receive two keyword arguments:

  • exceptions: a single exception class or a tuple containing several exception classes. These represent the exceptions that should be swallowed. Any other exception will be propagated as usual. Defaults to BaseException (the root of Python's exception hierarchy) if it's not explicitly provided.
  • default: the value to return if one of the specified exceptions is raised. Defaults to None when not provided.

Let's look at three usage examples.

Example 1: If a division by zero is attempted, return zero:

1
2
3
@swallow(exceptions=ZeroDivisionError, default=0)
def divide(dividend=0, divisor=1):
    return dividend / divisor

Example 2: If a ZeroDivisionError or a TypeError is raised, return zero:

1
2
3
4
5
@swallow(
    exceptions=(ZeroDivisionError, TypeError), 
    default=0)
def divide(dividend=0, divisor=1):
    return dividend / divisor

Example 3: Chain two swallow decorators, so that each specific exception has its own default value:

1
2
3
4
@swallow(exceptions=TypeError, default='Huh?')
@swallow(exceptions=ZeroDivisionError, default=0)
def divide(dividend=0, divisor=1):
    return dividend / divisor

For this last example, this is how we could now use our decorated divide function:

1
2
3
4
5
6
>>> divide(1, 2)
0.5
>>> divide(1, 0)
0
>>> divide("hello")
'Huh?'

In order to implement the swallow decorator, we must take a better look on how the @ syntax works. After the @ sign, you must actually specify an expression that when evaluated produces a callable object2. When this callable object is effectively called, it receives as its only argument the function that is to be decorated, and it returns that same function or some new callable thing.

The expression after the @ sign is commonly just the name of a function (the decorator function), but it can also be: 1) a new instance of a class that contains an implementation of the __call__ method; or 2) a call to some other function. Both these options allow us to send additional information to the decorator by specifying it via input parameters.

A first implementation using classes that define the __call__ method could be as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class swallow:
    
    class helper:
        
        def __init__(self, outer, fun):
            self.outer = outer
            self.fun = fun            
            
        def __call__(self, *args, **kwargs):
            try:
                return self.fun(*args, **kwargs)
            except self.outer.exceptions:
                return self.outer.default
    
    def __init__(self, 
                 default=None, 
                 exceptions=BaseException):
        self.default = default
        self.exceptions = exceptions
        
    def __call__(self, fun):                            
        return swallow.helper(self, fun)

Let's try to understand how this works. This code:

1
2
3
@swallow(exceptions=ZeroDivisionError, default=0)
def divide(dividend=0, divisor=1):
    return dividend / divisor

is basically equivalent to this one:

1
2
3
4
def divide(dividend=0, divisor=1):
    return dividend / divisor     
divide = swallow(exceptions=ZeroDivisionError, 
                 default=0).__call__(divide)

In the last statement of the above code, an instance of the swallow class is created and initialized according to our needs. Then, the __call__ method is invoked on that very same instance, which in turn creates and returns a new instance of the swallow.helper nested class. The final effect is that the divide variable refers to an instance of this specific class. 

The swallow.helper class implements a __call__ method that accepts any number of positional and keyword arguments3. Thus, instances of this class can effectively decorate any function that takes whatever arguments it needs. The __call__ method itself contains a try statement that does all the work: it invokes the decorated function and sends back the returned value, unless any of the specified exceptions get caught, in which case it returns the specified default value. Note that all the values that are required to do the job are conveniently stored and shared using the instance variables of the swallow and swallow.helper classes.

A second implementation of the swallow decorator can be coded using only function definitions (and their corresponding lexical closures). Although it's considerably shorter, it might be a little bit more difficult to understand at first:

1
2
3
4
5
6
7
8
9
def swallow(default=None, exceptions=BaseException):
    def helper1(fun):
        def helper2(*args, **kwargs):
            try:
                return fun(*args, **kwargs)
            except exceptions:
                return default
        return helper2
    return helper1

As can be observed, the swallow function takes the two input parameters that allow us to configure the decorator. It returns the nested function helper1, which is just what the @ syntax expects. The helper1 function will be immediately called with the function being decorated as its only argument, returning function helper2 as its result. This means that if we are decorating a function called f, variable f will end up holding a reference to the helper2 function. So now, whenever f gets invoked, helper2 will be called and the try statement will do its job exactly as described before.

Notes

1 Swallowing exceptions can be convenient under certain circumstances, but you should avoid using this technique indiscriminately. Specifically, it can make debugging code very hard.

2 Callable objects contain a special attribute named __call__. If x is a callable object, then the syntax:

1
x(arg1, arg2, arg3)

is equivalent to:

1
x.__call__(arg1, arg2, arg3)

Callable objects include user-defined functions, built-in functions, methods of built-in objects, class objects, methods of class instances, and instances of classes that define or inherit their own __call__ method.

3 If your not familiar with the *args and **kwargs notation, check the Python tutorial for more details.

Comments

Forgot numeric type emulation

Heikki Toivonen | 16/02/2009, 03:21

It is incorrect to say the divide function can only raise two kinds of exceptions. Python allows you to emulate numeric types, for example by defining the __div__ method: http://www.python.org/doc/3.0/reference/datamodel.html#emulating-numeric-types

Try this with your code:

class A:
def __div__(self, other):
raise ValueError("Oops!")

divide(A(), 1)

Re: More On Function Decorators

aortiz | 16/02/2009, 07:44

aortiz

You're absolutely right Heikki, thanks for your observation.

Re: More On Function Decorators

erdinc | 24/07/2009, 08:23

Thank you suh a good explanation, at last I understand decoraters :-)

Thanks!

iJames | 04/03/2010, 10:40

This posting was quite a valuable posting in the midst of all that is python on the web. I found it by searching for 'python "at sign"' because I had no idea what it was doing in code! Thanks a lot! The posting was the kind that, by reading a piece and working through it made me want to read more. It's one big nugget of value. I'm working in Django with Python 2.6.4 for the first time and coming from the PHP world, it's been such a relief to be in such a powerful tool and especially one that's interpreted!!! I can feel the power!

Add comment

authimage

 
Accessible and Valid XHTML 1.0 Strict and CSS
Powered by LT - Design by BalearWeb