带参数的装饰器

Decorators with parameters

我有一个 collection 函数,它们具有(大部分)共享参数但不同的进程。我想使用装饰器将每个参数的描述添加到函数的 headline-level 文档字符串中。

我试图通过在 appender 中加入嵌套函数来模仿 this answer 中的结构,但失败了。我也试过 functools.partial 但有点不对劲。

我的尝试:

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

这失败了,我相信它失败了,因为传递给 appender 的第一个参数被解释为 func。因此,当我查看 g 的结果文档字符串时,我得到:

print(g.__doc__)
Title-level docstring.

    b : int, default 1
        the second parameter

因为当我希望它成为 *args 的第一个元素时,'a' 又被解释为 'func'。我该如何纠正?

想要的结果:

print(g.__doc__)
Title-level docstring.

    a : int, default 0
        the first parameter

    b : int, default 1
        the second parameter

发生这种情况是因为您传递的变量名称实际上被捕获到 func 参数中。

为了在 Python 中执行可调用装饰器,您需要对函数进行两次编码,使用 external 函数来接受装饰器参数和 internal 接受原函数的函数。可调用装饰器只是 return 其他装饰器的高阶函数。例如:

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

外部 (appender) 函数充当新装饰器的工厂,而 _doc 函数是一个实际的装饰器。总是这样传递:

  • 将装饰器参数传递给外部函数
  • 将原函数传递给内部函数

Python 看到后:

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

...它会在引擎盖下做这样的事情:

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

...扩展为:

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

由于 Python 中作用域的工作方式,每个新 returned _doc 函数实例都将具有来自外部函数的自己的本地 args 值。

使用 inspect.signature 收集传递的函数参数的替代解决方案。

import inspect
import textwrap

def appender(defaultdocs):
    def _doc(func):
        params = inspect.signature(func).parameters 
        params = [param.name for param in params.values()] 
        params = ''.join([textwrap.dedent(defaultdocs[param]) 
                      for param in params])
        func.__doc__ += '\n\nParameters\n' + 10 * '=' + params
        return func

    return _doc

示例:

# default docstrings for parameters that are re-used often
# class implementation not a good alternative in my specific case

defaultdocs = {

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

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

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

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

这会将 ab 的描述附加到 g.__doc__ 而无需在装饰器中指定它们:

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

g(a, b)
    Title-level docstring.

    Parameters
    ==========
    a : int, default 0
        the first parameter
    b : int, default 1
        the second parameter