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
上次修改: 2023-09-09 17:39:29 GMT