PEP 238 – 更改除法运算符
- 作者:
- Moshe Zadka <moshez at zadka.site.co.il>, Guido van Rossum <guido at python.org>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2001年3月11日
- Python 版本:
- 2.2
- 发布历史:
- 2001年3月16日,2001年7月26日,2001年7月27日
摘要
当前的除法 (/) 运算符对于数值参数具有模糊的含义:如果参数是 int 或 long,它返回数学除法结果的向下取整;但如果参数是 float 或 complex,它返回除法结果的合理近似值。这使得当不期望整数但可能作为输入时,期望浮点或复数结果的表达式容易出错。
我们建议通过为不同操作引入不同的运算符来解决此问题:x/y 返回数学除法结果的合理近似值(“精确除法”),x//y 返回向下取整(“向下取整除法”)。我们将 x/y 当前的混合含义称为“经典除法”。
由于严重的向后兼容性问题,更不用说 c.l.py 上的激烈争论,我们建议采取以下过渡措施(从 Python 2.2 开始):
- 在 Python 2.x 系列中,经典除法仍将是默认值;精确除法将在 Python 3.0 中成为标准。
//运算符将可用,明确请求向下取整除法。- 未来除法语句,拼写为
from __future__ import division,将使模块中的/运算符表示精确除法。 - 一个命令行选项将对应用于 int 或 long 参数的经典除法启用运行时警告;另一个命令行选项将使精确除法成为默认值。
- 标准库将适当地使用未来除法语句和
//运算符,以完全避免经典除法。
动机
经典除法运算符使得编写预期从任意数值输入中给出正确结果的数值表达式变得困难。对于所有其他运算符,可以编写一个公式,例如 x*y**2 + z,并且计算结果将接近数学结果(当然,在数值精度范围内)对于任何数值输入类型(int、long、float 或 complex)。但除法存在问题:如果两个参数的表达式都碰巧是整数类型,它会实现向下取整除法而不是精确除法。
这个问题是动态类型语言所特有的:在像 C 这样的静态类型语言中,输入(通常是函数参数)将被声明为 double 或 float,当调用传递整数参数时,它会在调用时转换为 double 或 float。Python 没有参数类型声明,因此整数参数很容易进入表达式。
这个问题尤其有害,因为在所有其他情况下,int 可以完美替代 float:math.sqrt(2) 返回与 math.sqrt(2.0) 相同的值,3.14*100 和 3.14*100.0 返回相同的值,依此类推。因此,数值例程的作者可能只使用浮点数来测试他的代码,并认为它工作正常,而用户可能会意外地传入整数输入值并得到不正确的结果。
另一种看待这个问题的方式是,经典除法使得编写能够很好地处理浮点或整数参数的多态函数变得困难;所有其他运算符已经做了正确的事情。没有一个算法既适用于整数又适用于浮点数,同时在一个情况下需要截断除法而在另一种情况下需要精确除法。
正确的解决方法是微妙的:如果一个参数可能是复数,将其转换为 float() 是错误的;如果一个参数是负零,向其添加 0.0 不会保留参数的符号。唯一的解决方案是(通常是第一个)参数乘以 1.0。这对于 float 和 complex 保持值和符号不变,并将 int 和 long 转换为具有相应值的 float。
作者认为这是 Python 中的一个真正的设计缺陷,应该尽早修复。假设 Python 的使用将继续增长,将此缺陷留在语言中的成本最终将超过修复旧代码的成本——需要修复的代码量有一个上限,但将来可能受此缺陷影响的代码量是无限的。
此更改的另一个原因是希望最终统一 Python 的数值模型。这是 PEP 228 的主题(目前尚未完成)。统一的数值模型消除了用户大部分需要了解不同数值类型的需求。这对于初学者有益,也消除了高级程序员对不同数值行为的担忧。(当然,它不会消除对数值稳定性和精度的担忧。)
在统一的数值模型中,不同的类型(int、long、float、complex,可能还有其他类型,例如新的有理数类型)主要作为存储优化,并在一定程度上指示正交属性,例如不精确性或复杂性。在统一模型中,整数 1 应该与浮点数 1.0 无法区分(除了其不精确性),并且两者在所有数值上下文中都应表现相同。显然,在统一数值模型中,如果 a==b 和 c==d,则 a/c 应该等于 b/d(由于不精确数的舍入,会略有不同),并且既然所有人都同意 1.0/2.0 等于 0.5,那么 1/2 也应该等于 0.5。同样,既然 1//2 等于零,那么 1.0//2.0 也应该等于零。
变体
从美学角度来看,x//y 并非人人满意,因此提出了几种变体。在此讨论它们:
x div y。这将引入一个新关键字。由于div是一个流行的标识符,这会破坏相当数量的现有代码,除非新关键字仅在未来除法语句下才被识别。由于预计需要转换的大部分代码是整数除法,这将大大增加对未来除法语句的需求。即使有未来语句,普遍反对添加新关键字(除非绝对必要)的情绪也反对这种做法。div(x, y)。这使得旧代码的转换更加困难。将x/y替换为x//y或x div y可以通过简单的查询替换完成;在大多数情况下,程序员可以很容易地验证特定模块只处理整数,因此所有出现的x/y都可以替换。(仍然需要查询替换以筛选掉注释或字符串文字中出现的斜杠。)将x/y替换为div(x, y)将需要一个更智能的工具,因为必须分析/左侧和右侧表达式的范围,才能决定div(和)部分的放置位置。x \ y。反斜杠已经是一个标记,表示行继续符,通常在 Unix 用户看来它表示“转义”。此外(这是由于 Terry Reedy),这会使诸如eval("x\y")之类的操作更难正确实现。
备选方案
为了减少需要转换的旧代码量,已经提出了几种替代提案。以下是对每项提案(或提案类别)的简要讨论。如果您知道在 c.l.py 上讨论过但此处未提及的替代方案,请发邮件给第二作者。
- 让
/保留其经典语义;引入//用于精确除法。这仍然在语言中留下一个有缺陷的运算符,并诱导使用该有缺陷的行为。它还关闭了通向 PEP 228 统一数值模型的道路。 - 让整数除法返回一个特殊的“复合”类型,在整数上下文中表现为整数,但在浮点上下文中表现为浮点数。这样做的问题是,经过几次操作后,整数值和浮点值可能会相去甚远,不清楚在比较中应该使用哪个值,当然许多上下文(如转换为字符串)没有明确的整数或浮点偏好。
- 使用指令而不是未来语句在模块中指定特定的除法语义。这使得经典除法作为语言中的一个永久缺陷保留下来,要求后代 Python 程序员了解这个问题和补救措施。
- 使用
from __past__ import division在模块中使用经典除法语义。这也保留了经典除法作为永久性的缺陷,或者至少在很长一段时间内保留(最终过去的除法语句可能会引发ImportError)。 - 使用指令(或其他方式)指定特定代码所开发的 Python 版本。这要求未来的 Python 解释器能够**精确**模拟几个以前的 Python 版本,并且在同一个解释器中为多个版本做到这一点。这工作量太大了。一个更简单的解决方案是保持安装多个解释器。反对这一点的另一个论点是,版本指令几乎总是过度指定:大多数为 Python X.Y 编写的代码,也适用于 Python X.(Y-1) 和 X.(Y+1),因此将 X.Y 指定为版本比需要的限制性更强。同时,无法知道代码将在哪个未来或过去版本中中断。
API 变更
在过渡阶段,我们必须在同一个程序中支持**三种**除法运算符:经典除法(用于没有未来除法语句的模块中的 /),精确除法(用于有未来除法语句的模块中的 /),以及向下取整除法(用于 //)。每个运算符都有两种形式:常规形式和增广赋值运算符形式(/= 或 //=)。
与这些变体相关的名称是:
- 重载运算符方法
__div__(), __floordiv__(), __truediv__(); __idiv__(), __ifloordiv__(), __itruediv__().
- 抽象 API C 函数
PyNumber_Divide(), PyNumber_FloorDivide(), PyNumber_TrueDivide(); PyNumber_InPlaceDivide(), PyNumber_InPlaceFloorDivide(), PyNumber_InPlaceTrueDivide().
- 字节码操作码
BINARY_DIVIDE, BINARY_FLOOR_DIVIDE, BINARY_TRUE_DIVIDE; INPLACE_DIVIDE, INPLACE_FLOOR_DIVIDE, INPLACE_TRUE_DIVIDE.
- PyNumberMethod 槽
nb_divide, nb_floor_divide, nb_true_divide, nb_inplace_divide, nb_inplace_floor_divide, nb_inplace_true_divide.
新增的 PyNumberMethod 槽位需要在 tp_flags 中添加一个额外的标志;这个标志将被命名为 Py_TPFLAGS_HAVE_NEWDIVIDE 并将包含在 Py_TPFLAGS_DEFAULT 中。
精确除法和向下取整除法 API 将查找相应的槽位并调用它;如果该槽位为 NULL,它们将引发异常。没有回退到经典除法槽位。
在 Python 3.0 中,经典除法语义将被移除;经典除法 API 将与精确除法同义。
命令行选项
-Q 命令行选项接受一个字符串参数,该参数可以取四个值:old、warn、warnall 或 new。在 Python 2.2 中默认值为 old,但在后续的 2.x 版本中将更改为 warn。old 值表示经典除法运算符按所述行为。 warn 值表示经典除法运算符应用于 int 或 long 时会发出警告(使用标准警告框架的 DeprecationWarning)。warnall 值还会对应用于 float 或 complex 的经典除法发出警告;这用于下面提到的 fixdiv.py 转换脚本。new 值全局更改默认值,使得 / 运算符始终解释为精确除法。new 选项仅用于某些教育环境,这些环境需要精确除法,但要求学生在所有代码中包含未来除法语句会带来问题。
此选项在 Python 3.0 中将不再支持;Python 3.0 将始终将 / 解释为精确除法。
(此选项最初提议为 -D,但结果发现它是 Jython 的一个现有选项,因此改为 Q——商数的助记符。还提出了其他名称,如 -Qclassic、-Qclassic-warn、-Qtrue 或 -Qold_division 等;对我来说,这些名称更冗长,并没有太大优势。毕竟,经典除法这个术语根本没有在语言中使用(只在 PEP 中),而精确除法这个术语在语言中也很少使用——只在 __truediv__ 中。)
向下取整除法的语义
向下取整除法将在所有 Python 数字类型中实现,其语义为:
a // b == floor(a/b)
除了结果类型将是*a*和*b*在操作前被强制转换的共同类型。
具体而言,如果 *a* 和 *b* 是相同类型,则 a//b 也将是该类型。如果输入是不同类型,它们首先使用所有其他算术运算符相同的规则强制转换为公共类型。
特别是,如果 *a* 和 *b* 都是 int 或 long,则结果具有与这些类型的经典除法相同的类型和值(包括混合输入类型的情况;int//long 和 long//int 都将返回一个 long)。
对于浮点输入,结果是浮点数。例如:
3.5//2.0 == 1.0
对于复数,// 会引发异常,因为不允许对复数执行 floor()。
对于用户自定义类和扩展类型,所有语义都取决于类或类型的实现。
精确除法的语义
对整数和长整数的精确除法将把参数转换为浮点数,然后进行浮点除法。也就是说,即使 2/1 也将返回一个 float (2.0),而不是整数。对于浮点数和复数,它将与经典除法相同。
2.2 实现的精确除法表现得好像浮点类型具有无界范围,因此除非数学**结果**的幅度太大而无法表示为浮点数,否则不会发生溢出。例如,在 x = 1L << 40000 之后,float(x) 会引发 OverflowError(请注意,这也是 2.2 中的新功能:以前的结果取决于平台,最常见的是浮点无穷大)。但是 x/x 返回 1.0 且不引发异常,而 x/1 则引发 OverflowError。
请注意,对于 int 和 long 参数,精确除法可能会丢失信息;这是精确除法的本质(只要语言中没有有理数)。有意使用 long 的算法应考虑使用 //,因为 long 的精确除法最多只保留 53 位精度(在大多数平台上)。
如果 Python 中添加了有理数类型(参见 PEP 239),则整数和长整数的精确除法应该返回一个有理数。这避免了整数和长整数精确除法丢失信息的问题。但在此之前,为了保持一致性,浮点数是精确除法的唯一选择。
未来除法语句
如果模块中存在 from __future__ import division,或者使用了 -Qnew,则 / 和 /= 运算符将转换为精确除法操作码;否则,它们将转换为经典除法(直到 Python 3.0 出现,届时它们将始终转换为精确除法)。
未来除法语句对 // 和 //= 的识别或转换没有影响。
有关未来语句的一般规则,请参阅 PEP 236。
(有人提议使用更长的短语,如 *true_division* 或 *modern_division*。这些似乎没有增加太多信息。)
未解决的问题
我们预计这些问题会随着时间的推移而解决,因为我们收到了更多的反馈,或者我们从最初的实现中获得了更多的经验。
- 有人提议将
//称为商运算符,将/运算符称为比率运算符。我对此不确定——对某些人来说,商只是除法的同义词,而比率暗示有理数,这是错误的。我更喜欢稍微笨拙的术语,如果这能避免歧义的话。此外,对某些人来说,*商*暗示向零截断,而不是像*向下取整除法*明确指出的那样向无穷大截断。 - 有人认为更改默认值的命令行选项是邪恶的。它在错误的人手中确实很危险:例如,不可能将一个需要
-Qnew的第三方库包与另一个需要-Qold的包结合起来。但我相信 VPython 的人们需要一种方法来默认启用精确除法,其他教育工作者可能也需要同样的方法。这些人通常对他们环境中可用的库包有足够的控制权。 - 对于类来说,必须支持
__div__()、__floordiv__()和__truediv__()三种方法似乎很痛苦;在 3.0 中该怎么做呢?也许我们只需要__div__()和__floordiv__(),或者至少精确除法应该首先尝试__truediv__(),然后尝试__div__()。
已解决的问题
- 问题:对于非常大的长整数,精确除法定义为返回浮点数会带来问题,因为 Python 长整数的范围远大于 Python 浮点数。如果支持有理数,这个问题将消失。
解决方案:对于长整数的精确除法,Python 使用内部浮点类型,该类型具有原生双精度但无界范围,因此除非商太大而无法表示为原生双精度数,否则不会发生 OverflowError。
- 问题:在此期间,也许可以将长整数到浮点数的转换设置为在长整数超出范围时引发
OverflowError。解决方案:这已经实现,但如上所述,长整数精确除法的输入的幅度并不重要;只有商的幅度才重要。
- 问题:Tim Peters 将确保只要返回的浮点数在范围内,就能保证体面的精度。
解决方案:如果长整数精确除法的商可以表示为浮点数,它不会遭受超过 3 次舍入误差:每次将输入转换为具有原生双精度但无界范围的内部浮点类型一次,以及除法一次。但是,请注意,如果商的幅度太**小**而无法表示为原生双精度数,则会返回 0.0 而不引发异常(“静默下溢”)。
常见问题
Python 3.0 何时发布?
我们没有那么长远的计划,所以不能确定。我们希望至少留出两年时间进行过渡。如果 Python 3.0 更早发布,我们将保持 2.x 系列的向后兼容性,直到 Python 2.2 发布至少两年。实际上,您可以在 Python 3.0 发布后继续使用 Python 2.x 系列数年,因此您可以慢慢进行过渡。预计网站将同时安装 Python 2.x 和 Python 3.x。
为什么精确除法不叫浮点除法?
因为我希望保留**可能**引入有理数的可能性,并让 1/2 返回有理数而不是浮点数。参见 PEP 239。
为什么需要 __truediv__ 和 __itruediv__?
我们不想让用户定义的类成为二等公民。当然,在类型/类统一进行时更是如此。
如何在经典规则和新规则下编写代码,而不使用 // 或未来除法语句?
使用x*1.0/y进行精确除法,使用divmod(x, y)(PEP 228) 进行整数除法。特别是后者最好隐藏在一个函数中。如果您确定不期望复数,也可以编写float(x)/y进行精确除法。如果您知道整数永远不会是负数,则可以使用int(x/y)——虽然int()的文档说int()可以根据 C 实现进行四舍五入或截断,但我们不知道有哪个 C 实现不截断,我们计划更改int()的规范以保证截断。请注意,经典除法(和向下取整除法)向负无穷方向舍入,而int()向零方向舍入,对于负数会给出不同的答案。
如何为 input()、compile()、execfile()、eval() 和 exec 指定除法语义?
codeop 模块编译的代码呢?
这已妥善处理;请参见 PEP 264。
会有转换工具或辅助工具吗?
当然。虽然这些超出 PEP 的范围,但我应该指出两个将随 Python 2.2a3 发布的小工具:Tools/scripts/finddiv.py用于查找除法运算符(比grep /稍智能),Tools/scripts/fixdiv.py可以根据运行时分析生成补丁。
为什么我的问题没有在这里得到解答?
因为我们不知道。如果在 c.l.py 上讨论过并且您认为答案具有普遍意义,请通知第二作者。(我们没有时间或意愿回答通过私人电子邮件发送的每个问题,因此要求首先在 c.l.py 上讨论。)
实施
这里提到的一切基本上都在 CVS 中实现,并将与 Python 2.2a3 一起发布;大部分内容已与 Python 2.2a2 一起发布。
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0238.rst