pythondecorator

Preserving signatures of decorated functions


Suppose I have written a decorator that does something very generic. For example, it might convert all arguments to a specific type, perform logging, implement memoization, etc.

Here is an example:

def args_as_ints(f):
    def g(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    return g

@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z

>>> funny_function("3", 4.0, z="5")
22

Everything well so far. There is one problem, however. The decorated function does not retain the documentation of the original function:

>>> help(funny_function)
Help on function g in module __main__:

g(*args, **kwargs)

Fortunately, there is a workaround:

def args_as_ints(f):
    def g(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    g.__name__ = f.__name__
    g.__doc__ = f.__doc__
    return g

@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z

This time, the function name and documentation are correct:

>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(*args, **kwargs)
    Computes x*y + 2*z

But there is still a problem: the function signature is wrong. The information "*args, **kwargs" is next to useless.

What to do? I can think of two simple but flawed workarounds:

1 -- Include the correct signature in the docstring:

def funny_function(x, y, z=3):
    """funny_function(x, y, z=3) -- computes x*y + 2*z"""
    return x*y + 2*z

This is bad because of the duplication. The signature will still not be shown properly in automatically generated documentation. It's easy to update the function and forget about changing the docstring, or to make a typo. [And yes, I'm aware of the fact that the docstring already duplicates the function body. Please ignore this; funny_function is just a random example.]

2 -- Not use a decorator, or use a special-purpose decorator for every specific signature:

def funny_functions_decorator(f):
    def g(x, y, z=3):
        return f(int(x), int(y), z=int(z))
    g.__name__ = f.__name__
    g.__doc__ = f.__doc__
    return g

This works fine for a set of functions that have identical signature, but it's useless in general. As I said in the beginning, I want to be able to use decorators entirely generically.

I'm looking for a solution that is fully general, and automatic.

So the question is: is there a way to edit the decorated function signature after it has been created?

Otherwise, can I write a decorator that extracts the function signature and uses that information instead of "*kwargs, **kwargs" when constructing the decorated function? How do I extract that information? How should I construct the decorated function -- with exec?

Any other approaches?


Solution

    1. Install decorator module:

      $ pip install decorator
      
    2. Adapt definition of args_as_ints():

      import decorator
      
      @decorator.decorator
      def args_as_ints(f, *args, **kwargs):
          args = [int(x) for x in args]
          kwargs = dict((k, int(v)) for k, v in kwargs.items())
          return f(*args, **kwargs)
      
      @args_as_ints
      def funny_function(x, y, z=3):
          """Computes x*y + 2*z"""
          return x*y + 2*z
      
      print funny_function("3", 4.0, z="5")
      # 22
      help(funny_function)
      # Help on function funny_function in module __main__:
      # 
      # funny_function(x, y, z=3)
      #     Computes x*y + 2*z
      

    Python 3.4+

    functools.wraps() from stdlib preserves signatures since Python 3.4:

    import functools
    
    
    def args_as_ints(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            args = [int(x) for x in args]
            kwargs = dict((k, int(v)) for k, v in kwargs.items())
            return func(*args, **kwargs)
        return wrapper
    
    
    @args_as_ints
    def funny_function(x, y, z=3):
        """Computes x*y + 2*z"""
        return x*y + 2*z
    
    
    print(funny_function("3", 4.0, z="5"))
    # 22
    help(funny_function)
    # Help on function funny_function in module __main__:
    #
    # funny_function(x, y, z=3)
    #     Computes x*y + 2*z
    

    functools.wraps() is available at least since Python 2.5 but it does not preserve the signature there:

    help(funny_function)
    # Help on function funny_function in module __main__:
    #
    # funny_function(*args, **kwargs)
    #    Computes x*y + 2*z
    

    Notice: *args, **kwargs instead of x, y, z=3.