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

Python 增强提案

PEP 737 – C API 用于格式化类型的完全限定名称

作者:
Victor Stinner <vstinner at python.org>
讨论列表:
Discourse 讨论线程
状态:
最终版
类型:
标准跟踪
创建日期:
2023年11月29日
Python 版本:
3.13
历史记录:
2023年11月29日
决议:
Discourse 消息

目录

摘要

添加新的方便的 C API 来格式化类型的完全限定名称。不再根据类型的实现方式以不同的方式格式化类型名称。

建议在新 C 代码的错误消息和 __repr__() 方法中使用类型的完全限定名称。建议在新 C 代码中不要截断类型名称。

PyUnicode_FromFormat() 添加 %T%#T%N%#N 格式,分别用于格式化对象的类型和类型的完全限定名称。

通过避免可能导致崩溃的借用引用,使 C 代码更安全。新的 C API 与有限的 C API 兼容。

基本原理

标准库

在 Python 标准库中,格式化类型名称或对象的类型名称是格式化错误消息和实现 __repr__() 方法的常见操作。有多种格式化类型名称的方法,它们会产生不同的输出。

使用 datetime.timedelta 类型的示例

  • 类型的短名称 (type.__name__) 和类型的限定名称 (type.__qualname__) 都是 'timedelta'
  • 类型的模块 (type.__module__) 是 'datetime'
  • 类型的完全限定名称是 'datetime.timedelta'
  • 类型的表示形式 (repr(type)) 包含完全限定名称:<class 'datetime.timedelta'>

Python 代码

在 Python 中,type.__name__ 获取类型的短名称,而 f"{type.__module__}.{type.__qualname__}" 格式化类型的“完全限定名称”。通常,type(obj)obj.__class__ 用于获取对象 obj 的类型。有时,类型名称用引号括起来。

示例

  • raise TypeError("str expected, not %s" % type(value).__name__)
  • raise TypeError("can't serialize %s" % self.__class__.__name__)
  • name = "%s.%s" % (obj.__module__, obj.__qualname__)

限定名称是在 Python 3.3 中由 PEP 3155“类和函数的限定名称”添加到类型 (type.__qualname__) 中的。

C 代码

在 C 中,格式化类型名称最常见的方法是获取类型的 PyTypeObject.tp_name 成员。示例

PyErr_Format(PyExc_TypeError, "globals must be a dict, not %.100s",
             Py_TYPE(globals)->tp_name);

类型的“完全限定名称”在少数几个地方使用:PyErr_Display()type.__repr__() 实现和 sys.unraisablehook 实现。

首选使用 Py_TYPE(obj)->tp_name,因为它比调用 PyType_GetQualName() 更方便,后者需要 Py_DECREF()。此外,PyType_GetQualName() 最近才添加到 Python 3.11 中。

某些函数使用 %R (repr(type)) 来格式化类型名称,输出包含类型的完全限定名称。示例

PyErr_Format(PyExc_TypeError,
             "calling %R should have returned an instance "
             "of BaseException, not %R",
             type, Py_TYPE(value));

使用 PyTypeObject.tp_name 与 Python 不一致

PyTypeObject.tp_name 成员根据类型的实现方式而有所不同

  • C 中的静态类型和堆类型:tp_name 是类型的完全限定名称。
  • Python 类:tp_name 是类型的短名称 (type.__name__)。

因此,使用 Py_TYPE(obj)->tp_name 来格式化对象类型名称会根据类型是在 C 中还是在 Python 中实现而产生不同的输出。

这违反了 PEP 399“纯 Python/C 加速器模块兼容性要求”原则,该原则建议代码在 Python 或 C 中编写时行为相同。

示例

$ python3.12
>>> import _datetime; c_obj = _datetime.date(1970, 1, 1)
>>> import _pydatetime; py_obj = _pydatetime.date(1970, 1, 1)
>>> my_list = list(range(3))

>>> my_list[c_obj]  # C type
TypeError: list indices must be integers or slices, not datetime.date

