Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 增强提案

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 对象。 namekind 是必需的,而 annotationdefault 是可选的。

当两个参数具有相同的名称、种类、默认值和注解时,它们是相等的。

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 结合使用,以进行任何参数处理。

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__ 方法,则为其返回一个 Signature。
    • 如果对象的类型在其 MRO 中定义了 __new__ 方法,则为其返回一个 Signature 对象。
    • 如果对象的类型在其 MRO 中定义了 __init__ 方法,则为其返回一个 Signature 对象。

  • 返回 signature(object.__call__)

请注意,Signature 对象是懒加载创建的,不会自动缓存。但是,用户可以通过将其存储在 __signature__ 属性中来手动缓存 Signature。

Python 3.3 的实现可以在 [1] 中找到。跟踪此补丁的 Python 问题是 [2]

设计考虑

不隐式缓存签名对象

第一个 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