PEP 208 – 重新设计类型强制转换模型
- 作者:
- Neil Schemenauer <nas at arctrix.com>, Marc-André Lemburg <mal at lemburg.com>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2000年12月4日
- Python 版本:
- 2.1
- 发布历史:
摘要
许多Python类型实现了数值运算。当数值运算的参数类型不同时,解释器会尝试将参数强制转换为一个通用类型。然后使用这个通用类型执行数值运算。本PEP提出了一个新的类型标志,用于指示类型数值运算的参数不应进行强制转换。不支持所提供类型的操作会通过返回一个新的单例对象来表示。未设置该类型标志的类型以向后兼容的方式处理。允许操作处理不同类型通常比让解释器进行强制转换更简单、更灵活、更快。
基本原理
在实现数值或其他相关操作时,通常不仅希望提供同一类型操作数之间的操作,例如整数 + 整数,还希望将操作的思想推广到其他类型组合,例如整数 + 浮点数。
处理这种混合类型情况的常见方法是提供一种将操作数“提升”到通用类型(强制转换)的方法,然后使用该类型的操作数方法作为执行机制。然而,这种策略存在一些缺点:
- “提升”过程至少会创建一个新的(临时)操作数对象;
- 由于强制转换方法未被告知后续操作,因此无法实现针对特定操作的类型强制转换;
- 在没有通用类型的情况下,没有优雅的方法来解决问题;
- 强制转换方法总是必须在操作方法本身之前被调用。
显然需要解决这种情况,因为这些缺点使得需要这些功能的类型实现非常麻烦,甚至不可能。例如,看看 DateTime 和 DateTimeDelta [1] 类型,前者是绝对值,后者是相对值。您总是可以将一个相对值添加到一个绝对值,从而得到一个新的绝对值。然而,现有的强制转换机制无法使用通用类型来实现该操作。
目前,PyInstance 类型被解释器特殊处理,其数值方法被传递不同类型的参数。删除这种特殊情况可以简化解释器,并允许其他类型实现行为类似于实例类型的数值方法。这对于像 ExtensionClass 这样的扩展类型特别有用。
规范
与其使用一个中央强制转换方法,不如将处理不同操作数类型的过程简单地留给操作本身。如果操作发现它无法处理给定的操作数类型组合,它可能会返回一个特殊的单例作为指示。
请注意,“数字”(任何实现数字协议或其一部分的东西)用 Python 编写时已经使用了这种策略的第一部分——我们在这里关注的是 C 级 API。
为了保持近乎100%的向后兼容性,我们必须非常小心地使那些对新策略一无所知(旧式数字)的数字也能正常工作,就像那些期望新方案(新式数字)的数字一样。此外,二进制兼容性是必须的,这意味着解释器只有在数字指示这些可用性时才能访问和使用新式操作。
当且仅当一个新式数字设置了类型标志 Py_TPFLAGS_CHECKTYPES 时,解释器才会将其视为新式数字。旧式数字和新式数字之间的主要区别在于,数值槽函数不再假定传递相同类型的参数。新式槽必须检查所有参数是否类型正确,并自行实现必要的转换。这似乎会给类型实现者带来更多的工作,但实际上并不比为旧式强制转换槽编写相同类型的例程更困难。
如果一个新式槽发现它无法处理传递的参数类型组合,它可能会向调用者返回特殊单例 Py_NotImplemented 的新引用。这将导致调用者尝试其他操作数的操作槽,直到找到一个确实为特定类型组合实现了该操作的槽。如果所有可能的槽都失败,它将引发 TypeError。
为了使实现易于理解(整个主题已经足够深奥),引入了数值操作处理的新层。这一层负责处理所有在处理旧式和新式数字的各种可能组合时需要考虑的不同情况。它由两个静态函数 binary_op() 和 ternary_op() 实现,这两个函数都是内部函数,只有 Objects/abstract.c 中的函数才能访问。数值 API (PyNumber_*) 很容易适应这一新层。
作为一个附带效果,所有数值槽都可以进行 NULL 检查(反正这也要做,所以增加这个功能没有额外开销)。
该层执行二元运算的方案如下:
| v | w | 采取的行动 |
|---|---|---|
| 新 | 新 | v.op(v,w), w.op(v,w) |
| 新 | 旧 | v.op(v,w), coerce(v,w), v.op(v,w) |
| 旧 | 新 | w.op(v,w), coerce(v,w), v.op(v,w) |
| 旧 | 旧 | coerce(v,w), v.op(v,w) |
指示的操作序列从左到右执行,直到操作成功并返回有效结果(!= Py_NotImplemented),或者引发异常。异常会原样返回给调用函数。如果槽返回 Py_NotImplemented,则执行序列中的下一个项目。
请注意,coerce(v,w) 将通过调用 PyNumber_Coerce() 使用旧式 nb_coerce 槽方法。
三元操作需要处理更多情况:
| v | w | z | 采取的行动 |
|---|---|---|---|
| 新 | 新 | 新 | v.op(v,w,z), w.op(v,w,z), z.op(v,w,z) |
| 新 | 旧 | 新 | v.op(v,w,z), z.op(v,w,z), coerce(v,w,z), v.op(v,w,z) |
| 旧 | 新 | 新 | w.op(v,w,z), z.op(v,w,z), coerce(v,w,z), v.op(v,w,z) |
| 旧 | 旧 | 新 | z.op(v,w,z), coerce(v,w,z), v.op(v,w,z) |
| 新 | 新 | 旧 | v.op(v,w,z), w.op(v,w,z), coerce(v,w,z), v.op(v,w,z) |
| 新 | 旧 | 旧 | v.op(v,w,z), coerce(v,w,z), v.op(v,w,z) |
| 旧 | 新 | 旧 | w.op(v,w,z), coerce(v,w,z), v.op(v,w,z) |
| 旧 | 旧 | 旧 | coerce(v,w,z), v.op(v,w,z) |
与上述注意事项相同,除了 coerce(v,w,z) 实际上执行的是
if z != Py_None:
coerce(v,w), coerce(v,z), coerce(w,z)
else:
# treat z as absent variable
coerce(v,w)
当前的实现已经使用了这个方案(只有一个三元槽:nb_pow(a,b,c))。
请注意,数值协议也用于一些其他相关任务,例如序列连接。这些也可以从新机制中受益,通过为否则会失败的类型组合实现右手操作。例如,字符串连接:目前只能进行字符串 + 字符串。使用新机制,新的类似字符串的类型可以实现 new_type + 字符串和字符串 + new_type,即使字符串对 new_type 一无所知。
由于比较也依赖于强制转换(每次将整数与浮点数比较时,整数首先被转换为浮点数,然后进行比较……),因此需要一个新的槽来处理数值比较:
PyObject *nb_cmp(PyObject *v, PyObject *w)
此槽应比较两个对象并返回一个表示结果的整数对象。目前,此结果整数只能是 -1、0、1。如果该槽无法处理类型组合,它可能会返回 Py_NotImplemented 的引用。[XXX 请注意,此槽仍在调整中,因为它应该考虑到丰富的比较(即 PEP 207)。]
数值比较由新的数值协议 API 处理:
PyObject *PyNumber_Compare(PyObject *v, PyObject *w)
此函数将两个对象作为“数字”进行比较,并返回一个表示结果的整数对象。目前,此结果整数只能是 -1、0、1。如果给定对象无法处理此操作,则会引发 TypeError。
需要相应调整 PyObject_Compare() API 以利用此新 API。
其他更改包括调整一些内置函数(例如 cmp())以使用此 API。此外,PyNumber_CoerceEx() 在调用 nb_coerce 槽之前需要检查新式数字。新式数字不提供强制转换槽,因此不能显式强制转换。
参考实现
Python CVS 版本的初步补丁可通过 Source Forge 补丁管理器获取 [2]。
致谢
此 PEP 和补丁主要基于 Marc-André Lemburg 完成的工作 [3]。
版权
本文档已置于公共领域。
参考资料
来源:https://github.com/python/peps/blob/main/peps/pep-0208.rst