>>> my_list[py_obj]  # Python type
TypeError: list indices must be integers or slices, not date

如果类型在 C 中实现,则错误消息包含类型的完全限定名称 (datetime.date),如果类型在 Python 中实现,则包含类型的短名称 (date)。

有限的 C API

Py_TYPE(obj)->tp_name 代码不能与有限的 C API 一起使用,因为 PyTypeObject 成员被排除在有限的 C API 之外。

类型名称应使用 PyType_GetName()PyType_GetQualName()PyType_GetModule() 函数读取,这些函数使用起来不太方便。

在 C 中截断类型名称

1998 年,在添加 PyErr_Format() 函数时,实现使用了 500 字节的固定缓冲区。该函数有以下注释

/* Caller is responsible for limiting the format */

2001 年,该函数被修改为在堆上分配动态缓冲区。为时已晚,截断类型名称的做法,例如使用 %.100s 格式,已经成为一种习惯,开发人员忘记了为什么类型名称会被截断。在 Python 中,类型名称不会被截断。

在 C 中截断类型名称但在 Python 中不截断违反了 PEP 399“纯 Python/C 加速器模块兼容性要求”原则,该原则建议代码在 Python 或 C 中编写时行为相同。

请参阅问题:在 PyErr_Format() 中用 %s 替换 %.100s:500 字节的任意限制已过时 (2011)。

规范

  • 添加 PyType_GetFullyQualifiedName() 函数。
  • 添加 PyType_GetModuleName() 函数。
  • PyUnicode_FromFormat() 添加格式。
  • 建议在新 C 代码的错误消息和 __repr__() 方法中使用类型的完全限定名称。
  • 建议在新 C 代码中不要截断类型名称。

添加 PyType_GetFullyQualifiedName() 函数

添加 PyType_GetFullyQualifiedName() 函数以获取类型的完全限定名称:类似于 f"{type.__module__}.{type.__qualname__}",或者如果 type.__module__ 不是字符串或等于 "builtins" 或等于 "__main__",则类似于 type.__qualname__

API

PyObject* PyType_GetFullyQualifiedName(PyTypeObject *type)

成功时,返回字符串的新引用。发生错误时,引发异常并返回 NULL

添加 PyType_GetModuleName() 函数

添加 PyType_GetModuleName() 函数以获取类型的模块名称 (type.__module__ 字符串)。API

PyObject* PyType_GetModuleName(PyTypeObject *type)

成功时,返回字符串的新引用。发生错误时,引发异常并返回 NULL

向 PyUnicode_FromFormat() 添加格式

PyUnicode_FromFormat() 添加以下格式

  • %N 格式化类型完全限定名称,类似于 PyType_GetFullyQualifiedName(type)N 代表类型Name。
  • %T 格式化对象的类型完全限定名称,类似于 PyType_GetFullyQualifiedName(Py_TYPE(obj))T 代表对象Type。
  • %#N%#T:替代形式使用冒号分隔符 (:) 而不是点分隔符 (.) 来分隔模块名称和限定名称。

例如,使用 tp_name 的现有代码

PyErr_Format(PyExc_TypeError,
             "__format__ must return a str, not %.200s",
             Py_TYPE(result)->tp_name);

可以用 %T 格式替换

PyErr_Format(PyExc_TypeError,
             "__format__ must return a str, not %T", result);

更新代码的优势

  • 更安全的 C 代码:避免使用 Py_TYPE(),它返回借用引用。
  • PyTypeObject.tp_name 成员不再被显式读取:代码变得与有限的 C API 兼容。
  • 格式化类型名称不再依赖于类型实现。
  • 类型名称不再被截断。

注意:%T 格式由 time.strftime() 使用,但不由 printf() 使用。

格式汇总

C 对象 C 类型 格式
%T %N 类型**完全限定**名称。
%#T %#N 类型**完全限定**名称,使用**冒号**分隔符。

建议使用类型的完全限定名称

