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 程序员更容易进行函数内省。
签名对象
签名对象表示函数的调用签名及其返回注解。对于函数接受的每个参数,它都会在其 parameters
集合中存储一个 参数对象。
签名对象具有以下公共属性和方法
- return_annotation : object
- 函数的“返回”注解。如果函数没有“返回”注解,则此属性设置为
Signature.empty
。
- parameters : OrderedDict
- 参数名称到相应参数对象的顺序映射。
- 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.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)
- 默认签名构造函数。接受可选的
Parameter
对象序列和可选的return_annotation
。验证参数序列以检查是否存在名称重复的参数,以及参数是否按正确顺序排列,即位置限定参数优先,然后是位置或关键字参数,依此类推。
- Signature.from_function(function)
- 返回反映传入函数签名的 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'"
绑定参数对象
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。
设计考虑
不隐式缓存签名对象
第一个 PEP 设计中包含了一个在 inspect.signature()
函数中隐式缓存 Signature
对象的规定。但是,这有以下缺点
- 如果
Signature
对象被缓存,则对其描述的函数的任何更改都不会反映在其中。但是,如果需要缓存,则始终可以手动且显式地进行。 - 最好将
__signature__
属性保留用于需要显式设置为与实际不同的Signature
对象的情况。
某些函数可能无法内省
某些函数在某些 Python 实现中可能无法进行内省。例如,在 CPython 中,用 C 定义的内置函数不提供关于其参数的任何元数据。为其添加支持不在此 PEP 的范围内。
签名和参数等价性
我们假设参数名称具有语义意义——只有当两个签名对应的参数相等且具有完全相同的名称时,它们才相等。希望进行更宽松的等价性测试的用户(例如,忽略 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
上次修改时间:2023-09-09 17:39:29 GMT