Python Signature.bind:参数能对上函数签名,为什么仍不代表值合格

10-01 4阅读

先检查怎么调用,再决定是否执行

工具分发器或任务入口拿到一组参数时,常常希望先知道它能否对应目标函数:必填项缺了吗,同一个参数给了两次吗,只能按位置传入的参数被写成关键字了吗。inspect.signature 得到的 Signature 对象可以用 bind 做这类检查,生成参数名到传入值的映射,而不执行函数体。

本文在 Python 3.12.14 上验证,代码保存为 demo.py 后运行即可。函数 render 接受只能按位置传入的 name、带默认值的 count,以及只能按关键字传入的 suffix。它每次真正执行都会在 calls 列表追加记录,方便证明前面的绑定步骤并没有偷偷运行函数。

默认值需要明确补入映射

bind("A", suffix="?") 成功后,arguments 只包含实际绑定的 name 和 suffix,count 尚未写入。调用 apply_defaults 后,映射才明确出现 count: 1。这个步骤适合需要展示完整调用参数或生成日志的入口,但它不会完成业务校验,也不会把字符串数字自动转换成整数。

Python Signature.bind:参数能对上函数签名,为什么仍不代表值合格

AI概念示意图:参数先进入形状匹配的槽位,默认项补齐空位,函数齿轮尚未运转。图片用于解释绑定过程,不是程序界面截图。

import inspect

calls = []
def render(name, /, count: int = 1, *, suffix="!"):
    calls.append(name)
    return (name + suffix) * count

signature = inspect.signature(render)
bound = signature.bind("A", suffix="?")
assert bound.arguments == {"name": "A", "suffix": "?"}
bound.apply_defaults()
assert bound.arguments == {"name": "A", "count": 1, "suffix": "?"}
print("bound:", bound.arguments)

bad_calls = [
    ((), {}),
    ((), {"name": "A"}),
    (("A", 2), {"count": 3}),
]
failures = 0
for args, kwargs in bad_calls:
    try:
        signature.bind(*args, **kwargs)
    except TypeError:
        failures += 1
    else:
        raise AssertionError("invalid call shape accepted")
assert failures == 3
wrong_type = signature.bind("A", count="two")
assert wrong_type.arguments["count"] == "two"
assert calls == []
print("failed shapes:", failures)
print("unchecked count:", wrong_type.arguments["count"])
print("calls before invoke:", len(calls))
result = render(*bound.args, **bound.kwargs)
assert result == "A?" and calls == ["A"]
print("result:", result)
print("calls after invoke:", len(calls))

缺少参数与缺少类型校验是两件事

三个失败样本分别省略必填 name、把位置专用参数写成 name="A",以及给 count 同时传入位置值与关键字值。它们都在 bind 阶段触发 TypeError;示例只断言异常类型,不依赖不同 Python 版本可能略有变化的错误文字。failed shapes: 3 和 calls before invoke: 0 共同说明这些检查没有执行函数体。

随后把 count 传成字符串 two,绑定仍然成功,尽管函数定义写着 count: int。bind 处理的是参数形状,不负责根据类型标注拒绝值。代码故意不执行这个错误类型的调用,而是打印它已经进入绑定映射的事实,提醒入口还需要独立的类型、范围及业务规则检查。

例如数量除了必须是整数,还可能要求大于零且不超过一次任务的上限。这些条件无法从参数位置关系推导出来,应该按领域规则单独校验,并在错误信息里指出具体不满足的条件。

真正执行仍然是单独的一步

最后使用 render(*bound.args, **bound.kwargs) 才执行此前准备好的合法调用,结果是 A?,调用记录也从零增加到一。BoundArguments 提供的 args 和 kwargs 根据当前绑定映射产生,适合按同一组参数转发;如果后续修改了 arguments,就应重新审视值是否仍满足业务要求,而不是把最初绑定成功当成永久通行证。

bind_partial 允许暂时省略某些必填参数,适用于分阶段收集参数,但不能直接替代完整调用前的 bind。设计入口时,可以把“参数还没收齐”和“调用已经准备好”明确区分,避免错误地放行缺项任务。另一方面,bind 也不是执行授权:是否允许调用某个函数,以及调用会产生什么影响,必须由入口自己的规则决定。

这个例子只处理已知的普通 Python 函数。有些内建或扩展可调用对象不能完整提供签名信息,包装器也可能影响获取到的签名,调用方应处理获取失败。把样本中的必填缺失、重复赋值和位置限制保留为验收例,再加上领域值检查,才能说明一个任务入口到底检查了哪些条件,哪些仍由实际执行负责。

参考资料

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。