如何使用 inspect.signature 检查一个函数是否只需要一个参数?

问题描述 投票:0回答:2

我想验证(在运行时,这不是打字问题),作为参数传递的函数只接受 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)
。然后看看你从框架中得到了什么,并调整身体。在函数中有异常是一回事,框架接受它但是在调用它之前出错是另一回事。所以快速“当我们调用它时它会起作用吗?”对用户更友好。

python signature introspection
2个回答
2
投票

我不认为你的问题是微不足道的。而且我不知道任何给定的实现,所以我按照你的思路得出的结论是,问题最终归结为回答以下问题:

  1. 可调用对象有任何参数吗?
  2. 如果是这样,第一个参数是positional吗?
  3. 如果是这样,所有其他参数都是可选吗?

如果您对所有问题的回答都是肯定的,那么您的 函数可以只用一个参数调用,否则不能。

在这种情况下,

  • positional 意思是:
    • 单个位置或关键字参数,如
      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

最后两个想法:

  1. 起初我认为问题需要更笼统地表述,即:(2)any argument positional吗?但是,我没有想出任何不是第一个参数必须是位置参数的组合(根据 positional 的给定定义),我非常相信当前的 Python 不可能语法规则。
  2. 肯定的解决方案不是最有效的解决方案,因为有了您已经检查过的参数的知识,某些其他参数是不可能遵循的,因此实际上不需要再次检查(例如,如果第一个可选参数是
    **kwargs
    然后可选的
    *args
    不能再跟了)。

0
投票

我在发布这个之后也写了我自己的功能。我已经从接受的答案中抄袭了默认处理(并且从

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 

© www.soinside.com 2019 - 2024. All rights reserved.