Python3
difference
Python2

Python 2 と Python 3 では四捨五入の仕様が違う

More than 1 year has passed since last update.

とあるプログラミングで解くクイズ的なサイトで遊んでいると、奇妙なことが起きた。

そのサイトでは回答に使える言語が豊富にあり、クイズの正誤を複数のテストケースの通過で判定する。

Python 2 で解いた問題を Python 3 用にほんの少しだけ書き換えて提出したところだった。

10 個ほどあるテストケース。

そのうちの 1 個だけが通過できずに失敗する。

なんでだろう。

単純に print ANSWERprint(ANSWER) に書き換えただけである。その程度の変更で問題ないと思っていた。

なぜ?

どこかに知らない癖があるのだろうか。

その問題はひとつのテストケースでも相当な数の計算の答えを要求される。その上で通過に失敗するテストケースはひとつなのだから、計算の根っこの仕様が変わっているわけではなさそうだ。

癖…。

なんとなく怪しげなのは四捨五入だった。


環境


  • Python 2.7.11

  • Python 3.5.1


参考


四捨五入の仕様が違っていた

検索したらあっさり出てきた。


Python 2

Python 2 では値を 0 から遠ざける形で値を丸める。

>>> round(1.5)

2.0
>>> round(2.5)
3.0

日本で一般的な四捨五入だ。


Python 3

Python 3 では丸めた先の値が偶数になるように値を丸める。

>>> round(1.5)

2
>>> round(2.5)
2

Wikipedia によると、このやり方だと値の正負による誤差が小さいらしい。


その他の違い

Python 2 と Python 3 では出力されている数値の型も違うようだ。

Python 3 の round 関数では第 2 引数で丸める桁の位置を指定できる。省略すると、結果が integer 型になる。小数点以下を削った上で、結果を float 型で欲しい場合は 0 をつける。

>>> round(2.5, 0)

2.0


ソースでの違い

なんとなく気になって Python のソースを見比べてみた。

round の処理は Objects/floatobject.c の double_round 系の関数らしい。

表現を「系」としているのは微妙に名前が違うからで、 Python 2 では _Py_double_round(double, int) で Python 3 では double_round(double, int) がそれにあたる。

肝となる部分は以下。


Python-2.7.11/Objects/floatobject.c

z = round(y);

if (fabs(y-z) == 0.5)
/* halfway between two integers; use round-away-from-zero */
z = y + copysign(0.5, y);


Python-3.5.1/Objects/floatobject.c

z = round(y);

if (fabs(y-z) == 0.5)
/* halfway between two integers; use round-half-even */
z = 2.0*round(y/2.0);

要点だけを取り出したので一部補足。

C の round 関数は整数への変換にしか対応しておらず、その変換仕様は Python 2 と同じ round-away-from-zero である。

y は その整数化されてしまう仕様から、事前に四捨五入する桁に合わせて小数点の位置を動かしたものだ。例えば小数点以下 2 位に丸めたい場合 1.259 は 125.9 に調整される。

z は round 関数を使い y を整数化したもの。

y と z の差が絶対値で 0.5 のときに、それぞれ違った処理になる。

へえ。


確かめてみる

C に詳しいわけじゃないのと、やはりなんとなく気になって、上記の部分から比較するコードを書いてみる。


test_roundings.c

#include<stdio.h>

#include<math.h>

double round_half_even(double d) {
return 2.0 * round(d / 2.0);
}

double round_away_from_zero(double d) {
return d + copysign(0.5, d);
}

void print_rounds(double d) {
printf("(%lf)\n", d);
printf("half even: %lf\n", round_half_even(d));
printf("away from zero: %lf\n", round_away_from_zero(d));
printf("\n");
}

int main() {
print_rounds(0.5);
print_rounds(1.5);

return 0;
}


コンパイルして実行。

(0.500000)

half even: 0.000000
away from zero: 1.000000

(1.500000)
half even: 2.000000
away from zero: 2.000000

うん、OK。