views:

1787

answers:

2

In a comment on the answer to another question, someone said they weren't sure what functools.wraps was doing. So I'm asking this question so that there will be a record of it on StackOverflow for future reference: what does functools.wraps do, exactly?

+25  A: 

When you use a decorator, you're replacing one function with another. In other words, if you have a decorator

def logged(func):
    def with_logging(*args, **kwargs):
        print func.__name__ + " was called"
        return func(*args, **kwargs)
    return with_logging

then when you say

@logged
def f(x):
   """does some math"""
   return x + x * x

it's exactly the same as saying

def f(x):
    """does some math"""
    return x + x * x
f = logged(f)

and your function f is replaced with the function with_logging. Unfortunately, this means that if you then say

print f.__name__

it will print with_logging because that's the name of your new function. In fact, if you look at the docstring for f, it will be blank because with_logging has no docstring, and so the docstring you wrote won't be there anymore. Also, if you look at the pydoc result for that function, it won't be listed as taking one argument x; instead it'll be listed as taking *args and **kwargs because that's what with_logging takes.

If using a decorator always meant losing this information about a function, it would be a serious problem. That's why we have functools.wraps. This takes a function used in a decorator and adds the functionality of copying over the function name, docstring, arguments list, etc. And since wraps is itself a decorator, the following code does the correct thing:

from functools import wraps
def logged(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        print func.__name__ + " was called"
        return func(*args, **kwargs)
    return with_logging

@logged
def f(x):
   """does some math"""
   return x + x * x

print f.__name__  # prints 'f'
print f.__doc__   # prints 'does some math'
Eli Courtwright
Nice answer, but if I use `help(f)` then it will say`f(*args, **kwargs)`` does some math`and so it hasn't got the functions arguments back for this case. I have so far failed to find a solution to this little problem...
Scott Griffiths
There's a solution to the help problem which uses the decorator module, and is mentioned in the comments on this answer: http://stackoverflow.com/questions/1782843/python-decorator-problem-with-docstrings/1782888#1782888
Scott Griffiths
Yep, I prefer to avoid the decorator module since functools.wraps is part of the standard library and thus doesn't introduce another external dependency. But the decorator module does indeed solve the help problem, which hopefully functools.wraps someday will as well.
Eli Courtwright
+2  A: 

I very often use classes, rather than functions, for my decorators. I was having some trouble with this because an object won't have all the same attributes that are expected of a function. For example, an object won't have the attribute name. I had a specific issue with this that was pretty hard to trace where Django was reporting the error "object has not attribute 'name'". Unfortunately, for class-style decorators, I don't believe that @wrap will do the job. I have instead created a base decorator class like so:

class DecBase(object):
    func = None

def __init__(self, func):
 self.__func = func

def __getattribute__(self, name):
 if name == "func":
  return super(dec_base, self).__getattribute__(name)

 return self.func.__getattribute__(name)

def __setattr__(self, name, value):
 if name == "func":
  return super(dec_base, self).__setattr__(name, value)

 return self.func.__setattr__(name, value)

This class proxies all the attribute calls over to the function that is being decorated. So, you can now create a simple decorator that checks that 2 arguments are specified like so:

class process_login(DecBase):
    def __call__(self, *args):
        if len(args) != 2:
            raise Exception("You can only specify two arguments")

        return self.func(*args)
Josh