使用浮点数计算的结果不准确 - 简单解决方案

inaccurate results for calculations using floats - Simple solution

提问人:gnoodle 提问时间:7/24/2020 更新时间:7/24/2020 访问量:370

问:

在 StackOverflow 和其他地方,已经提出了许多关于 Python 与使用浮点数的计算混淆行为的问题——通常返回的结果明显是错误的。对此的解释总是与此相关。然而,通常不提供实用的简单解决方案。

这不仅仅是错误(通常可以忽略不计)——更多的是像 .3.9999999999999998.7 - 4.7

我为此写了一个简单的解决方案,我的问题是,为什么像这样的 sthg 不是由 Python 在幕后自动实现的?

基本概念是将所有浮点数转换为整数,进行运算,然后适当地转换回浮点数。上面链接的文档中解释的困难仅适用于浮点数,不适用于整数,这就是它工作的原因。代码如下:

def justwork(x,operator,y):
    numx = numy = 0
    if "." in str(x):
        numx = len(str(x)) - str(x).find(".") -1
    if "." in str(y):
        numy = len(str(y)) - str(y).find(".") -1
    num = max(numx,numy)

    factor = 10 ** num
    newx = x * factor
    newy = y * factor

    if operator == "%":
        ans1 = x % y
        ans = (newx % newy) / factor
    elif operator == "*":
        ans1 = x * y
        ans = (newx * newy) / (factor**2)
    elif operator == "-":
        ans1 = x - y
        ans = (newx - newy) / factor
    elif operator == "+":
        ans1 = x + y
        ans = (newx + newy) / factor
    elif operator == "/":
        ans1 = x / y
        ans = (newx / newy)
    elif operator == "//":
        ans1 = x // y
        ans = (newx // newy)

    return (ans, ans1)

诚然,这是相当不优雅的,也许可以通过一些思考来改进,但它可以完成工作。该函数返回一个元组,其中包含正确的结果(通过转换为整数)和不正确的结果(自动提供)。以下是如何提供准确结果的示例,而不是正常操作。

#code                           #returns tuple with (correct, incorrect) result
print(justwork(0.7,"%",0.1))    #(0.0, 0.09999999999999992)
print(justwork(0.7,"*",0.1))    #(0.07, 0.06999999999999999)
print(justwork(0.7,"-",0.2))    #(0.5, 0.49999999999999994)
print(justwork(0.7,"+",0.1))    #(0.8, 0.7999999999999999)
print(justwork(0.7,"/",0.1))    #(7.0, 6.999999999999999)
print(justwork(0.7,"//",0.1))   #(7.0, 6.0)

TLDR:本质上的问题是,为什么浮点数被存储为以 2 为基数的二进制分数(本质上不精确),而它们可以像整数一样存储(这才有效)?

Python 二进制 浮点精度 IEEE-754

评论

3赞 Eric Postpischil 7/24/2020
这只能在一个简单的域中“工作”,特别是带有短十进制数字的简单算术。当涉及更复杂的计算时,例如非十进制分数或计算链,这些计算链产生的结果无法用短十进制数字表示,这将不起作用。至于为什么使用浮点而不是固定整数格式,那是因为点是浮点的:浮点数具有内置的刻度,使它们能够处理非常大或非常小的数字,例如在物理学中发生的那样。这称为动态范围。
0赞 chtz 7/24/2020
十进制浮点是一回事。不过,在大多数系统上,你对二进制浮点有更好的支持(这在硬件中更容易有效地实现)。
2赞 Sayandip Dutta 7/24/2020
主要原因是性能。对于执行数十亿次浮点计算的程序来说,这是一个太大的成本。而且绝对不仅仅是 Python。
0赞 chtz 7/24/2020
看起来 python 有一个用于十进制浮点数的模块:docs.python.org/3/library/decimal.html
2赞 Kelly Bundy 7/24/2020
如果我将其更改为(您声称的“正确”结果),则结果为 .而对于(您声称的“不正确”结果),它会导致 .return ansjustwork(justwork(1, '/', 3), '*', 3)0.9999999999999998return ans11.0

答:

0赞 gnoodle 7/24/2020 #1

三点:

  1. 问题/一般方法中提出的函数,虽然它在许多情况下确实避免了这个问题,但在许多其他情况下,即使是相对简单的情况,它也有同样的问题。
  2. 有一个模块总是提供准确的答案(即使问题中的函数无法提供)decimaljustwork()
  3. 使用该模块会大大减慢速度 - 大约需要 100 倍的时间。默认方法会牺牲准确性来优先考虑速度。[将其作为默认方法是否是正确的方法值得商榷]。decimal

为了说明这三点,请考虑以下函数,大致基于问题中的函数:

def justdoesntwork(x,operator,y):
    numx = numy = 0
    if "." in str(x):
        numx = len(str(x)) - str(x).find(".") -1
    if "." in str(y):
        numy = len(str(y)) - str(y).find(".") -1
    factor = 10 ** max(numx,numy)
    newx = x * factor
    newy = y * factor

    if operator == "+":     myAns = (newx + newy) / factor
    elif operator == "-":   myAns = (newx - newy) / factor
    elif operator == "*":   myAns = (newx * newy) / (factor**2)
    elif operator == "/":   myAns = (newx / newy)
    elif operator == "//":  myAns = (newx //newy)
    elif operator == "%":   myAns = (newx % newy) / factor

    return myAns

from decimal import Decimal
def doeswork(x,operator,y):
    if operator == "+":     decAns = Decimal(str(x)) + Decimal(str(y))
    elif operator == "-":   decAns = Decimal(str(x)) - Decimal(str(y))
    elif operator == "*":   decAns = Decimal(str(x)) * Decimal(str(y))
    elif operator == "/":   decAns = Decimal(str(x)) / Decimal(str(y))
    elif operator == "//":  decAns = Decimal(str(x)) //Decimal(str(y))
    elif operator == "%":   decAns = Decimal(str(x)) % Decimal(str(y))

    return decAns

然后遍历许多值以查找与 :myAnsdecAns

operatorlist = ["+", "-", "*", "/", "//", "%"]
for a in range(1,1000):
    x = a/10
    for b in range(1,1000):
        y=b/10
        counter = 0
        for operator in operatorlist:
            myAns, decAns = justdoesntwork(x, operator, y),  doeswork(x, operator, y)
            if (float(decAns) != myAns)   and     len(str(decAns)) < 5  :
                print(x,"\t", operator, " \t ", y, " \t=   ", decAns,  "\t\t{", myAns, "}")

=> 这将遍历从 0.1 到 1 d.9 的所有值 - 并且确实找不到任何不同于 的值。myAnsdecAns

但是,如果将其更改为给出 2d.p.(即 either 或 ),则会出现许多示例。例如, - 这可以通过在控制台中键入 来轻松检查,它使用问题的基本方法,并返回而不是 .错误的来源是返回 .[简单地输入也会产生相同的错误]。因此,问题中建议的方法并不总是有效。x = a/100y = b/1000.1+1.09((0.1*100)+(1.09*100)) / (100)1.19000000000000021.191.09*100109.000000000000010.1+1.09

但是,使用 Decimal() 返回正确答案: 返回 。Decimal('0.1')+Decimal('1.09')Decimal('1.19')

[注意:不要忘记用引号将 0.1 和 1.09 括起来。如果不这样做,则返回 - 因为它以存储不准确的浮点数 0.1 开头,然后将其转换为十进制 - GIGO。Decimal() 必须被输入一个字符串。获取浮点数,将其转换为字符串,然后从那里转换为十进制,似乎确实有效,但问题仅在直接从浮点数转换为十进制时]。Decimal(0.1)+Decimal(1.09)Decimal('1.190000000000000085487172896')


就时间成本而言,运行以下命令:

import timeit
operatorlist = ["+", "-", "*", "/", "//", "%"]

for operator in operatorlist:
    for a in range(1,10):
        a=a/10
        for b in range(1,10):
            b=b/10
            
            DECtime  = timeit.timeit("Decimal('" +str(a)+ "') " +operator+ " Decimal('" +str(b)+ "')", setup="from decimal import Decimal")
            NORMtime = timeit.timeit(str(a) +operator+ str(b))
            timeslonger = DECtime // NORMtime
            print("Operation:  ", str(a) +operator +str(b) , "\tNormal operation time: ", NORMtime, "\tDecimal operation time: ", DECtime, "\tSo Decimal operation took ", timeslonger, " times longer")

这表明,对于所有测试的运算符,十进制运算始终需要大约 100 倍的时间。

[在运算符列表中包括幂表明幂可能需要 3000 - 5000 倍的时间。然而,这在一定程度上是因为 Decimal() 的计算精度远高于正常操作 - Decimal() 默认精度为 28 位 - 返回 ,而返回 .如果通过替换 with 来限制为整数(这将防止出现高 SF 的结果),则与其他运算符一样,十进制计算所需的时间大约是整数的 100 倍]。Decimal("1.5")**Decimal("1.5")1.8371173070873835736479630561.5**1.51.8371173070873836bb=b/10b=float(b)


仍然可以说,时间成本只对执行数十亿次计算的用户来说很重要,大多数用户会优先考虑获得可理解的结果而不是时间差,这在大多数适度的应用程序中是微不足道的。

评论

0赞 gnoodle 7/24/2020
感谢 @SayandipDutta @ EricPostpischil @ chtz 和其他所有人的有用评论