我想验证(在运行时,这不是打字问题),作为参数传递的函数只接受 1 个位置变量(基本上该函数将使用字符串作为输入调用并返回真值)。
天真地,这就是我所拥有的:
def check(v_in : Callable):
"""check that the function can be called with 1 positional parameter supplied"""
sig = signature(v_in)
len_param = len(sig.parameters)
if not len_param == 1:
raise ValueError(
f"expecting 1 parameter, of type `str` for {v_in}. got {len_param}"
)
return v_in
如果我检查了以下功能,就可以了,这很好:
def f_ok1_1param(v : str):
pass
但是下一个失败了,尽管
*args
会很好地接收 1 个参数并且 **kwargs
会是空的
def f_ok4_vargs_kwargs(*args,**kwargs):
pass
from rich import inspect as rinspect
try:
check(f_ok1_1param)
print("\n\npasses:")
rinspect(f_ok1_1param, title=False,docs=False)
except (Exception,) as e:
print("\n\nfails:")
rinspect(f_ok1_1param, title=False,docs=False)
try:
check(f_ok4_vargs_kwargs)
print("\n\npasses:")
rinspect(f_ok4_vargs_kwargs, title=False,docs=False)
except (Exception,) as e:
print("\n\nfails:")
rinspect(f_ok4_vargs_kwargs, title=False,docs=False)
第一个通过,第二个不及格,而不是两个都通过:
passes:
╭─────────── <function f_ok1_1param at 0x1013c0f40> ───────────╮
│ def f_ok1_1param(v: str): │
│ │
│ 37 attribute(s) not shown. Run inspect(inspect) for options. │
╰──────────────────────────────────────────────────────────────╯
fails:
╭──────── <function f_ok4_vargs_kwargs at 0x101512200> ────────╮
│ def f_ok4_vargs_kwargs(*args, **kwargs): │
│ │
│ 37 attribute(s) not shown. Run inspect(inspect) for options. │
╰──────────────────────────────────────────────────────────────╯
所有不同的签名组合定义如下:
def f_ok1_1param(v : str):
pass
def f_ok2_1param(v):
pass
def f_ok3_vargs(*v):
pass
def f_ok4_p_vargs(p, *v):
pass
def f_ok4_vargs_kwargs(*args,**kwargs):
pass
def f_ok5_p_varg_kwarg(param,*args,**kwargs):
pass
def f_bad1_2params(p1, p2):
pass
def f_bad2_kwargs(**kwargs):
pass
def f_bad3_noparam():
pass
现在,我确实已经检查了更多关于参数的信息:
rinspect(signature(f_ok4_vargs_kwargs).parameters["args"])
╭─────────────────── <class 'inspect.Parameter'> ───────────────────╮
│ Represents a parameter in a function signature. │
│ │
│ ╭───────────────────────────────────────────────────────────────╮ │
│ │ <Parameter "*args"> │ │
│ ╰───────────────────────────────────────────────────────────────╯ │
│ │
│ KEYWORD_ONLY = <_ParameterKind.KEYWORD_ONLY: 3> │
│ kind = <_ParameterKind.VAR_POSITIONAL: 2> │
│ name = 'args' │
│ POSITIONAL_ONLY = <_ParameterKind.POSITIONAL_ONLY: 0> │
│ POSITIONAL_OR_KEYWORD = <_ParameterKind.POSITIONAL_OR_KEYWORD: 1> │
│ VAR_KEYWORD = <_ParameterKind.VAR_KEYWORD: 4> │
│ VAR_POSITIONAL = <_ParameterKind.VAR_POSITIONAL: 2> │
╰───────────────────────────────────────────────────────────────────╯
我想检查每个参数的
Parameter.kind
与 _ParameterKind
枚举是需要如何处理的,但我想知道我是否想得太多了,或者是否已经存在可以做到这一点的东西,无论是在 inspect
中还是在 typing
protocol
支持可以用来做,但是在运行时.
注意,理论上
def f_ok_cuz_default(p, p2 = None):
也可以,但我们暂时忽略它。
附注动机是在验证框架中提供自定义回调函数。调用位置在框架的深处,并且该特定参数也可以是字符串(转换为正则表达式)。它甚至可以是无。这里最简单的就是贴一个
def myfilter(*args,**kwargs): breakpoint
。或者myfilter(foo)
。然后看看你从框架中得到了什么,并调整身体。在函数中有异常是一回事,框架接受它但是在调用它之前出错是另一回事。所以快速“当我们调用它时它会起作用吗?”对用户更友好。
我不认为你的问题是微不足道的。而且我不知道任何给定的实现,所以我按照你的思路得出的结论是,问题最终归结为回答以下问题:
如果您对所有问题的回答都是肯定的,那么您的 函数可以只用一个参数调用,否则不能。
在这种情况下,
f(a)
,或f(a, /)
,或f(*args)
;f(a, *args)
,或f(a, **kwargs)
,或f(a, b=None)
.您可以执行相应的检查如下:
from inspect import Parameter, signature
from typing import Callable
def _is_positional(param: Parameter) -> bool:
return param.kind in [
Parameter.POSITIONAL_OR_KEYWORD,
Parameter.POSITIONAL_ONLY,
Parameter.VAR_POSITIONAL]
def _is_optional(param: Parameter) -> bool:
return (param.kind in [
Parameter.VAR_POSITIONAL,
Parameter.VAR_KEYWORD] or
param.default is not Parameter.empty)
def has_one_positional_only(fct: Callable) -> bool:
args = list(signature(fct).parameters.values())
return (len(args) > 0 and # 1. We have one or more args
_is_positional(args[0]) and # 2. First is positional
all(_is_optional(a) for a in args[1:])) # 3. Others are optional
这将为您的测试用例返回正确的结果(我稍微扩展了一下):
def f_ok1_1param(v : str): pass
def f_ok2_1param(v): pass
def f_ok3_vargs(*v): pass
def f_ok4_p_vargs(p, *v): pass
def f_ok4_vargs_kwargs(*args, **kwargs): pass
def f_ok5_p_varg_kwarg(param,*args,**kwargs): pass
def f_ok6_pos_only(v, /): pass # also ok: explicitly positional only
def f_ok7_defaults(p, d=None): pass # also ok: with default value
def f_bad1_2params(p1, p2): pass
def f_bad2_kwargs(**kwargs): pass
def f_bad3_noparam(): pass
def f_bad4_kwarg_after_args(*args, v): pass # also not ok: v after *args is keyword-only
print(has_one_positional_only(f_ok1_1param)) # True
print(has_one_positional_only(f_ok2_1param)) # True
print(has_one_positional_only(f_ok3_vargs)) # True
print(has_one_positional_only(f_ok4_p_vargs)) # True
print(has_one_positional_only(f_ok5_p_varg_kwarg)) # True
print(has_one_positional_only(f_ok6_pos_only)) # True
print(has_one_positional_only(f_ok7_defaults)) # True
print(has_one_positional_only(f_bad1_2params)) # False
print(has_one_positional_only(f_bad2_kwargs)) # False
print(has_one_positional_only(f_bad3_noparam)) # False
print(has_one_positional_only(f_bad4_kwarg_after_args)) # False
最后两个想法:
**kwargs
然后可选的 *args
不能再跟了)。我在发布这个之后也写了我自己的功能。我已经从接受的答案中抄袭了默认处理(并且从
another answer进行了更清洁的
.kind
检查)但是我的方法是不同的,因为我知道没有required positionals following.
令人惊讶的复杂and难以概括。检查另一个签名的东西(这次说 2 个位置)将不得不重做大部分。
def _check_function_has_one_parameter(v_in : Callable) -> bool:
"""check that only NEED to provide one and only one positional"""
count = 0
for param in signature(v_in).parameters.values():
pkind = param.kind
#standard parameters
if pkind in (param.POSITIONAL_ONLY,param.POSITIONAL_OR_KEYWORD):
if param.default is param.empty or not count:
#first positional or any w.o. default
count += 1
else:
#positionals following will have also defaults
break
#BREAK as we know we are done with positionals once we hit these...
elif pkind == param.VAR_POSITIONAL:
#but first, we'll count it as a positional if its the first...
count = 1 if count == 0 else count
break
elif pkind in (param.KEYWORD_ONLY, param.VAR_KEYWORD):
break
else:
#shouldn't happen, but...
raise TypeError(f" unknown {pkind}")
return count == 1