提问人:gnoodle 提问时间:7/24/2020 更新时间:7/24/2020 访问量:370
使用浮点数计算的结果不准确 - 简单解决方案
inaccurate results for calculations using floats - Simple solution
问:
在 StackOverflow 和其他地方,已经提出了许多关于 Python 与使用浮点数的计算混淆行为的问题——通常返回的结果明显是错误的。对此的解释总是与此相关。然而,通常不提供实用的简单解决方案。
这不仅仅是错误(通常可以忽略不计)——更多的是像 .3.999999999999999
8.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 为基数的二进制分数(本质上不精确),而它们可以像整数一样存储(这才有效)?
答:
三点:
- 问题/一般方法中提出的函数,虽然它在许多情况下确实避免了这个问题,但在许多其他情况下,即使是相对简单的情况,它也有同样的问题。
- 有一个模块总是提供准确的答案(即使问题中的函数无法提供)
decimal
justwork()
- 使用该模块会大大减慢速度 - 大约需要 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
然后遍历许多值以查找与 :myAns
decAns
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 的所有值 - 并且确实找不到任何不同于 的值。myAns
decAns
但是,如果将其更改为给出 2d.p.(即 either 或 ),则会出现许多示例。例如, - 这可以通过在控制台中键入 来轻松检查,它使用问题的基本方法,并返回而不是 .错误的来源是返回 .[简单地输入也会产生相同的错误]。因此,问题中建议的方法并不总是有效。x = a/100
y = b/100
0.1+1.09
((0.1*100)+(1.09*100)) / (100)
1.1900000000000002
1.19
1.09*100
109.00000000000001
0.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.837117307087383573647963056
1.5**1.5
1.8371173070873836
b
b=b/10
b=float(b)
仍然可以说,时间成本只对执行数十亿次计算的用户来说很重要,大多数用户会优先考虑获得可理解的结果而不是时间差,这在大多数适度的应用程序中是微不足道的。
评论
return ans
justwork(justwork(1, '/', 3), '*', 3)
0.9999999999999998
return ans1
1.0