Following system colour scheme - Python 增強提案 Selected dark colour scheme - Python 增強提案 Selected light colour scheme - Python 增強提案

Python 增強提案 (Python Enhancement Proposals)

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)
    實例化一個參數物件。namekind 為必要參數,而 annotationdefault 為選用參數。

當兩個參數的名稱、種類、預設值與註解皆相等時,它們即為相等。

參數物件是不可變的。與其修改參數物件,您可以使用 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 結合使用,以處理任何引數處理需求。

argskwargs 屬性可用於呼叫函式。

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 物件,並移除其第一個參數(通常是 selfcls)。(classmethodstaticmethod 也受到支援。由於兩者皆為描述器,前者回傳綁定方法,後者回傳其包裝的函式。)
  • 如果物件是 functools.partial 的實例,則根據其 partial.func 屬性建構新的 Signature,並計入已綁定的 partial.argspartial.kwargs
  • 如果物件是類別或元類別:
    • 如果物件的型別在其 MRO 中定義了 __call__ 方法,則回傳該方法的簽章。
    • 如果物件在其 MRO 中定義了 __new__ 方法,則回傳該方法的簽章物件。
    • 如果物件在其 MRO 中定義了 __init__ 方法,則回傳該方法的簽章物件。
  • 回傳 signature(object.__call__)

請注意,Signature 物件是以惰性方式建立的,並不會自動快取。然而,使用者可以透過將簽章儲存在 __signature__ 屬性中來手動快取它。

Python 3.3 的實作可參見 [1]。追蹤此修補程式的 Python 問題單為 [2]

設計考量

不對簽章物件進行隱含式快取

第一版 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)。

參考文獻


來源: https://github.com/python/peps/blob/main/peps/pep-0362.rst

最後修改: 2025-02-01 08:59:27 GMT