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

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 起才有。

有些函数用 %Rrepr(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。
  • 类型名不再因实现方式不同而不同。
  • 不再截断类型名。

注意:%Ttime.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 inspectsetuptools 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 APIPython 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.abcunittest)只在模块为 "builtins" 时省略模块名。

只有 tracebackpdb 模块还会在模块等于 "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'>

讨论记录

版权

本文档可置于公有领域或 CC0-1.0-Universal 许可下,以更宽松者为准。

原文地址:https://peps.python.org/pep-0737/

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

csdddn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值