签名更改装饰器:正确记录其他参数

时间:2015-12-21 19:16:08

标签: python python-decorators

我们说我有一个自定义装饰器,我希望它能正确处理装饰函数的文档字符串。问题是:我的装饰者添加了一个参数。

from functools import wraps

def custom_decorator(f):
    @wraps(f)
    def wrapper(arg, need_to_do_more):
        '''
        :param need_to_do_more: if True: do more
        '''
        args = do_something(arg)

        if need_to_do_more:
            args = do_more(args)

        return f(args)

    return wrapper

您可以看到参数实际上并未传递给修饰函数,而是由包装器使用 - 可能也可能使用 这里没有关系。

如何正确处理记录附加参数? 对于包装器采取额外的参数是一个好习惯,还是应该避免它?

或者我应该使用不同的解决方案,例如:

  • 使包装器成为一个简单的高阶函数,并将其调用的函数作为第三个参数传递
  • 将包装器重构为两个独立的函数?

2 个答案:

答案 0 :(得分:2)

如果预期装饰的结果总是为该参数提供相同的东西,我建议将其作为参数化装饰器。我猜你想到了这一点,但需要说出来。

除此之外,我肯定会建议将其分成两部分,就像你的第二个建议一样。然后,装饰器的用户可以提供使用两个不同装饰器的“重载”版本(不会真正重载,因为它们需要不同的名称)。

另一种可能的选择是给参数一个默认值。

最后,如果您必须保持原样,则需要在包装器定义之后将新参数文档附加到__doc__ 的末尾。

所以你的例子(缩短版)看起来像这样:

def custom_decorator(f):
    @wraps(f)
    def wrapper(arg, need_to_do_more):
        ...

    wrapper.__doc__ += "/n:param need_to_do_more: if True: do more"
    return wrapper

这是因为@wraps(f)装饰器用wrapper替换了f的文档。之后添加它实际上将两者结合起来。

这些行中的另一个选项是记录custom_decorator,以便它说包装的方法需要将参数添加到他们的文档中。这个,以及装饰器的分裂,给用户带来了负担,但是他们使意图更加明确(“......明确比隐含更好......” - 禅宗Python)

答案 1 :(得分:0)

所以-__doc__不仅棘手-而且,由于越来越多的开发人员在编码时依靠IDE内省的自动参数建议(这是IDE内省的),因此对于任何需要添加额外内容的装饰器来说,实际上都是需要的函数的命名参数。

在开发的项目中,我解决了这个问题,解决方案是创建一个新的虚拟函数,该虚拟函数将具有所需的要显示的组合签名-然后将这个新的虚拟函数用作{ {1}}通话。

这是我的代码-足够好,因此与其他项目无关,我可能会很快将其放入装饰器Python包中。现在:

@wraps

在交互模式下进行测试可以得到以下结果:


def combine_signatures(func, wrapper=None):
    """Adds keyword-only parameters from wrapper to signature

    Use this in place of `functools.wraps` 
    It works by creating a dummy function with the attrs of func, but with
    extra, KEYWORD_ONLY parameters from 'wrapper'.
    To be used in decorators that add new keyword parameters as
    the "__wrapped__"

    Usage:

    def decorator(func):
        @combine_signatures(func)
        def wrapper(*args, new_parameter=None, **kwargs):
            ...
            return func(*args, **kwargs)
    """
    # TODO: move this into 'extradeco' independent package
    from functools import partial, wraps
    from inspect import signature, _empty as insp_empty, _ParameterKind as ParKind
    from itertools import groupby

    if wrapper is None:
        return partial(combine_signatures, func)

    sig_func = signature(func)
    sig_wrapper = signature(wrapper)
    pars_func = {group:list(params)  for group, params in groupby(sig_func.parameters.values(), key=lambda p: p.kind)}
    pars_wrapper = {group:list(params)  for group, params in groupby(sig_wrapper.parameters.values(), key=lambda p: p.kind)}

    def render_annotation(p):
        return f"{':' + (repr(p.annotation) if not isinstance(p.annotation, type) else repr(p.annotation.__name__)) if p.annotation != insp_empty else ''}"

    def render_params(p):
        return f"{'=' + repr(p.default) if p.default != insp_empty else ''}"

    def render_by_kind(groups, key):
        parameters = groups.get(key, [])
        return [f"{p.name}{render_annotation(p)}{render_params(p)}" for p in parameters]

    pos_only = render_by_kind(pars_func, ParKind.POSITIONAL_ONLY)
    pos_or_keyword = render_by_kind(pars_func, ParKind.POSITIONAL_OR_KEYWORD)
    var_positional = [p for p in pars_func.get(ParKind.VAR_POSITIONAL,[])]
    keyword_only = render_by_kind(pars_func, ParKind.KEYWORD_ONLY)
    var_keyword = [p for p in pars_func.get(ParKind.VAR_KEYWORD,[])]

    extra_parameters = render_by_kind(pars_wrapper, ParKind.KEYWORD_ONLY)

    def opt(seq, value=None):
        return ([value] if value else [', '.join(seq)]) if seq else []

    annotations = func.__annotations__.copy()
    for parameter in pars_wrapper.get(ParKind.KEYWORD_ONLY):
        annotations[parameter.name] = parameter.annotation

    param_spec = ', '.join([
        *opt(pos_only),
        *opt(pos_only, '/'),
        *opt(pos_or_keyword),
        *opt(keyword_only or extra_parameters, ('*' if not var_positional else f"*{var_positional[0].name}")),
        *opt(keyword_only),
        *opt(extra_parameters),
        *opt(var_keyword, f"**{var_keyword[0].name}" if var_keyword else "")
    ])
    declaration = f"def {func.__name__}({param_spec}): pass"

    f_globals = func.__globals__
    f_locals = {}

    exec(declaration, f_globals, f_locals)

    result = f_locals[func.__name__]
    result.__qualname__ = func.__qualname__
    result.__doc__ = func.__doc__
    result.__annotations__ = annotations

    return wraps(result)(wrapper)

(对于文档字符串,就像在问题中一样-一旦获得了所有包装器和函数数据,这就是粘贴IPython 7.8.0 -- An enhanced Interactive Python. Type '?' for help. In [1]: from terminedia.utils import combine_signatures In [2]: def add_color(func): ...: @combine_signatures(func) ...: def wrapper(*args, color=None, **kwargs): ...: global context ...: context.color = color ...: return func(*args, **kw) ...: return wrapper ...: In [3]: @add_color ...: def line(p1, p2): ...: pass ...: In [4]: line Out[4]: <function __main__.line(p1, p2, *, color=None)> 之前的文本处理问题。因为每个项目的文档参数样式都不同在文档字符串内部,无法“一刀切”地可靠地完成它,但是通过一些字符串拼接和测试,它可以完美地适合任何给定的文档字符串样式)