> 22th May 2009, 00:27       Duss

Python decorators and optional arguments

Python decorators are available since version 2.4. Version 2.6 includes class decorators, but the following discussion refers exclusively to function decorators.

Function decorators are implemented as functions that take -eventually- another function as parameter, the function it is decorating-. This is not the only implementation alternative, as decorators can be also implemented as classes, but for the purpose of this document, the function implementation assumption is generic enough. For detailed information on decorators, I would refer to its PEP, to the decorator.py package, or to this tutorial at developersworks

In short, the syntax:

@decorate
def f(....)

is quite equivalent to do:

f = decorate(f)

That is, the decorator takes f as parameter, and the return value overrides the initial f definition.

Decorators can have arguments, like in:

@decorate(message)
def f(....)

In this case, the decoration is not carried out by decorate function, but by its return value. That is, this example is quite equivalent to do:

f = decorate(message)(f)

Just in case, this is:

temp = decorate(message)
f = temp(f)

The distinction is important when the decorator's arguments are optional.

To see why, I will use a simple example, a unit tests package, that requires decorating the functions to test with a decorator @test, like in:

@test
def functionToTest(...):
  pass

Its implementation could be rather simple, just adding an attribute to the function to decorate:

def test(f):
   f.test = True
   return f

And the testing framework would then test all the functions with the given attribute. As the framework evolves, functionality is added to test categories, so that only the functions in the specified category are tested. With this purpose, the previous decorator is extended, accepting now a parameter category; note that its implementation gets a bit more complicated, as the real decorator is now the returned value:

def test(category):
   def real_decorator(f):
      f.test = True
      f.test_category = category
      return f
   return real_decorator

This nesting is required, as we will use the decoration like in:

@test('database')
  def f(....)

equivalent to:

test('database')(f)

But updating the decorator as shown above would break all the existing codebase, as it requires now a mandatory argument. A basic solution would be providing a default value:

def test(category=None):
   def real_decorator(f):
      f.test = True
      f.test_category = category
      return f
   return real_decorator

Unfortunately, this won't work. The codebase will still contain many functions decorated without arguments. In those cases, we hare invoking the decorator with one parameter, but that passed parameter, that should be the category is, in fact, the function to decorate. For this to work, the decoration should be:

f = test()(f)

So the correct way to use the decorator becomes now:

@test()
def functionToTest():
  print 'tested'

Just to start, this is not elegant. A decorator without arguments is expected to be called without those brackets.

And, to continue, there exists no complete solution, or better said, not completely generic solution. Fortunately, there is a solution for most of the cases; the problem is that the decorator's function must be able to handle two different situations:

  • Called with arguments that represent the expected arguments. And return then something that will be called again with the function to decorate.
  • Called with a single argument: the function to decorate.

The only possibility is to check:

  • That there is only one single argument, and is the first of the defined arguments.
  • And that its type is a function.

In this case, it is possible to assume that the decorator is being used without brackets. And this solution is generic as far as the decorator doesn't expect as first optional argument a function.

The previous example could be implemented now as:

def test(category=None):
   def real_decorator(f):
      f.test = True
      f.test_category = category
      return f
   if type(category) == type(real_decorator):
      return real_decorator(category)
   return real_decorator

Obviously, if the category could be a function, this wouldn't work. If the decorator expects more than one optional argument, better place those with function values -if any- on the second or later positions.

Just a final note on the previous code: it verifies if the type of the argument is a function, but not if it is a method in a class. When a method's class is decorated, the decoration mechanism is triggered before it is bounded to the class, so it would be useless to write the more extended check:

if type(category)==types.FunctionType or type(category)==types.MethodType :
	...