PEP 238 – 改变除法运算符
- 作者:
- Moshe Zadka <moshez at zadka.site.co.il>, Guido van Rossum <guido at python.org>
- 状态:
- 最终
- 类型:
- 标准追踪
- 创建时间:
- 2001-03-11
- Python 版本:
- 2.2
- 历史记录:
- 2001-03-16, 2001-07-26, 2001-07-27
摘要
当前的除法 (/
) 运算符对数值参数具有歧义:如果参数是整数或长整数,它将返回除法数学结果的向下取整;如果参数是浮点数或复数,它将返回除法结果的合理近似值。这使得当整数不是预期但可能是输入时,预期浮点数或复数结果的表达式容易出错。
我们建议通过引入不同的运算符来解决这个问题: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 或 int 参数下都能正常工作的多态函数变得很困难;所有其他运算符都以正确的方式工作。任何适用于 int 和 float 的算法都不需要在一情况下进行截断除法,而在另一情况下进行真除法。
正确的解决方法很微妙:如果参数可能是复数,那么将其强制转换为 float() 是错误的;将 0.0 添加到参数不会保留参数的符号,如果它是负零。唯一没有这两种缺点的解决方案是将参数(通常是第一个)乘以 1.0。这将保留 float 和 complex 的值和符号,并将 int 和 long 转换为具有相应值的 float。
作者认为这是一个 Python 中的真正设计缺陷,应该尽快修复。假设 Python 的使用将继续增长,将此 bug 留在语言中的成本最终将超过修复旧代码的成本——需要修复的代码量有一个上限,但未来可能受此 bug 影响的代码量是无限的。
此更改的另一个原因是希望最终统一 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 的统一数值模型的道路。 - 让 int 除法返回一个特殊的“混合”类型,该类型在整数上下文中表现为整数,但在浮点数上下文中表现为浮点数。这种方法的问题是,经过几次运算后,int 和 float 值可能相差很大,不清楚在比较中应该使用哪个值,当然许多上下文(例如转换为字符串)没有明确的整数或浮点数偏好。
- 使用指令在模块中使用特定的除法语义,而不是使用未来语句。这将保留经典除法作为语言中的永久性缺陷,要求未来的 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
表示经典除法运算符在应用于整数或长整数时发出警告(使用标准警告框架的 DeprecationWarning
)。值 warnall
还会在经典除法应用于浮点数或复数时发出警告;这用于下面提到的 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
和 long//int
都将返回长整数)。
对于浮点数输入,结果为浮点数。例如
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
。
注意,对于整数和长整数参数,真除法可能会丢失信息;这是真除法的本质(只要语言中没有有理数)。有意识地使用长整数的算法应该考虑使用 //
,因为长整数的真除法保留的精度不超过 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
最后修改时间:2023-09-09 17:39:29 GMT