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>
- 狀態:
- 最終 (Final)
- 類型:
- 標準軌跡 (Standards Track)
- 建立日期:
- 2006年8月21日
- Python 版本:
- 3.3
- 公告歷史:
- 2012年6月4日
- 決議:
- Python-Dev 訊息
摘要
Python 一直以來都支援強大的內省功能,包括對函式和方法進行內省(在本 PEP 的後續內容中,「函式」一詞同時涵蓋函式與方法)。透過檢查函式物件,您可以完整重建該函式的簽章。遺憾的是,這些資訊儲存的方式並不方便,且散佈在半打以上深度巢狀的屬性中。
本 PEP 提出了一種新的函式簽章表示法。這種新的表示法包含了關於函式及其參數的所有必要資訊,並使內省變得簡單直接。
然而,此物件並未取代現有的函式中繼資料,後者仍由 Python 本身用於執行這些函式。新的中繼資料物件僅旨在讓 Python 程式設計師更容易進行函式內省。
簽章物件 (Signature Object)
簽章(Signature)物件代表函式的呼叫簽章及其回傳註解。對於函式所接受的每個參數,它都會在 parameters 集合中儲存一個 參數物件 (Parameter object)。
簽章物件具有以下公開屬性與方法:
- 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(parameters=<optional>, *, return_annotation=Signature.empty)
- 預設簽章建構子。接受一個選用的
Parameter物件序列,以及一個選用的return_annotation。參數序列會經過驗證,檢查是否有重複的參數名稱,並確保參數順序正確,例如:僅限位置參數在前,接著是位置或關鍵字參數,依此類推。
- Signature.from_function(function)
- 回傳一個反映所傳入函式簽章的簽章物件。
可以測試簽章是否相等。當兩個簽章的參數相等、位置參數與僅限位置參數以相同順序出現,且具有相等的回傳註解時,它們即為相等。
對簽章物件或其任何資料成員的變更,不會影響函式本身。
簽章也實作了 __str__。
>>> str(Signature.from_function((lambda *args: None)))
'(*args)'
>>> str(Signature())
'()'
參數物件 (Parameter Object)
Python 富有表現力的語法意味著函式可以接受許多不同類型的參數,並具有許多細微的語意差異。我們提出了一個功能豐富的參數物件,旨在表示任何可能的函式參數。
參數物件具有以下公開屬性與方法:
- 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” 參數。
設定和檢查
kind屬性的值時,請務必使用Parameter.*常數。
- replace(*, name=<optional>, kind=<optional>, default=<optional>, annotation=<optional>) -> Parameter
- 根據呼叫
replace的實例建立一個新的 Parameter 實例。若要覆蓋參數屬性,請傳入對應的引數。若要從Parameter中移除屬性,請傳入Parameter.empty。
參數建構子
- Parameter(name, kind, *, annotation=Parameter.empty, default=Parameter.empty)
- 實例化一個參數物件。
name與kind為必要參數,而annotation與default為選用參數。
當兩個參數的名稱、種類、預設值與註解皆相等時,它們即為相等。
參數物件是不可變的。與其修改參數物件,您可以使用 Parameter.replace() 來建立修改後的副本,如下所示:
>>> 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 Object)
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__方法,則回傳該方法的簽章。 - 如果物件在其 MRO 中定義了
__new__方法,則回傳該方法的簽章物件。 - 如果物件在其 MRO 中定義了
__init__方法,則回傳該方法的簽章物件。
- 如果物件的型別在其 MRO 中定義了
- 回傳
signature(object.__call__)。
請注意,Signature 物件是以惰性方式建立的,並不會自動快取。然而,使用者可以透過將簽章儲存在 __signature__ 屬性中來手動快取它。
設計考量
不對簽章物件進行隱含式快取
第一版 PEP 設計在 inspect.signature() 函式中預留了 Signature 物件的隱含式快取機制。然而,這具有以下缺點:
- 如果快取了
Signature物件,則對該物件所描述函式的任何變更都不會反映出來。不過,若有快取需求,隨時可以手動且顯式地進行。 - 最好將
__signature__屬性保留給那些需要顯式設定與實際簽章不同的Signature物件的情況。
某些函式可能無法進行內省 (Introspection)
某些函式在特定的 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)'
註解檢查器 (Annotation Checker)
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]。參考實作於當日稍後提交至主幹 (trunk)。
參考文獻
版權
此文件已歸入公有領域 (public domain)。
來源: https://github.com/python/peps/blob/main/peps/pep-0362.rst
最後修改: 2025-02-01 08:59:27 GMT