PEP 362 – 函数签名对象
- 作者:
- Brett Cannon <brett at python.org>,Jiwon Seo <seojiwon at gmail.com>,Yury Selivanov <yury at edgedb.com>,Larry Hastings <larry at hastings.org>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2006年8月21日
- Python 版本:
- 3.3
- 发布历史:
- 2012年6月4日
- 决议:
- Python-Dev 消息
摘要
Python 一直支持强大的内省能力,包括内省函数和方法(在本 PEP 的其余部分,“函数”指函数和方法)。通过检查函数对象,您可以完全重建函数的签名。不幸的是,这些信息以不方便的方式存储,并且分散在半打深层嵌套的属性中。
本 PEP 提出了一种新的函数签名表示方式。新的表示方式包含有关函数及其参数的所有必要信息,并使内省变得轻松直接。
然而,此对象不会取代现有的函数元数据,该元数据由 Python 自身用于执行这些函数。新的元数据对象仅旨在使 Python 程序员更容易进行函数内省。
签名对象
Signature 对象表示函数的调用签名及其返回注解。对于函数接受的每个参数,它都会在其 parameters 集合中存储一个 Parameter 对象。
Signature 对象具有以下公共属性和方法
- return_annotation : object
- 函数的“返回”注解。如果函数没有“返回”注解,此属性将设置为
Signature.empty。
- parameters : OrderedDict
- 参数名称到相应 Parameter 对象的有序映射。
- bind(*args, **kwargs) -> BoundArguments
- 创建从位置参数和关键字参数到参数的映射。如果传入的参数与签名不匹配,则引发
TypeError。
- bind_partial(*args, **kwargs) -> BoundArguments
- 工作方式与
bind()相同,但允许省略某些必需参数(模仿functools.partial的行为)。如果传入的参数与签名不匹配,则引发TypeError。
- replace(parameters=<optional>, *, return_annotation=<optional>) -> Signature
- 根据调用
replace的实例创建新的 Signature 实例。可以传入不同的parameters和/或return_annotation来覆盖基本签名的相应属性。要从复制的Signature中删除return_annotation,请传入Signature.empty。请注意,“=<optional>”表示该参数是可选的。此表示法适用于本 PEP 的其余部分。
Signature 对象是不可变的。使用 Signature.replace() 来创建修改后的副本
>>> def foo() -> None:
... pass
>>> sig = signature(foo)
>>> new_sig = sig.replace(return_annotation="new return annotation")
>>> new_sig is not sig
True
>>> new_sig.return_annotation != sig.return_annotation
True
>>> new_sig.parameters == sig.parameters
True
>>> new_sig = new_sig.replace(return_annotation=new_sig.empty)
>>> new_sig.return_annotation is Signature.empty
True
有两种方法可以实例化 Signature 类
- Signature(parameters=<optional>, *, return_annotation=Signature.empty)
- 默认 Signature 构造函数。接受一个可选的
Parameter对象序列和一个可选的return_annotation。参数序列经过验证,以检查是否存在具有重复名称的参数,以及参数是否按正确顺序排列,即先位置参数,然后位置或关键字参数,等等。
- Signature.from_function(function)
- 返回一个 Signature 对象,反映传入函数的签名。
可以测试 Signatures 的相等性。当它们的参数相等,它们的位置参数和纯位置参数以相同的顺序出现,并且它们具有相等的返回注解时,两个签名相等。
对 Signature 对象或其任何数据成员的更改不会影响函数本身。
Signature 也实现了 __str__
>>> str(Signature.from_function((lambda *args: None)))
'(*args)'
>>> str(Signature())
'()'
参数对象
Python 富有表现力的语法意味着函数可以接受许多不同种类的参数,并具有许多微妙的语义差异。我们提出了一个丰富的 Parameter 对象,旨在表示任何可能的函数参数。
Parameter 对象具有以下公共属性和方法
- name : str
- 参数的名称,为字符串。必须是有效的 Python 标识符名称(
POSITIONAL_ONLY参数除外,其可以设置为None)。
- default : object
- 参数的默认值。如果参数没有默认值,此属性将设置为
Parameter.empty。
- annotation : object
- 参数的注解。如果参数没有注解,此属性将设置为
Parameter.empty。
- kind
- 描述参数值如何绑定到参数。可能的值
Parameter.POSITIONAL_ONLY- 值必须作为位置参数提供。Python 没有定义纯位置参数的显式语法,但许多内置和扩展模块函数(尤其是那些只接受一两个参数的函数)接受它们。
Parameter.POSITIONAL_OR_KEYWORD- 值可以作为关键字参数或位置参数提供(这是用 Python 实现的函数的标准绑定行为)。Parameter.KEYWORD_ONLY- 值必须作为关键字参数提供。纯关键字参数是指在 Python 函数定义中出现在“*”或“*args”条目之后的参数。Parameter.VAR_POSITIONAL- 未绑定到任何其他参数的位置参数元组。这对应于 Python 函数定义中的“*args”参数。Parameter.VAR_KEYWORD- 未绑定到任何其他参数的关键字参数字典。这对应于 Python 函数定义中的“**kwargs”参数。
始终使用
Parameter.*常量来设置和检查kind属性的值。
- replace(*, name=<optional>, kind=<optional>, default=<optional>, annotation=<optional>) -> Parameter
- 根据调用
replaced的实例创建新的 Parameter 实例。要覆盖 Parameter 属性,请传入相应的参数。要从Parameter中删除属性,请传入Parameter.empty。
Parameter 构造函数
- Parameter(name, kind, *, annotation=Parameter.empty, default=Parameter.empty)
- 实例化 Parameter 对象。
name和kind是必需的,而annotation和default是可选的。
当两个参数具有相等的名称、种类、默认值和注解时,它们相等。
Parameter 对象是不可变的。您可以使用 Parameter.replace() 创建修改后的副本,而不是修改 Parameter 对象,如下所示
>>> param = Parameter('foo', Parameter.KEYWORD_ONLY, default=42)
>>> str(param)
'foo=42'
>>> str(param.replace())
'foo=42'
>>> str(param.replace(default=Parameter.empty, annotation='spam'))
"foo:'spam'"
BoundArguments 对象
Signature.bind 调用的结果。保存参数到函数参数的映射。
具有以下公共属性
- arguments : OrderedDict
- 参数名称到参数值的有序、可变映射。只包含显式绑定的参数。对于
bind()依赖默认值的参数会被跳过。
- args : tuple
- 位置参数值的元组。从“arguments”属性动态计算。
- kwargs : dict
- 关键字参数值的字典。从“arguments”属性动态计算。
arguments 属性应与 Signature.parameters 结合使用,以进行任何参数处理。
args 和 kwargs 属性可用于调用函数
def test(a, *, b):
...
sig = signature(test)
ba = sig.bind(10, b=20)
test(*ba.args, **ba.kwargs)
可以作为 *args 或 **kwargs 的一部分传递的参数将仅包含在 BoundArguments.args 属性中。请看以下示例
def test(a=1, b=2, c=3):
pass
sig = signature(test)
ba = sig.bind(a=10, c=13)
>>> ba.args
(10,)
>>> ba.kwargs:
{'c': 13}
实施
此实现向 inspect 模块添加了一个新函数 signature()。该函数是获取可调用对象的 Signature 的首选方式。
该函数实现了以下算法
- 如果对象不可调用 - 抛出 TypeError
- 如果对象具有
__signature__属性且不为None- 返回它 - 如果它具有
__wrapped__属性,返回signature(object.__wrapped__) - 如果对象是
FunctionType的实例,则为其构造并返回一个新的Signature - 如果对象是绑定方法,则构造并返回一个新的
Signature对象,并删除其第一个参数(通常是self或cls)。(也支持classmethod和staticmethod。由于两者都是描述符,前者返回绑定方法,后者返回其包装函数。) - 如果对象是
functools.partial的实例,则从其partial.func属性构造一个新的Signature,并考虑已绑定的partial.args和partial.kwargs - 如果对象是类或元类
- 如果对象的类型在其 MRO 中定义了
__call__方法,则返回其 Signature - 如果对象在其 MRO 中定义了
__new__方法,则返回其 Signature 对象 - 如果对象在其 MRO 中定义了
__init__方法,则返回其 Signature 对象
- 如果对象的类型在其 MRO 中定义了
- 返回
signature(object.__call__)
请注意,Signature 对象以惰性方式创建,并且不会自动缓存。但是,用户可以通过将其存储在 __signature__ 属性中来手动缓存 Signature。
设计考虑
不隐式缓存 Signature 对象
第一个 PEP 设计提供了在 inspect.signature() 函数中隐式缓存 Signature 对象的规定。然而,这有以下缺点
- 如果
Signature对象被缓存,那么对它描述的函数的任何更改都不会反映在其中。但是,如果需要缓存,则可以始终手动和显式地完成 - 最好将
__signature__属性保留用于需要显式设置为与实际 Signature 对象不同的Signature对象的情况
某些函数可能无法内省
在 Python 的某些实现中,某些函数可能无法内省。例如,在 CPython 中,C 中定义的内置函数不提供关于其参数的元数据。为它们添加支持超出了本 PEP 的范围。
Signature 和 Parameter 等价性
我们假设参数名称具有语义意义——只有当它们的相应参数相等且具有完全相同的名称时,两个签名才相等。希望进行更宽松的等价测试(例如忽略 VAR_KEYWORD 或 VAR_POSITIONAL 参数的名称)的用户需要自己实现这些。
示例
可视化可调用对象的签名
让我们定义一些类和函数
from inspect import signature
from functools import partial, wraps
class FooMeta(type):
def __new__(mcls, name, bases, dct, *, bar:bool=False):
return super().__new__(mcls, name, bases, dct)
def __init__(cls, name, bases, dct, **kwargs):
return super().__init__(name, bases, dct)
class Foo(metaclass=FooMeta):
def __init__(self, spam:int=42):
self.spam = spam
def __call__(self, a, b, *, c) -> tuple:
return a, b, c
@classmethod
def spam(cls, a):
return a
def shared_vars(*shared_args):
"""Decorator factory that defines shared variables that are
passed to every invocation of the function"""
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
full_args = shared_args + args
return f(*full_args, **kwargs)
# Override signature
sig = signature(f)
sig = sig.replace(tuple(sig.parameters.values())[1:])
wrapper.__signature__ = sig
return wrapper
return decorator
@shared_vars({})
def example(_state, a, b, c):
return _state, a, b, c
def format_signature(obj):
return str(signature(obj))
现在,在 python REPL 中
>>> format_signature(FooMeta)
'(name, bases, dct, *, bar:bool=False)'
>>> format_signature(Foo)
'(spam:int=42)'
>>> format_signature(Foo.__call__)
'(self, a, b, *, c) -> tuple'
>>> format_signature(Foo().__call__)
'(a, b, *, c) -> tuple'
>>> format_signature(Foo.spam)
'(a)'
>>> format_signature(partial(Foo().__call__, 1, c=3))
'(b, *, c=3) -> tuple'
>>> format_signature(partial(partial(Foo().__call__, 1, c=3), 2, c=20))
'(*, c=20) -> tuple'
>>> format_signature(example)
'(a, b, c)'
>>> format_signature(partial(example, 1, 2))
'(c)'
>>> format_signature(partial(partial(example, 1, b=2), c=3))
'(b=2, c=3)'
注解检查器
import inspect
import functools
def checktypes(func):
'''Decorator to verify arguments and return types
Example:
>>> @checktypes
... def test(a:int, b:str) -> int:
... return int(a * b)
>>> test(10, '1')
1111111111
>>> test(10, 1)
Traceback (most recent call last):
...
ValueError: foo: wrong type of 'b' argument, 'str' expected, got 'int'
'''
sig = inspect.signature(func)
types = {}
for param in sig.parameters.values():
# Iterate through function's parameters and build the list of
# arguments types
type_ = param.annotation
if type_ is param.empty or not inspect.isclass(type_):
# Missing annotation or not a type, skip it
continue
types[param.name] = type_
# If the argument has a type specified, let's check that its
# default value (if present) conforms with the type.
if param.default is not param.empty and not isinstance(param.default, type_):
raise ValueError("{func}: wrong type of a default value for {arg!r}". \
format(func=func.__qualname__, arg=param.name))
def check_type(sig, arg_name, arg_type, arg_value):
# Internal function that encapsulates arguments type checking
if not isinstance(arg_value, arg_type):
raise ValueError("{func}: wrong type of {arg!r} argument, " \
"{exp!r} expected, got {got!r}". \
format(func=func.__qualname__, arg=arg_name,
exp=arg_type.__name__, got=type(arg_value).__name__))
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Let's bind the arguments
ba = sig.bind(*args, **kwargs)
for arg_name, arg in ba.arguments.items():
# And iterate through the bound arguments
try:
type_ = types[arg_name]
except KeyError:
continue
else:
# OK, we have a type for the argument, lets get the corresponding
# parameter description from the signature object
param = sig.parameters[arg_name]
if param.kind == param.VAR_POSITIONAL:
# If this parameter is a variable-argument parameter,
# then we need to check each of its values
for value in arg:
check_type(sig, arg_name, type_, value)
elif param.kind == param.VAR_KEYWORD:
# If this parameter is a variable-keyword-argument parameter:
for subname, value in arg.items():
check_type(sig, arg_name + ':' + subname, type_, value)
else:
# And, finally, if this parameter a regular one:
check_type(sig, arg_name, type_, arg)
result = func(*ba.args, **ba.kwargs)
# The last bit - let's check that the result is correct
return_type = sig.return_annotation
if (return_type is not sig._empty and
isinstance(return_type, type) and
not isinstance(result, return_type)):
raise ValueError('{func}: wrong return type, {exp} expected, got {got}'. \
format(func=func.__qualname__, exp=return_type.__name__,
got=type(result).__name__))
return result
return wrapper
接受
PEP 362 于 2012 年 6 月 22 日星期五被 Guido 接受 [3]。参考实现于当天晚些时候提交到主干。
参考资料
版权
本文档已置于公共领域。
来源: https://github.com/python/peps/blob/main/peps/pep-0362.rst
最后修改: 2025-02-01 08:59:27 GMT