python 如何使用检查签名来检查一个函数是否需要一个且只有一个参数?

cvxl0en2  于 2023-04-28  发布在  Python
关注(0)|答案(2)|浏览(91)

我想验证(在运行时,这不是一个类型问题),作为参数传递的函数只需要1个位置变量(基本上,该函数将以字符串作为输入调用并返回一个truthy)。
天真地,这就是我所拥有的:

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

如果我检查下面的函数,它是OK的,这是好的:

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中还是typingprotocol支持是否可以这样做,* 但在运行时 *。
注意,理论上def f_ok_cuz_default(p, p2 = None):也可以,但我们暂时忽略它。
p.s.动机是在验证框架中提供自定义回调函数。调用位置位于框架的深处,并且该特定参数也可以是字符串(转换为正则表达式)。它甚至可以是没有。这里最简单的是贴一个def myfilter(*args,**kwargs): breakpoint。或者myfilter(foo)。然后看看你从框架中得到了什么,调整身体。函数中有异常是一回事,框架接受它,但在调用它之前出错是另一回事。所以我问你“我们叫它的时候它能用吗?”更方便用户使用。

ruyhziif

ruyhziif1#

我不认为你的问题是微不足道的。我不知道任何给定的实现,所以我跟随你的思路并得出结论,最终,问题归结为回答以下问题:
1.可调用对象有任何参数吗?
1.如果是,那么第一个参数是 * 位置 * 吗?
1.如果是,所有其他参数都是 * 可选 * 的吗?
如果你对所有问题的回答都是yes,那么你的函数可以用一个(位置)参数调用,否则不能。在这方面,

    • 位置 * 表示:
  • 单个位置或关键字参数,如f(a),或
  • 一个仅位置参数,如f(a, /),或
  • 位置参数的变量列表,如f(*args);
    • 可选 * 表示:
  • 位置参数的变量列表,如f(a, *args),或
  • 关键字参数的变量Map,如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
def f_bad5_kwarg_only(*, v): pass  # also not ok: explicitly 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
print(has_one_positional_only(f_bad5_kwarg_only))        # False

最后两个想法:
1.首先,我认为第二个问题需要更一般地表述,即:any 参数是位置性的吗?然而,我没有提出任何组合,其中第一个参数不一定是位置参数(根据给定的 positional 定义),并且我很有信心这在当前的Python语法规则中是不可能的。
1.这个解决方案肯定不是最有效的,考虑到你已经检查过的参数的知识,某些其他的参数是不可能遵循的,因此实际上不需要再次检查(例如:例如,如果第一个可选参数是**kwargs,则不能再跟随可选的*args)。

更新以澄清:所提供的解决方案将在某种意义上“工作”,它检查给定可调用对象的 * 签名 * 是否与仅用一个位置参数调用它兼容。它 * 不能 * 确保的是一个给定的位置参数满足可调用的 * 内部逻辑 *。例如,如果f(*args)在内部尝试访问args中第一个元素之后的元素,如果只使用一个参数调用,即使建议的检查允许它通过,它也会失败。除了运行可调用对象或检查其实际代码之外,这种方式从外部检查内部逻辑的工作并不多。(此外,据我理解,问题并没有问及后者。)

xmjla07d

xmjla07d2#

我也写了我自己的函数后,张贴这一点。我已经从接受的答案中复制了默认处理(并且从another answer中复制了更干净的.kind检查),但我的方法不同,我一旦知道后面没有 * 必需的位置 *,就立即退出检查循环。
令人惊讶的复杂 * 和 * 难以概括。检查另一个签名(比如这次是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

相关问题