とあるプログラミングで解くクイズ的なサイトで遊んでいると、奇妙なことが起きた。
そのサイトでは回答に使える言語が豊富にあり、クイズの正誤を複数のテストケースの通過で判定する。
Python 2 で解いた問題を Python 3 用にほんの少しだけ書き換えて提出したところだった。
10 個ほどあるテストケース。
そのうちの 1 個だけが通過できずに失敗する。
なんでだろう。
単純に print ANSWER
を print(ANSWER)
に書き換えただけである。その程度の変更で問題ないと思っていた。
なぜ?
どこかに知らない癖があるのだろうか。
その問題はひとつのテストケースでも相当な数の計算の答えを要求される。その上で通過に失敗するテストケースはひとつなのだから、計算の根っこの仕様が変わっているわけではなさそうだ。
癖…。
なんとなく怪しげなのは四捨五入だった。
環境
- Python 2.7.11
- Python 3.5.1
参考
- Language differences and workarounds — Supporting Python 3 - The Book Site
-
端数処理 - Wikipedia
- 最近接偶数への丸め の項目
四捨五入の仕様が違っていた
検索したらあっさり出てきた。
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) がそれにあたる。
肝となる部分は以下。
z = round(y);
if (fabs(y-z) == 0.5)
/* halfway between two integers; use round-away-from-zero */
z = y + copysign(0.5, y);
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 に詳しいわけじゃないのと、やはりなんとなく気になって、上記の部分から比較するコードを書いてみる。
#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。