Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Nested function decorators that operate on arguments in python

I am writing a function decorator that will apply a conversion to the first argument of the function. It works fine if I only decorate my functions once but if I decorate them twice I get an error. Below is some code that demonstrates the problem, it is a simplified version of the code I'm working on. I have excluded the code that does the conversion so as to not distract from the problem

from inspect import getargspec
from functools import wraps

def dec(id):
    def _dec(fn):
        @wraps(fn)
        def __dec(*args, **kwargs):
            if len(args):
                return fn(args[0], *args[1:], **kwargs)
            else:
                first_arg = getargspec(fn).args[0]
                new_kwargs = kwargs.copy()
                del new_kwargs[first_arg]
                return fn(kwargs[first_arg], **new_kwargs)
        return __dec
    return _dec

@dec(1)
def functionWithOneDecorator(a, b, c):
    print "functionWithOneDecorator(a = %s, b = %s, c = %s)" % (a, b, c)

@dec(1)
@dec(2)
def functionWithTwoDecorators(a, b, c):
    print "functionWithTwoDecorators(a = %s, b = %s, c = %s)" % (a, b, c)

functionWithOneDecorator(1, 2, 3)
functionWithOneDecorator(1, b=2, c=3)
functionWithOneDecorator(a=1, b=2, c=3)
functionWithOneDecorator(c=3, b=2, a=1)

functionWithTwoDecorators(1, 2, 3)
functionWithTwoDecorators(1, b=2, c=3)
functionWithTwoDecorators(a=1, b=2, c=3)
functionWithTwoDecorators(c=3, b=2, a=1)

When I run the above code I get the following output:

functionWithOneDecorator(a = 1, b = 2, c = 3)
functionWithOneDecorator(a = 1, b = 2, c = 3)
functionWithOneDecorator(a = 1, b = 2, c = 3)
functionWithOneDecorator(a = 1, b = 2, c = 3)
functionWithTwoDecorators(a = 1, b = 2, c = 3)
functionWithTwoDecorators(a = 1, b = 2, c = 3)
IndexError: list index out of range

This is because when the second decorator inspects the function it is decorating to find the argument names and fails because it is decorating a decorator and that only takes *args and **kwargs.

I can think of ways around the problem which would work in the code above but would still break if a function was decorated with my decorator and another from a 3rd party. Is there a general way to fix this? or is there a better way to achieve the same result?

Update: Thanks to @Hernan for pointing out the decorator module. It solves this problem exactly. Now my code looks like this:

from decorator import decorator

def dec(id):
    @decorator
    def _dec(fn, *args, **kwargs):
        return fn(args[0], *args[1:], **kwargs)
    return _dec

@dec(1)
def functionWithOneDecorator(a, b, c):
    print "functionWithOneDecorator(a = %s, b = %s, c = %s)" % (a, b, c)

@dec(1)
@dec(2)
def functionWithTwoDecorators(a, b, c):
    print "functionWithTwoDecorators(a = %s, b = %s, c = %s)" % (a, b, c)

functionWithOneDecorator(1, 2, 3)
functionWithOneDecorator(1, b=2, c=3)
functionWithOneDecorator(a=1, b=2, c=3)
functionWithOneDecorator(c=3, b=2, a=1)

functionWithTwoDecorators(1, 2, 3)
functionWithTwoDecorators(1, b=2, c=3)
functionWithTwoDecorators(a=1, b=2, c=3)
functionWithTwoDecorators(c=3, b=2, a=1)    

Much cleaner, and it works!

like image 267
Andrew Burrows Avatar asked Oct 09 '11 21:10

Andrew Burrows


1 Answers

The problem is that the signature of your decorated function is not the signature (getargspec) from the original. It is really well explained in the help of the decorator module which you can to solve your problem. Basically, you should use a signature-preserving decorators, so that the second decorator sees the same signature as the first.

like image 182
Hernan Avatar answered Nov 15 '22 15:11

Hernan