Brad Solomon Brad Solomon - 1 month ago 19
Python Question

Decorators with parameters

I have a collection of functions with (mostly) shared parameters but different processes. I'd like to use a decorator to add the description for each parameter to a function's headline-level docstring.

I've tried to mimic the structure found in this answer by incorporating a nested function within

appender
but failed. I've also tried
functools.partial
but something is slightly off.

My attempt:

def appender(func, *args):
"""Appends additional parameter descriptions to func's __doc__."""
def _doc(func):
params = ''.join([defaultdocs[arg] for arg in args])
func.__doc__ += '\n' + params
return func
return _doc

defaultdocs = {

'a' :
"""
a : int, default 0
the first parameter
""",

'b' :
"""
b : int, default 1
the second parameter
"""
}

@appender('a')
def f(a):
"""Title-level docstring."""
return a

@appender('a', 'b')
def g(a, b):
"""Title-level docstring."""
return a + b


This fails, and it fails I believe because the first arg passed to
appender
is interpreted as
func
. So when I view the resulting docstring for
g
I get:

print(g.__doc__)
Title-level docstring.

b : int, default 1
the second parameter


because, again,
'a'
is interpreted to be
'func'
when I want it to be the first element of
*args
. How can I correct this?

Desired result:

print(g.__doc__)
Title-level docstring.

a : int, default 0
the first parameter

b : int, default 1
the second parameter

Answer Source

This happens because the variable names you pass actually get captured into a func argument.

In order to do callable decorators in Python you need to code the function twice, having external function to accept decorator arguments and internal function to accept original function. Callable decorators are just higher-order functions that return other decorators. For example:

def appender(*args):  # This is called when a decorator is called,
                      # e. g. @appender('a', 'b')
    """Appends additional parameter descriptions to func's __doc__."""
    def _doc(func):  # This is called when the function is about
                     # to be decorated
        params = ''.join([defaultdocs[arg] for arg in args])
        func.__doc__ += '\n' + params
        return func
    return _doc

The external (appender) function acts as a factory for new decorator while _doc function is an actual decorator. Always pass it this way:

  • Pass decorator args to the external function
  • Pass original function to the internal function

Once the Python sees this:

@appender('a', 'b')
def foo(): pass

...it will do something like this under the hood:

foo = appender('a', 'b')(foo)

...which expands to this:

decorator = appender('a', 'b')
foo = decorator(foo)

Because of how scopes in Python work, each newly returned _doc function instance will have its own local args value from the external function.