在错误消息和新 C 代码中的 __repr__() 方法中,建议使用类型的完全限定名称。

在非简单应用程序中,可能会有两个类型在两个不同的模块中定义了相同的短名称,尤其是在使用通用名称时。使用完全限定名称有助于以明确的方式识别类型。

建议不要截断类型名称

新的 C 代码中不应截断类型名称。例如,应避免使用 %.100s 格式:而应使用 %s 格式(或 C 中的 %T 格式)。

实现

向后兼容性

本 PEP 中提出的更改是向后兼容的。

添加新的 C API 对向后兼容性没有影响。现有的 C API 保持不变。没有更改 Python API。

仅建议在新 C 代码中用类型的完全限定名称替换类型短名称。仅建议在新 C 代码中不再截断类型名称。现有代码应保持不变,因此仍然向后兼容。对于 Python 代码没有建议。

被拒绝的想法

添加 type.__fully_qualified_name__ 属性

添加 type.__fully_qualified_name__ 只读属性,它是类型的完全限定名称:类似于 f"{type.__module__}.{type.__qualname__}",或者如果 type.__module__ 不是字符串或等于 "builtins" 或等于 "__main__",则类似于 type.__qualname__

type.__repr__() 保持不变,它仅在模块等于 "builtins" 时省略模块。

此更改被指导委员会拒绝

我们可以看到 PEP 提出的 C API 更改的实用性,并且可能会按原样接受这些更改。

我们认为 Python 级别更改的理由较少。我们尤其质疑 __fully_qualified_name__ 的必要性。

Thomas Wouters 添加

如果真的希望以与 C API 完全相同的方式格式化类型,对我个人而言,实用函数比 type.__format__ 更合理,但我认为 SC 在获得一些具体用例的情况下可能会被说服。

添加 type.__format__() 方法

添加 type.__format__() 方法,并使用以下格式

  • N 格式化类型的**完全限定名称**(type.__fully_qualified_name__);N 代表**N**ame(名称)。
  • #N(替代形式)格式化类型的**完全限定名称**,使用**冒号**(:)分隔符,而不是模块名称和限定名称之间的点分隔符(.)。

使用 f-string 的示例

>>> import datetime
>>> f"{datetime.timedelta:N}"  # fully qualified name
'datetime.timedelta'
>>> f"{datetime.timedelta:#N}" # fully qualified name, colon separator
'datetime:timedelta'

当您想导入名称时,#N 格式使用的冒号(:)分隔符消除了猜测,请参阅 pkgutil.resolve_name()python -m inspect 命令行界面和 setuptools 入口点。

此更改被指导委员会拒绝

更改 str(type)

type.__str__() 方法可以修改为以不同的方式格式化类型名称。例如,它可以返回类型的完全限定名称。

问题在于这是一个向后不兼容的更改。例如,标准库的 enumfunctoolsoptparsepdbxmlrpc.server 模块必须更新。test_dataclassestest_descrtuttest_cmd_line_script 测试也必须更新。

请参阅pull request:type(str) 返回完全限定名称

添加 !t 格式化程序以获取对象类型

使用 f"{obj!t:T}" 格式化 type(obj).__fully_qualified_name__,类似于 f"{type(obj):T}"

当 2018 年提出 !t 格式化程序时,Eric Smith 强烈反对这一点;Eric 是 f-string PEP 498“文字字符串插值”的作者。

向 str % args 添加格式

有人提议添加格式以在 str % arg 中格式化类型名称。例如,添加 %T 格式以格式化类型的完全限定名称。

如今,f-string 是新代码的首选。

在 C 中格式化类型名称的其他方法

printf() 函数支持多个大小修饰符:hhchar)、hshort)、llong)、lllong long)、zsize_t)、tptrdiff_t)和 jintmax_t)。PyUnicode_FromFormat() 函数支持其中大多数。

使用 hhh 长度修饰符的提议格式

  • %hhT 格式化 type.__name__
  • %hT 格式化 type.__qualname__
  • %T 格式化 type.__fully_qualified_name__

长度修饰符用于指定参数的 C 类型,而不是更改参数的格式化方式。替代形式(#)更改参数的格式化方式。此处参数的 C 类型始终为 PyObject*

其他提议的格式

  • %Q
  • %t.
  • %lT 格式化 type.__fully_qualified_name__
  • %Tn 格式化 type.__name__
  • %Tq 格式化 type.__qualname__
  • %Tf 格式化 type.__fully_qualified_name__

拥有更多格式化类型名称的选项会导致不同模块之间出现不一致,并使 API 更容易出错。

关于 %t 格式,printf() 现在使用 t 作为 ptrdiff_t 参数的长度修饰符。

以下 API 用于格式化类型

C API Python API 格式
PyType_GetName() type.__name__ 类型**短**名称。
PyType_GetQualName() type.__qualname__ 类型**限定**名称。
PyType_GetModuleName() type.__module__ 类型**模块**名称。

使用 %T 格式化程序和 Py_TYPE(): 传递类型

有人提议将类型传递给 %T 格式,例如

PyErr_Format(PyExc_TypeError, "object type name: %T", Py_TYPE(obj));

Py_TYPE() 函数返回一个借用的引用。仅用于格式化错误,使用对类型的借用引用看起来是安全的。在实践中,它会导致崩溃。示例

import gc
import my_cext

class ClassA:
    pass

def create_object():
     class ClassB:
          def __repr__(self):
                self.__class__ = ClassA
                gc.collect()
                return "ClassB repr"
     return ClassB()

obj = create_object()
my_cext.func(obj)

其中 my_cext.func() 是一个调用以下内容的 C 函数

PyErr_Format(PyExc_ValueError,
             "Unexpected value %R of type %T",
             obj, Py_TYPE(obj));

PyErr_Format() 使用对 ClassB 的借用引用进行调用。当 repr(obj)%R 格式调用时,对 ClassB 的最后一个引用被移除,并且该类被释放。当处理 %T 格式时,Py_TYPE(obj) 已经是一个悬空指针,并且 Python 会崩溃。

获取类型完全限定名称的其他提议 API

  • 添加 type.__fullyqualname__ 属性:名称中单词之间没有下划线。几个 dunders,包括一些最近添加的 dunders,在单词中包含下划线:__class_getitem____release_buffer____type_params____init_subclass____text_signature__
  • 添加 type.__fqn__ 属性:FQN 名称代表**F**ully **Q**ualified **N**ame(完全限定名称)。
  • 添加 type.fully_qualified_name() 方法。添加到 type 的方法会被所有类型继承,因此可能会影响现有代码。

  • inspect模块添加一个函数。需要导入inspect模块才能使用它。

在类型完全限定名称中包含 __main__ 模块

type.__fully_qualified_name__格式化为f"{type.__module__}.{type.__qualname__}",或者如果type.__module__不是字符串或等于"builtins",则格式化为type.__qualname__。不要对__main__模块进行特殊处理:将其包含在名称中。

现有的代码,例如type.__repr__()collections.abcunittest模块,使用f'{obj.__module__}.{obj.__qualname__}'格式化类型名称,并且仅当模块等于builtins时才省略模块部分。

只有tracebackpdb模块也会在模块等于"builtins""__main__"时省略模块。

type.__fully_qualified_name__属性省略了__main__模块,以便为常见情况(在使用python script.py运行的脚本中定义的类型)生成更短的名称。对于调试,可以使用repr()函数作用于类型,它会在类型名称中包含__main__模块。或者使用f"{type.__module__}.{type.__qualname__}"格式始终包含模块名称,即使对于"builtins"模块也是如此。

脚本示例

class MyType:
    pass

print(f"name: {MyType.__fully_qualified_name__}")
print(f"repr: {repr(MyType)}")

输出

name: MyType
repr: <class '__main__.MyType'>

讨论


来源:https://github.com/python/peps/blob/main/peps/pep-0737.rst

上次修改:2024-06-01 20:53:34 GMT