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,因为它比调用需要 Py_DECREF() 的 PyType_GetQualName() 更方便。此外,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() 中的 %.100s 替换为 %s: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" 时省略模块。
此更改被 Steering Council 拒绝
我们看到了 PEP 提出的 C API 更改的有用性,并且可能会原样接受这些更改。我们认为 Python 级别更改的理由较少。我们尤其质疑
__fully_qualified_name__的必要性。
Thomas Wouters 补充道
如果真的希望以 C API 完全相同的方式格式化类型,那么一个实用函数对我个人来说比type.__format__更有意义,但我认为如果有一些具体的用例,SC 可能会被说服。
添加 type.__format__() 方法
添加 type.__format__() 方法,包含以下格式
N格式化类型 完全限定名称 (type.__fully_qualified_name__);N代表 Name。#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 入口点。
此更改被 Steering Council 拒绝。
更改 str(type)
type.__str__() 方法可以修改为以不同方式格式化类型名称。例如,它可以返回类型完全限定名称。
问题是这是一个向后不兼容的更改。例如,标准库的 enum、functools、optparse、pdb 和 xmlrpc.server 模块必须更新。test_dataclasses、test_descrtut 和 test_cmd_line_script 测试也必须更新。
添加 !t 格式化程序以获取对象类型
使用 f"{obj!t:T}" 格式化 type(obj).__fully_qualified_name__,类似于 f"{type(obj):T}"。
当 !t 格式化程序于 2018 年被提出时,Eric Smith 强烈反对;Eric 是 f-string PEP 498“字面字符串插值”的作者。
向 str % args 添加格式
有人提议向 str % arg 添加格式以格式化类型名称。例如,添加 %T 格式以格式化类型完全限定名称。
如今,新代码更倾向于 f-string。
在 C 中格式化类型名称的其他方法
printf() 函数支持多种大小修饰符:hh (char)、h (short)、l (long)、ll (long long)、z (size_t)、t (ptrdiff_t) 和 j (intmax_t)。PyUnicode_FromFormat() 函数支持它们中的大多数。
使用 h 和 hh 长度修饰符的提议格式
%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__ |
类型 模块 名称。 |
对 Py_TYPE() 使用 %T 格式:传递一个类型
有人提议将类型传递给 %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 的借用引用调用。当 %R 格式调用 repr(obj) 时,ClassB 的最后一个引用被移除,类被释放。当 %T 格式继续时,Py_TYPE(obj) 已经是一个悬空指针,Python 崩溃。
获取类型完全限定名称的其他提议 API
- 添加
type.__fullyqualname__属性:名称单词之间没有下划线。许多双下划线名称,包括一些最近添加的名称,在单词中包含下划线:__class_getitem__、__release_buffer__、__type_params__、__init_subclass__和__text_signature__。 - 添加
type.__fqn__属性:FQN 名称代表 Fully Qualified Name。 - 添加
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.abc 和 unittest 模块,使用 f'{obj.__module__}.{obj.__qualname__}' 格式化类型名称,并且仅在模块等于 builtins 时省略模块部分。
只有 traceback 和 pdb 模块在模块等于 "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'>
讨论
- 讨论:PEP 737 – 统一类型名称格式 (2023)。
- 讨论:增强引发异常时的类型名称格式:在 C 中添加 %T 格式,并添加 type.__fullyqualname__ (2023)。
- 问题:PyUnicode_FromFormat():添加 %T 格式以格式化对象的类型名称 (2023)。
- 问题:C API:调查如何从公共 C API 中移除 PyTypeObject 成员 (2023)。
- python-dev 线程:bpo-34595:如何格式化类型名称? (2018)。
- 问题:PyUnicode_FromFormat():为对象类型名称添加 %T 格式 (2018)。
- 问题:将 PyErr_Format() 中的 %.100s 替换为 %s:500 字节的任意限制已过时 (2011)。
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0737.rst