我们说我有一个自定义装饰器,我希望它能正确处理装饰函数的文档字符串。问题是:我的装饰者添加了一个参数。
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
您可以看到参数实际上并未传递给修饰函数,而是由包装器使用 - 可能也可能使用 这里没有关系。
如何正确处理记录附加参数? 对于包装器采取额外的参数是一个好习惯,还是应该避免它?
或者我应该使用不同的解决方案,例如:
答案 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)>
之前的文本处理问题。因为每个项目的文档参数样式都不同在文档字符串内部,无法“一刀切”地可靠地完成它,但是通过一些字符串拼接和测试,它可以完美地适合任何给定的文档字符串样式)