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
格式)。
实现
- Pull request: 添加 type.__fully_qualified_name__ 属性。
- Pull request: 向 PyUnicode_FromFormat() 添加 %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__()
方法可以修改为以不同的方式格式化类型名称。例如,它可以返回类型的完全限定名称。
问题在于这是一个向后不兼容的更改。例如,标准库的 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}"
。
当 2018 年提出 !t
格式化程序时,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__ |
类型**模块**名称。 |
使用 %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.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()中用%s替换%.100s:500字节的任意限制已过时(2011)。
版权
本文件置于公有领域或根据CC0-1.0-Universal许可证,以两者中许可范围更广者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0737.rst