Python2系と3系の四捨五入round()の動作の違い

はじめに

こんなコードがある。

for i in range(10):
    num = i+0.5
    print("round(" + str(num) + ") = " + str(round(num)))

これは、0.5から9.5までの値を四捨五入して出力するだけの関数である。
round関数は、引数の値を四捨五入する関数。
これをpython2系とpython3系でそれぞれ実行すると、以下の出力が得られる。

python2系

[shell] round(0.5) = 1.0
round(1.5) = 2.0
round(2.5) = 3.0
round(3.5) = 4.0
round(4.5) = 5.0
round(5.5) = 6.0
round(6.5) = 7.0
round(7.5) = 8.0
round(8.5) = 9.0
round(9.5) = 10.0
[/shell]

python3系

[shell] round(0.5) = 0
round(1.5) = 2
round(2.5) = 2
round(3.5) = 4
round(4.5) = 4
round(5.5) = 6
round(6.5) = 6
round(7.5) = 8
round(8.5) = 8
round(9.5) = 10
[/shell]

あれっ…Python2系3系で出力が違う…。特にPython3系は思ってたのと違ってなんだかハマりそう。。
結論から言えば、これはバグでもなんでもなくて仕様です。この仕様についてちょっと調べたのでまとめておきます。

 

環境

  • Python2系: 2.7.15
  • Python3系: 3.7.0

(それぞれ、2018.9.21現在、pyenvで構築できるそれぞれの最新の安定バージョン)

 

仕様について

わからんので、とりあえず公式マニュアルを参照してみる。

Python2系(2.7)でのround関数の説明
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 (so, for example, round(0.5) is 1.0 and round(-0.5) is -1.0).<span class="su-quote-cite"><a href="https://docs.python.org/2.7/library/functions.html#round" target="_blank">2. Built-in Functions — Python 2.7.15 documentation</a></span>

 

Python3系(3.6)でのround関数の説明
Return number rounded to ndigits precision after the decimal point. If ndigits is omitted or is None, it returns the nearest integer to its input. For the built-in types supporting round(), values are rounded to the closest multiple of 10 to the power minus ndigits; if two multiples are equally close, rounding is done toward the even choice (so, for example, both round(0.5) and round(-0.5) are 0, and round(1.5) is 2). Any integer value is valid for ndigits (positive, zero, or negative). The return value is an integer if ndigits is omitted or None. Otherwise the return value has the same type as number.<span class="su-quote-cite"><a href="https://docs.python.org/3.6/library/functions.html#round" target="_blank">2. Built-in Functions — Python 3.6.6 documentation</a></span>

うん。英語があんまり得意ではないので、完璧には原理を理解できませんが…小数点を2進数で示した際に正確に表す事ができない事が原因のようです。それぞれの説明のあとにNoteとしPythonの浮動小数点演算、その問題と制限を参照してくださいと書いてあります。こちらを参照すると、まぁよくある 0.1+0.1+0.1 != 0.3 みたいな話が書いてあります。

なぜ、Python2系でもほぼ同様の話がなされていますが…round関数の仕様からなぜ2系と3系で出力が異なるのかといったことに対する結論は見つけられませんでした。。。

 

解決策(案)

roundの仕様から正確な原因はわかりませんが、このままでは困る事がありそうなので解決策を示しておきたいと思います。

小数点以下1桁を四捨五入した際の0.5の扱いが怪しい…ということで、0.5を加算したあとに小数点以下を切り捨てする事で期待する動作をpython2系でも3系でも実現することができました。

import math
for i in range(10):
    num = i+0.5
    print("round(" + str(num) + ") = " + str(math.floor(num+0.5)))
    # num+0.5 : 0.5を加算... 0.5=>1.0, 1.5=>2.0 みたいな。
    # math.floor : 切り上げ

Python2系

[shell] round(0.5) = 1.0
round(1.5) = 2.0
round(2.5) = 3.0
round(3.5) = 4.0
round(4.5) = 5.0
round(5.5) = 6.0
round(6.5) = 7.0
round(7.5) = 8.0
round(8.5) = 9.0
round(9.5) = 10.0
[/shell]

Python3系

[shell] round(0.5) = 1
round(1.5) = 2
round(2.5) = 3
round(3.5) = 4
round(4.5) = 5
round(5.5) = 6
round(6.5) = 7
round(7.5) = 8
round(8.5) = 9
round(9.5) = 10
[/shell]

うぬ。期待していた出力が出てきてくれた気がする。よしよし。

ついでに、python3系ではround関数の引数に桁数を指定しないと少数第一位を四捨五入して整数にまとめるのですが、第二引数に0を指定すると戻り値が少数になってpython2系と同様になります。

round(1.5)   # 戻り値は 2
round(1.5,0) # 戻り値は 2.0 

 

まとめ

う〜む。あんまり爽快じゃないですが…とりあえず解決って感じです。Pythonに強い方がおられましたら2系と3系で異なる理由を教えてもらえると嬉しいです。とりあえず解決ってことで!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください