python - 签名更 retrofit 饰器 : properly documenting additional argument

标签 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

你可以看到参数实际上并没有传递给装饰函数,而是被包装器使用——这可能是也可能是 在这里不相关。

如何正确处理记录附加参数? 包装器采用额外参数是一种好习惯,还是我应该避免它?

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

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

最佳答案

所以 - __doc__ 除了这很棘手 - 而且,由于越来越多的开发人员在编码时依赖自动参数建议,这是由 IDE 自省(introspection)提供的,任何装饰器都确实需要它将向函数添加额外的命名参数。

我在 a project I am developing 中做到了这一点,解决方案是创建一个新的虚拟函数,它将显示所需的组合签名 - 然后使用这个新的虚拟函数作为 @wraps 调用的参数,。

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


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)>

(至于 doc 字符串,就像问题中一样 - 一旦获得了所有包装器和函数数据,这是粘贴之前的文本处理问题 result.__doc__ = func.__doc__ 。因为每个项目在 docstrings 中都有不同的文档参数样式,它不能以“一刀切”的方式可靠地完成,但是通过一些字符串拼接和测试,它可以针对任何给定的文档字符串样式进行完善)

关于python - 签名更 retrofit 饰器 : properly documenting additional argument,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34402773/

相关文章:

python - 如何检查Python中的函数装饰器

python - 在pygame python3中创建登录系统

python - 在python中将32位二进制转换为十进制

python - 更改标签之间的文本 - shell 脚本

python - 如何防止使用装饰器在views.py 中处理表单时出现重复代码?

python - 即使被装饰的函数被调用多次,装饰器也会运行一次吗?

Python3 不可能将 @property 作为装饰器参数传递

python - Pandas:当我尝试使用 pip 安装它时出错

python - 从编辑器与控制台设置 pandas scatter_matrix 的轴限制

python - 在 Python 中尝试/排除 : How to avoid duplication?