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

inaccurate results for calculations using floats - Simple solution

关于 Python 与使用浮点数的计算的混淆行为,人们在 Whosebug 和其他地方提出了许多问题 - 通常会返回一个明显有少量错误的结果。 explanation for this 总是链接到。然而,通常不会提供实用的简单解决方案。

这不仅仅是错误(通常可以忽略不计) - 它更多的是获得像 3.999999999999999 这样的结果而得到像 8.7 - 4.7.[=15 这样的简单总和的混乱和不优雅=]

我已经为此编写了一个简单的解决方案,我的问题是,为什么 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)

诚然,这有点不雅,可能可以通过一些思考加以改进,但是 它完成了工作。函数 returns 具有正确结果(通过转换为整数)和错误结果(自动提供)的元组。以下是如何提供准确结果的示例,而不是正常执行。

#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 为底的二进制分数(本质上是不精确的)?

三分:

  1. 提出的question/general方法中的函数,虽然在很多情况下确实避免了这个问题,但也有很多其他情况,即使是比较简单的情况,也有同样的问题。
  2. 有一个 decimal 模块 始终 提供准确的答案(即使问题中的 justwork() 功能失败)
  3. 使用 decimal 模块会大大减慢速度 - 大约需要 100 倍的时间。默认方法牺牲准确性以优先考虑速度。 [将此设置为默认值是否是正确的做法值得商榷]。

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

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, "}")

=> 这会遍历所有值到 1 d.p。从 0.1 到 99.9 - 确实找不到 myAns 不同于 decAns.

的任何值

不过如果改成给2d.p。 (即 x = a/100y = b/100),然后会出现许多示例。例如,0.1+1.09 - 这可以通过在控制台中键入 ((0.1*100)+(1.09*100)) / (100) 轻松检查,它使用问题的基本方法,其中 returns 1.1900000000000002 而不是 1.19。错误的来源在 1.09*100 其中 returns 109.00000000000001。 [简单地输入 0.1+1.09 也会给出同样的错误]。所以问题中建议的方法并不总是有效。

但使用 Decimal() returns 正确答案:Decimal('0.1')+Decimal('1.09') returns Decimal('1.19').

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


在时间成本方面,运行这个:

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 位 - Decimal("1.5")**Decimal("1.5") returns 1.837117307087383573647963056,而 1.5**1.5 returns 1.8371173070873836。如果通过将 b=b/10 替换为 b=float(b) 来将 b 限制为整数(这将防止结果具有高 SF),与其他运算符一样,小数计算需要大约 100 倍的时间]。 =41=]


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