LoginSignup
29
22

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-08-28

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

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

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。

29
22
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
22