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,因为它比调用需要 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__() 方法可以修改为以不同方式格式化类型名称。例如,它可以返回类型完全限定名称。

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

请参阅 拉取请求:type(str) 返回完全限定名称

添加 !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() 函数支持它们中的大多数。

使用 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__ 类型 模块 名称。

对 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.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