Python - PEP 737 – C API 用于格式化类型的完全限定名
摘要
新增便捷的 C API 用于格式化类型的完全限定名。类型名的格式不再因实现方式不同而不同。
建议在新的 C 代码中,错误消息和 __repr__() 方法中使用类型的完全限定名;不建议在新 C 代码中截断类型名。
在 PyUnicode_FromFormat() 中新增 %T、%#T、%N 和 %#N 格式,分别用于格式化对象类型或类型的完全限定名。
通过避免借用引用,让 C 代码更安全,防止崩溃。新 C API 兼容 limited C API。
原理
标准库中的现状
在 Python 标准库中,格式化类型名或对象类型名是生成错误消息和实现 __repr__() 方法的常见操作。格式化类型名的方法多样,输出也不同。
以 datetime.timedelta 类型为例:
- 类型短名(
type.__name__)和限定名(type.__qualname__)均为'timedelta'。 - 类型模块(
type.__module__)为'datetime'。 - 类型完全限定名为
'datetime.timedelta'。 - 类型的 repr(
repr(type))包含完全限定名:<class 'datetime.timedelta'>。
Python 代码
Python 中,type.__name__ 得到短名,f"{type.__module__}.{type.__qualname__}" 得到“完全限定名”。通常用 type(obj) 或 obj.__class__ 获取对象类型,有时类型名还会加引号。
示例:
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__)
限定名(__qualname__)自 Python 3.3(PEP 3155)加入类型。
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(),且仅自 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 实现的静态和 heap 类型:tp_name 是完全限定名。
- Python 类:tp_name 是类型短名(
type.__name__)。
因此,Py_TYPE(obj)->tp_name 的结果依赖于类型是 C 还是 Python 实现的。
这违背了 PEP 399“Python/C 加速模块兼容性要求”中推荐的“Python 与 C 行为一致”原则。
示例:
>>> 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 类型
TypeError: list indices must be integers or slices, not datetime.date
>>> my_list[py_obj] # Python 类型
TypeError: list indices must be integers or slices, not date
C 实现类型的错误消息含完全限定名(datetime.date),Python 实现的只含短名(date)。
limited C API 问题
Py_TYPE(obj)->tp_name 不能用于 limited C API,因为 PyTypeObject 成员未包含在 limited 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 的兼容性原则。
参见议题:Replace %.100s by %s in PyErr_Format(): the arbitrary limit of 500 bytes is outdated。
规范
- 新增
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() 新增格式
新增下列格式:
%N:格式化类型的完全限定名,类似PyType_GetFullyQualifiedName(type);N 代表 type Name。%T:格式化对象类型的完全限定名,类似PyType_GetFullyQualifiedName(Py_TYPE(obj));T 代表 object 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);
优势:
- 更安全:避免用
Py_TYPE()返回的借用引用。 - 不再直接读
tp_name,兼容 limited C API。 - 类型名不再因实现方式不同而不同。
- 不再截断类型名。
注意:%T 在 time.strftime() 中有用,但 printf 没有该格式。
格式总结
| C 对象 | C 类型 | 格式 |
|---|---|---|
%T | %N | 类型完全限定名 |
%#T | %#N | 类型完全限定名(冒号分隔) |
建议用类型完全限定名
新 C 代码中,错误消息和 __repr__() 建议用类型的完全限定名。
在非平凡项目中,不同模块可能会有同名类型(尤其泛型名),用完全限定名能明确区分。
建议不截断类型名
新 C 代码不应截断类型名,如应避免 %.100s,而用 %s 或 %T。
实现
向后兼容性
本 PEP 方案向后兼容。
新增 C API 不影响旧代码,现有 C API 不变,Python API 不变。
建议在新代码中用完全限定名、避免截断,现有代码不变,确保兼容性。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:用冒号分隔完全限定名
例如:
>>> import datetime
>>> f"{datetime.timedelta:N}" # 完全限定名
'datetime.timedelta'
>>> f"{datetime.timedelta:#N}" # 冒号分隔
'datetime:timedelta'
冒号分隔便于自动导入,见 pkgutil.resolve_name()、python -m inspect、setuptools entry points。
此提议被指导委员会拒绝。
修改 str(type)
可修改 type.__str__(),如输出完全限定名。但这属于破坏性变更,需更改标准库多个模块和测试。
见:type(str) returns the fully qualified name。
新增 !t 格式化器获取对象类型
用 f"{obj!t:T}" 格式化 type(obj).__fully_qualified_name__,等价于 f"{type(obj):T}"。
2018 年提出 !t 格式化器时,PEP 498 作者 Eric Smith强烈反对。
str % args 新增格式
建议为 str % arg 新增格式,如 %T 输出完全限定名。但新代码更推荐用 f-string。
C 层格式化类型名其他方案
printf() 支持多种长度修饰符(如 hh, h, l, ll, z, t, j),PyUnicode_FromFormat() 亦支持。
曾建议:
%hhT输出type.__name__%hT输出type.__qualname__%T输出完全限定名
但长度修饰符用于指定参数 C 类型,不应改变格式化方式,建议用 # 替代。参数类型始终为 PyObject*。
其他建议还有 %Q、%t、%lT、%Tn、%Tq、%Tf 等。选项太多反而导致模块间不一致,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() 里:
PyErr_Format(PyExc_ValueError,
"Unexpected value %R of type %T",
obj, Py_TYPE(obj));
当 %R 取 repr(obj) 时,最后引用被回收,ClassB 被释放,此时再用 %T,Py_TYPE(obj) 已悬空,Python 崩溃。
获取类型完全限定名的其他 API
- 新增
type.__fullyqualname__属性(无下划线隔词)。但大部分 dunder 新属性都带下划线。 - 新增
type.__fqn__属性(Fully Qualified Name)。 - 新增
type.fully_qualified_name()方法。type 方法会被所有类型继承,影响现有代码。 - 新增 inspect 模块函数,但需额外 import inspect。
完全限定名中包含 main 模块
格式为 f"{type.__module__}.{type.__qualname__}",除非模块不是字符串或为 "builtins"。不特殊处理 "__main__",即照常包含。
现有代码(如 type.__repr__()、collections.abc、unittest)只在模块为 "builtins" 时省略模块名。
只有 traceback 和 pdb 模块还会在模块等于 "builtins" 或 "__main__" 时省略模块名。
type.__fully_qualified_name__ 属性省略 "__main__",便于脚本简短显示。调试时可用 repr(type),会包含 "__main__"。若想始终包含模块名,可用 f"{type.__module__}.{type.__qualname__}"。
示例:
class MyType: pass
print(f"name: {MyType.__fully_qualified_name__}")
print(f"repr: {repr(MyType)}")
输出:
name: MyType
repr: <class '__main__.MyType'>
讨论记录
- Discourse:PEP 737 – 统一类型名格式化
- Discourse:异常增强 type 名格式化、C 层 %T、新增 type.fullyqualname
- Issue:PyUnicode_FromFormat(): Add %T format to format the type name of an object
- Issue:C API: Investigate how the PyTypeObject members can be removed from the public C API
- python-dev:bpo-34595: How to format a type name?
- Issue:PyUnicode_FromFormat(): add %T format for an object type name
- Issue:Replace %.100s by %s in PyErr_Format(): the arbitrary limit of 500 bytes is outdated
版权
本文档可置于公有领域或 CC0-1.0-Universal 许可下,以更宽松者为准。
原文地址:https://peps.python.org/pep-0737/
927

被折叠的 条评论
为什么被折叠?



