PEP 485 – 用于测试近似相等性的函数
- 作者:
- Christopher Barker <PythonCHB at gmail.com>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2015年1月20日
- Python 版本:
- 3.5
- 发布历史:
- 决议:
- Python-Dev 消息
摘要
本PEP提议在标准库的math模块中添加一个isclose()函数,用于判断一个值是否近似等于或“接近”另一个值。
基本原理
浮点值具有有限精度,这导致它们无法精确表示某些值,并且重复计算会导致误差累积。因此,通常建议只在非常特定的情况下使用相等比较。通常不相等比较能解决问题,但在某些时候(通常在测试中),程序员希望确定计算值是否“接近”预期值,而不需要它们完全相等。这种情况很常见,尤其是在测试中,而且如何实现并不总是显而易见的,因此将其添加到标准库将是一个有用的补充。
现有实现
标准库包含 unittest.TestCase.assertAlmostEqual 方法,但它
- 被埋在 unittest.TestCase 类中
- 是一个断言,因此您不能(轻易地)将其用作命令行中的通用测试等
- 是一个绝对差值测试。通常,对于浮点数,衡量差值需要相对误差,即“这两个值是否在彼此的x%以内?”,而不是绝对误差。尤其是在事先不知道值的大小时。
numpy 包有 allclose() 和 isclose() 函数,但它们只能在 numpy 中使用。
statistics 包的测试中包含一个用于其单元测试的实现。
人们还可以在 Stack Overflow 和其他帮助网站上找到讨论和示例实现。
许多其他非 Python 系统也提供了这样的测试,包括 Boost C++ 库和 APL 语言 [4]。
这些现有实现表明这是一个常见的需求,并且自行编写并非易事,因此它有望成为标准库的一部分。
提议的实现
注意:此PEP是 python-ideas 列表上进行广泛讨论的结果 [1]。
新函数将添加到 math 模块中,并具有以下签名
isclose(a, b, rel_tol=1e-9, abs_tol=0.0)
a 和 b:是要测试相对接近度的两个值
rel_tol:是相对容差——它允许的误差量,相对于 a 或 b 的较大绝对值。例如,要设置 5% 的容差,请传入 tol=0.05。默认容差为 1e-9,这确保两个值在大约 9 位小数范围内相同。rel_tol 必须大于 0.0
abs_tol:是最小绝对容差级别——对于接近零的比较很有用。
除了错误检查等,该函数将返回以下结果
abs(a-b) <= max( rel_tol * max(abs(a), abs(b)), abs_tol )
名称 isclose 的选择是为了与现有的 isnan 和 isinf 保持一致。
非有限数的处理
IEEE 754 特殊值 NaN、inf 和 -inf 将根据 IEEE 规则处理。具体而言,NaN 不被视为与任何其他值(包括 NaN)接近。inf 和 -inf 仅被视为与自身接近。
非浮点类型
主要用例预计是浮点数。然而,用户可能希望以类似的方式比较其他数字类型。理论上,它应该适用于任何支持 abs()、乘法、比较和减法的类型。然而,math 模块中的实现是用 C 编写的,因此不能(轻易地)使用 Python 的鸭子类型。相反,传入函数的参数值将在执行计算之前转换为浮点类型。传入不能转换为浮点数的类型(或值)将引发适当的异常(TypeError、ValueError 或 OverflowError)。
代码将进行测试,以适应至少这些类型的一些值
DecimalintFractioncomplex:对于复数,一个伴随函数将添加到cmath模块中。在cmath.isclose()中,容差被指定为浮点数,复数值的绝对值将用于缩放和比较。如果传入复数容差,则绝对值将用作容差。
注:添加一个与 decimal 类型完全正确配合使用的 Decimal.isclose() 函数可能是有意义的,但这不属于本 PEP 的一部分。
接近零时的行为
如果任一值为零,相对比较就会出现问题。根据定义,任何值相对于零都不算小。而在计算上,如果任一值为零,则差值是另一个值的绝对值,计算出的绝对容差将是 rel_tol 乘以该值。当 rel_tol 小于 1 时,差值永远不会小于容差。
然而,尽管在数学上是正确的,但在许多用例中,用户需要知道计算值是否“接近”零。这就需要进行绝对容差测试。如果用户需要在循环或推导式中调用此函数,其中一些(但不是所有)预期值可能为零,那么用一个带有一组参数的单个函数来测试相对容差和绝对容差是很重要的。
如果两个要比较的值跨越零,也会出现类似的问题:如果 a 大约等于 -b,那么 a 和 b 永远不会被计算为“接近”。
为了处理这种情况,可以使用可选参数 abs_tol 来设置在计算出的相对容差非常小或为零时使用的最小容差。也就是说,如果两个值之间的差值小于 abs_tol,则它们总是被认为是接近的。
默认的绝对容差值设置为零,因为对于一般情况没有合适的值。在不知道给定用例的预期值的情况下,不可能知道合适的值。如果所有测试值都在 1 的数量级,那么大约 1e-9 的值可能合适,但如果预期值在 1e-9 或更小的数量级,那将太大。
任何非零的默认值都可能导致用户的测试完全不合适地通过。另一方面,如果使用默认值对零的测试首次失败,则会提示用户选择一个适用于当前问题的合适值,以使测试通过。
注:此 PEP 的作者已决定重新检查许多使用 numpy allclose() 函数的测试,该函数提供默认的绝对容差,并确保默认值是合适的。
如果用户将 rel_tol 参数设置为 0.0,那么只有绝对容差会影响结果。虽然这不是函数的目标,但它确实允许将其用作纯粹的绝对容差检查。
实施
一个 Python 示例实现(截至2015年1月22日)可在 GitHub 上找到
https://github.com/PythonCHB/close_pep/blob/master/is_close.py
此实现有一个标志,允许用户选择要应用的相对容差测试——本 PEP 不建议保留该标志,而是选择弱测试。
此处也有此 PEP 和测试代码等的草稿
相对差
基本上有两种方法来思考两个数字的接近程度
绝对差值:简单地 abs(a-b)
相对差值:abs(a-b)/scale_factor [2]。
绝对差值足够简单,本提案主要关注相对差值。
通常,比例因子是所考虑值的一些函数,例如
- 其中一个输入值的绝对值
- 两者中的最大绝对值
- 两者中的最小绝对值。
- 两者的算术平均值的绝对值
这些导致以下确定两个值 a 和 b 是否彼此接近的可能性。
abs(a-b) <= tol*abs(a)abs(a-b) <= tol * max( abs(a), abs(b) )abs(a-b) <= tol * min( abs(a), abs(b) )abs(a-b) <= tol * abs(a + b)/2
注意:(2) 和 (3) 也可以写成
(abs(a-b) <= abs(tol*a)) 或 (abs(a-b) <= abs(tol*b))(abs(a-b) <= abs(tol*a)) 和 (abs(a-b) <= abs(tol*b))
(Boost 将这些称为“弱”和“强”公式 [3])它们在计算上可能效率稍高,因此在示例代码中使用。
这些公式中的每一个都可能导致略微不同的结果。然而,如果容差值很小,则差异也相当小。实际上,通常小于可用的浮点精度。
这会产生多大的差异?
选择判断接近度的方法时,人们可能想知道使用一种测试与另一种测试会产生多大差异——即,有多少值(或什么范围的值)会通过一个测试但不会通过另一个测试。
最大的差异在于选项 (2) 和 (3) 之间,其中允许的绝对差值按两个值中较大者或较小者进行缩放。
定义 delta 为由较大值定义的允许绝对容差与由较小值定义的允许绝对容差之间的差值。也就是说,为了从两个测试中获得不同结果,两个输入值需要不同的量。tol 是相对容差值。
假设 a 是较大的值,并且 a 和 b 都是正数,这样分析会更容易。delta 因此为
delta = tol * (a-b)
或
delta / tol = (a-b)
通过测试的最大绝对差值:(a-b),等于容差乘以较大值
(a-b) = tol * a
代入delta的表达式
delta / tol = tol * a
所以
delta = tol**2 * a
例如,对于 a = 10,b = 9,tol = 0.1 (10%)
最大容差 tol * a == 0.1 * 10 == 1.0
最小容差 tol * b == 0.1 * 9.0 == 0.9
delta = (1.0 - 0.9) = 0.1 或 tol**2 * a = 0.1**2 * 10 = .1
在这种情况下,最大和最小容差测试之间的绝对差异可能很大。然而,所提议函数的主要用例是测试计算结果。在这种情况下,可能会选择小得多的相对容差。
例如,1e-8 的相对容差约为 Python 浮点数可用精度的一半。在这种情况下,两种测试之间的差异是 1e-8**2 * a 或 1e-16 * a,这接近 Python 浮点数的精度极限。如果相对容差设置为建议的默认值 1e-9(或更小),则两种测试之间的差异将因浮点精度限制而消失。也就是说,对于 a 和 b 的所有值,这四种方法将产生完全相同的结果。
此外,在常见用法中,容差定义为 1 个有效数字——即,1e-9 表示大约 9 位小数的精度。因此,各种可能测试之间的差异远低于指定容差的精度。
对称性
相对比较可以是对称的也可以是非对称的。对于对称算法
isclose(a,b) 总是与 isclose(b,a) 相同
如果相对接近度测试只使用其中一个值(例如上面的 (1)),则结果是非对称的,即 isclose(a,b) 不一定与 isclose(b,a) 相同。
哪种方法最合适取决于要问的问题。如果问题是:“这两个数字是否彼此接近?”,则没有明显的顺序,对称测试最合适。
然而,如果问题是:“计算值是否在已知值的 x% 以内?”,那么将容差缩放到已知值是合适的,并且非对称测试最合适。
从上一节可以清楚地看出,在常见用例中,这两种方法会产生相同或相似的结果。在这种情况下,本提案的目标是提供一个最不可能产生令人惊讶结果的函数。
对称方法提供了一种引人注目的连贯性——它反映了等式对称性,并且不太可能让人感到困惑。对称测试也省去了用户考虑参数设置顺序的需要。还有人指出,在某些情况下,评估顺序可能不明确,例如在比较一组值时。
在某些情况下,用户可能确实需要知道某个值是否在已知值的特定范围内。在这种情况下,直接编写测试就足够简单了
if a-b <= tol*a:
(在这种情况下假设 a > b)。对于这个特定情况,提供一个函数没有什么必要。
本提案采用对称测试。
选择哪种对称测试?
考虑了三种对称测试
使用两个值算术平均值的情况要求这些值在除以 2 之前先加在一起,这可能导致非常大的数字溢出到 inf,或者要求每个值在加在一起之前先除以 2,这可能导致非常小的数字下溢到零。这种效应只会在浮点值的极限处发生,但已决定没有值得缩小功能范围或增加检查值以确定计算顺序的复杂性的方法。
这留下 Boost 的“弱”测试 (2)——或使用较大值来缩放容差,或 Boost 的“强”测试 (3),它使用较小的值来缩放容差。对于小容差,它们产生相同的结果,但本提案使用 Boost 的“弱”测试用例:它具有对称性,并且对于非常大的容差提供更有用的结果。
大容差
最常见的用例预计是小容差——大约为默认值 1e-9。然而,在某些用例中,用户可能希望知道两个差异相当大的值是否在彼此的特定范围内:“a 是否在 b 的 200% (rel_tol = 2.0) 范围内?”在这种情况下,如果其中一个值为零,强测试永远不会指示两个值在彼此的该范围内。然而,弱测试将使用较大(非零)值进行测试,因此如果一个值为零,则返回 True。例如:0 是否在 10 的 200% 范围内?10 的 200% 是 20,因此在 10 的 200% 范围内的值是 -10 到 +30。零落在这个范围内,因此它将返回 True。
默认值
相对容差和绝对容差需要默认值。
相对容差默认值
两个值被认为“接近”所需的相对容差完全取决于用例。然而,相对容差必须大于 1e-16(Python 浮点数的近似精度)。选择 1e-9 的值是因为它是各种可能方法将产生相同结果的最大相对容差,而且它也是 Python 浮点数可用精度的一半左右。在一般情况下,一个好的数值算法预计不会损失超过大约一半的可用有效数字精度,如果可以接受更大的容差,用户应该在这种情况下考虑适当的值。因此,1e-9 有望在许多情况下“正常工作”。
绝对容差默认值
绝对容差值主要用于与零比较。确定一个值是否“接近”零所需的绝对容差完全取决于用例。其有用范围也基本上没有界限——预期值可以想象在 Python 浮点数的限制内的任何地方。因此,选择 0.0 作为默认值。
如果对于给定的用例,用户需要与零进行比较,则测试将保证首次失败,用户可以选择合适的值。
有人建议,与零进行比较实际上是一个常见的用例(有证据表明 numpy 函数经常与零一起使用)。在这种情况下,最好有一个“有用”的默认值。建议使用 1e-8 左右的值,这大约是值 1 左右的浮点精度的一半。
然而,引用《Python之禅》:“面对歧义,拒绝猜测的诱惑。”猜测用户最常关心接近 1.0 的值,当与较小的值一起使用时,会导致错误的通过测试——这可能比要求用户深思熟虑地选择一个合适的值更具破坏性。
预期用途
主要预期用例是各种形式的测试——“计算结果是否接近我的预期?”这种测试可能属于也可能不属于正式的单元测试套件。此类测试可以一次性在命令行、IPython Notebook、doctest 中使用,或者在 if __name__ == "__main__" 块中进行简单断言。
它也将是一个适用于隐式函数的简单迭代解的终止准则的函数
guess = something
while True:
new_guess = implicit_function(guess, *args)
if isclose(new_guess, guess):
break
guess = new_guess
不当用途
浮点比较的一个用例是测试数值算法的准确性。然而,在这种情况下,数值分析师理想情况下会进行仔细的误差传播分析,并且应该确切地了解要测试什么。ULP(末位单位)比较也可能适用。虽然此函数在此类情况下可能有用,但如果不仔细考虑,它不打算以这种方式使用。
其他方法
unittest.TestCase.assertAlmostEqual
(https://docs.pythonlang.cn/3/library/unittest.html#unittest.TestCase.assertAlmostEqual)
通过计算差值,四舍五入到给定的小数位数(默认为 7),然后与零进行比较,测试值是否近似(或不近似)相等。
此方法纯粹是绝对容差测试,并未解决相对容差测试的需求。
numpy isclose()
https://docs.scipy.org.cn/doc/numpy-dev/reference/generated/numpy.isclose.html
numpy 包提供了矢量化函数 isclose() 和 allclose(),用于与本提案类似的用例。
isclose(a, b, rtol=1e-05, atol=1e-08, equal_nan=False)
返回一个布尔数组,其中两个数组在容差范围内元素级相等。容差值是正数,通常是非常小的数字。相对差值(rtol * abs(b))和绝对差值 atol 相加,与 a 和 b 之间的绝对差值进行比较。
在这种方法中,绝对容差和相对容差是相加的,而不是本提案中使用的 or 方法。这在计算上更简单,如果相对容差大于绝对容差,那么相加将不起作用。然而,如果绝对容差和相对容差的量级相似,那么允许的差值将是预期值的两倍左右。
这使得函数更难理解,在这种情况下没有任何计算优势。
更重要的是,如果传入的值相对于绝对容差很小,那么相对容差将完全被淹没,这可能出乎意料。
这就是为什么在本提案中,绝对容差默认为零——用户将被要求选择一个适合当前值的值。
Boost 浮点比较
Boost 项目 ([3]) 提供了一个浮点比较函数。它采用对称方法,同时提供“弱”(两个相对误差中较大者)和“强”(两个相对误差中较小者)选项。本提案采用 Boost 的“弱”方法。没有必要通过提供选择不同方法的选项来使 API 复杂化,因为在大多数情况下结果会相似,而且用户在任何情况下都不太可能知道该选择哪一个。
替代提案
一个方法
主要的替代提案是根本不提供标准库函数,而是提供一个供用户参考的方法。这样做的好处是,该方法可以提供并解释各种选项,并让用户选择最合适的选项。然而,这将要求任何需要此类测试的人至少将该函数复制到其代码库中,并选择要使用的比较方法。
zero_tol
一种可能性是提供一个零容差参数,而不是绝对容差参数。这将是一个绝对容差,只在其中一个参数恰好为零的情况下才适用。这样做的好处是,对于所有非零值,都可以保留完整的相对容差行为,同时允许对零的测试有效。然而,它也可能导致潜在的令人惊讶的结果:一个很小的值可能“接近”零,但却不“接近”一个更小的值。例如,1e-10“接近”零,但却不“接近”1e-11。
无绝对容差
鉴于与零比较的问题,另一种可能性是只提供相对容差,并让与零的比较失败。在这种情况下,用户需要在比较涉及零时进行简单的绝对测试:abs(val) < zero_tol。
然而,这将不允许将相同的调用用于一系列值,例如在循环或推导式中。这会大大降低函数的实用性。需要注意的是,如果未覆盖默认值,则默认的 abs_tol=0.0 可实现相同的效果。
其他测试
上面“相对误差”一节讨论了所有其他考虑的测试。
参考资料
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0485.rst
最后修改时间:2025-02-01 08:59:27 GMT