Heim  >  Artikel  >  Backend-Entwicklung  >  Python 为什么不解决四舍五入(round)的“bug”?

Python 为什么不解决四舍五入(round)的“bug”?

WBOY
WBOYOriginal
2016-06-06 16:22:592154Durchsuche

回复内容:

因为二进制浮点数不能解决这个问题。

先看一个现象,和 round 无关的:
<code class="language-python"><span class="o">>>></span> <span class="k">def</span> <span class="nf">show</span><span class="p">(</span><span class="n">x</span><span class="p">):</span>
<span class="o">...</span>     <span class="s">"""打印一个数,20 位精度"""</span>
<span class="o">...</span>     <span class="k">print</span><span class="p">(</span><span class="s">'{:.20f}'</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">x</span><span class="p">))</span>
<span class="o">...</span>
<span class="o">>>></span> <span class="n">show</span><span class="p">(</span><span class="mf">1.5</span><span class="p">)</span>
<span class="mf">1.50000000000000000000</span>
<span class="o">>>></span> <span class="n">show</span><span class="p">(</span><span class="mf">1.25</span><span class="p">)</span>
<span class="mf">1.25000000000000000000</span>
<span class="o">>>></span> <span class="n">show</span><span class="p">(</span><span class="mf">1.245</span><span class="p">)</span>
<span class="mf">1.24500000000000010658</span>
<span class="o">>>></span> <span class="n">show</span><span class="p">(</span><span class="mf">1.45</span><span class="p">)</span>
<span class="mf">1.44999999999999995559</span>
<span class="o">>>></span> <span class="n">show</span><span class="p">(</span><span class="mf">1.415</span><span class="p">)</span>
<span class="mf">1.41500000000000003553</span>
</code>
四舍五入是基于十进制的,在二进制无法精确表示的时候是会有误差的。
任何需要十进制运算的地方,都需要用 decimal.Decimal 取代 float:
<code class="language-python"><span class="o">>>></span> <span class="n">Decimal</span><span class="p">(</span><span class="mf">1.45</span><span class="p">)</span>
<span class="n">Decimal</span><span class="p">(</span><span class="s">'1.4499999999999999555910790149937383830547332763671875'</span><span class="p">)</span>
<span class="o">>>></span> <span class="n">Decimal</span><span class="p">(</span><span class="s">'1.45'</span><span class="p">)</span>
<span class="n">Decimal</span><span class="p">(</span><span class="s">'1.45'</span><span class="p">)</span>
<span class="o">>>></span> <span class="n">Context</span><span class="p">(</span><span class="n">prec</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">rounding</span><span class="o">=</span><span class="n">ROUND_HALF_UP</span><span class="p">)</span><span class="o">.</span><span class="n">create_decimal</span><span class="p">(</span><span class="s">'1.45'</span><span class="p">)</span>
<span class="n">Decimal</span><span class="p">(</span><span class="s">'1.5'</span><span class="p">)</span>
<span class="o">>>></span> <span class="n">Decimal</span><span class="p">(</span><span class="s">'1.45'</span><span class="p">)</span><span class="o">.</span><span class="n">normalize</span><span class="p">(</span><span class="n">Context</span><span class="p">(</span><span class="n">prec</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">rounding</span><span class="o">=</span><span class="n">ROUND_HALF_UP</span><span class="p">))</span>
<span class="n">Decimal</span><span class="p">(</span><span class="s">'1.5'</span><span class="p">)</span>
<span class="o">>>></span> <span class="n">Decimal</span><span class="p">(</span><span class="n">Decimal</span><span class="p">(</span><span class="s">'1.45'</span><span class="p">)</span><span class="o">.</span><span class="n">quantize</span><span class="p">(</span><span class="n">Decimal</span><span class="p">(</span><span class="s">'.1'</span><span class="p">),</span> <span class="n">rounding</span><span class="o">=</span><span class="n">ROUND_HALF_UP</span><span class="p">))</span>
<span class="n">Decimal</span><span class="p">(</span><span class="s">'1.5'</span><span class="p">)</span>
</code>
Note that this is in the very nature of binary floating-point: this is not a bug in Python, and it is not a bug in your code either. You’ll see the same kind of thing in all languages that support your hardware’s floating-point arithmetic (although some languages may not display the difference by default, or in all output modes).
  • 某些十进制数(如0.1)在机器内部无法用有限数目的二进制位0和1精确表示,只能通过增加固定的位数来提高精度,从而更逼近原来十进制数。

round(number[, ndigits])
Return the floating point value number rounded to ndigits digits after the decimal point. If ndigits is omitted, it defaults to zero. The result is a floating point number. Values are rounded to the closest multiple of 10 to the power minus ndigits; if two multiples are equally close, rounding is done away from 0.
  • python的round函数定义为 在任意整数*10^(-ndigits) 中取最靠近number的数值,如果有两个整数距离number相等则按照远离0的一侧(负数-0-正数)取值 。
<code class="language-text">round(0.5) is 1.0 and round(-0.5) is -1.0)
</code>
题主测试的和那个bug无关。round是四舍六入五成双的。 python 的官方实现确实有问题,至于为什么作者不改进,原作者可能是什么心态,可以参见刘海洋的答案。大致心态也许是:反正这个错误不在我,所以虽然有方法,但我也不会去解决这个问题。

咋一看,很多人会把这个问题理解为浮点数不精确的问题。浮点数不精确,这一点是常识,是对的。

但浮点数并非在所有情况下都不精确,也并非「只要浮点数不精确,所涉及的相关计算问题就毫无解决的价值」。

Python 的 round 问题是个典型的例子,推测作者的观点是认为这类与浮点数精确度有关的问题没有解决的必要。如同 @刘海洋 的观点一样,所以导致了现在的结果。

不过仔细分析会发现,0.5 这种浮点数是可以被精确表示的,而 round 这个函数的特定性在于,round 舍入之后的精确性毫无意义。所以 round 这个问题本身造成的不精确性是可以被解决的。

对于 round 的不精确性,重要的在于结果是进一还是去尾。我写了一个简单的例子来说明这个问题,当然这个函数在特定情况下也不准确,不过在题目给出的情况下都可以得到正确的结果。

<code class="language-c"><span class="cp">#include <stdio.h></stdio.h></span>

<span class="kt">float</span> <span class="nf">my_round</span><span class="p">(</span><span class="kt">float</span> <span class="n">src</span><span class="p">,</span> <span class="kt">int</span> <span class="n">idx</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">int</span> <span class="n">i</span><span class="p">;</span>
    <span class="k">for</span> <span class="p">(</span><span class="n">i</span><span class="o">=</span><span class="n">idx</span><span class="p">;</span><span class="n">i</span><span class="o">--</span><span class="p">;)</span>
        <span class="n">src</span> <span class="o">*=</span><span class="mi">10</span><span class="p">;</span>
    <span class="kt">float</span> <span class="n">dest</span> <span class="o">=</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">src</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">src</span> <span class="o">>=</span> <span class="n">dest</span><span class="o">+</span><span class="mf">0.5</span><span class="p">)</span>
        <span class="n">dest</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">;</span>
    <span class="k">for</span> <span class="p">(</span><span class="n">i</span><span class="o">=</span><span class="n">idx</span><span class="p">;</span><span class="n">i</span><span class="o">--</span><span class="p">;)</span>
        <span class="n">dest</span> <span class="o">/=</span><span class="mi">10</span><span class="p">;</span>
    <span class="k">return</span> <span class="n">dest</span><span class="p">;</span>
<span class="p">}</span>


<span class="kt">int</span> <span class="nf">main</span><span class="p">()</span>
<span class="p">{</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"result=%f</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">my_round</span><span class="p">(</span><span class="mf">1.5</span><span class="p">,</span> <span class="mi">0</span><span class="p">));</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"result=%f</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">my_round</span><span class="p">(</span><span class="mf">1.25</span><span class="p">,</span> <span class="mi">1</span><span class="p">));</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"result=%f</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">my_round</span><span class="p">(</span><span class="mf">1.245</span><span class="p">,</span> <span class="mi">2</span><span class="p">));</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"result=%f</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">my_round</span><span class="p">(</span><span class="mf">1.45</span><span class="p">,</span> <span class="mi">1</span><span class="p">));</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"result=%f</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">my_round</span><span class="p">(</span><span class="mf">1.415</span><span class="p">,</span> <span class="mi">2</span><span class="p">));</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"result=%f</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">my_round</span><span class="p">(</span><span class="mf">2.675</span><span class="p">,</span> <span class="mi">2</span><span class="p">));</span>
<span class="p">}</span>
</code>
两个问题,一个是
浮点数在计算机中的真实存储(这里是在 Python 中的存储)
<code class="language-text">expect:  1.0
actual:  1
expect:  1.1
actual:  1.100000000000000088817841970012523233890533447265625
expect:  1.2
actual:  1.1999999999999999555910790149937383830547332763671875
expect:  1.3
actual:  1.3000000000000000444089209850062616169452667236328125
expect:  1.4
actual:  1.399999999999999911182158029987476766109466552734375
expect:  1.5
actual:  1.5
expect:  1.6
actual:  1.600000000000000088817841970012523233890533447265625
expect:  1.7
actual:  1.6999999999999999555910790149937383830547332763671875
expect:  1.8
actual:  1.8000000000000000444089209850062616169452667236328125
expect:  1.9
actual:  1.899999999999999911182158029987476766109466552734375
</code>
很简单,因为2.675在表示的时候可能是2.6749,所以round以后还是2.67了 你的需求本质是:精确小数运算。然而,float不是为了满足这一需求而设计的,decimal才是。所以,为float单独定制一个round,不符合float的设计意图,也很难实现。以你的函数为例,temp*10这个操作在float下不是精确的。

>>> 1.222*10
12.219999999999999 我的意思是为啥不写个可以解决这个问题的函数:例如(仅仅一个例子,可能从性能啊之类肯定不行,官方是出于什么原因不写一个类似的)
def myround(par,l):
temp = 1
for i in range(l):
temp*=10
v = int((par+0.5/temp)*temp) / temp
return v

i = 1.25
print(myround(i,1))
i = 1.245
print(myround(i,2))
i = 1.21
print(myround(i,1))
i = 1.249
print(myround(i,2))

----
1.3
1.25
1.2
1.25 他的解释应该说的是这个不是个bug。默认数值都是float,float本来就不精确,所以python应该是只对decimal精确。我觉得这个也挺正常的。
Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn