2009年4月30日木曜日

整数の割り算の問題

原文:http://python-history.blogspot.com/2009/03/problem-with-integer-division.html
原文投稿者:Guido van Rossum

Pythonの整数の割り算は、初期の間違いが、とてつもなく大きな結果引き起こすという一つの例である。前に書いた記事で触れたが、Pythonは設計時に、ABCで使用されてきた数値の処理方法とは違う方法を採用している。例えば、ABCでは2つの整数の割り算を行うと、正確な有理数で表された結果が返された。しかし、Pythonでは整数の割り算を行うと、整数に丸められた結果が返される。

私の経験上、有理数を返すという方法は、ABCの設計者が期待したほどは成功しなかった。シンプルなビジネスアプリケーションのプログラム(税金の計算)を行ったときのに遭遇した事象が典型的な例である。期待していたよりもずっと処理速度が遅かったのである。デバッグをしたときに内部では何千桁もの精度の有理数の数値が使われていたということに気づいた。しかし、最終的な結果は、有効数字が2桁か3桁に丸められて出力されるのである。この問題は、不正確なゼロ(inexact zero)を追加し始めることで簡単に修正することができたが、直感的とは言えないため初心者にはデバッグするのが困難である。

そのため、Pythonでは、ABCとは異なる、私が慣れ親しんでいたC言語に沿った数値のモデルに頼ることにした。C言語では、整数と浮動小数点数の両方で、さまざまなサイズが存在する。そこで私はPythonの整数の表現にはC言語のlong(少なくとも32ビットの長さは保証される)を利用し、浮動小数点数の表現はC言語のdoubleを利用することにした。また、私が"long"と呼ぶ、任意精度の整数型も追加した。

数値に関する大きな間違いは、高級言語ではなく、C言語に近いルールを採用してしまったことにある。例えば、割り算を含む、標準の数値演算子の結果は、計算に使用したのと同じ型が常に返るようにした。私は最初は、これとはまた別の間違ったルールを使用していた。それは、数値の型を混ぜて使うのを禁止したことである。このルールは型の実装をお互いに独立させるのを目的としていた。そのため、最初のPythonでは、int型にfloat型を足すこともできなかったし、int型とlong型を足すのもできなかったのである。そのため、Pythonが一般向けにリリースされた直後に、Tim Peters氏から、このルールは本当に悪い考え方であるということを納得させられ、通常の型強制ルールに従って数値型を混ぜて計算するモードを導入することになった。例えば、int型とlong型が混ぜて使用された場合には、引数の型をint型からlong型に変換し、計算結果はlong型を返す。また、float型が使用された場合には、int型やlong型の引数はfloat型に変換し、結果はfloat型で返すようになったのである。

しかし、整数の割り算の結果が整数になるというダメージは残っていたのである。もしかしたら「なぜこのことがそんなに悪いことなのか?」と思う方もいるかもしれない。何も問題がないのに、ただ騒いでいるだけなのか?と。歴史的には、この仕様を変更しようとすると、昔から強く反対する人々が必ず現れてきた。彼らは、数値の割り算を学ぶことは、すべてのプログラマが「通過する儀式」であると信じているのである。そのような方もいるため、なぜこの設計がバグであるかと考えている理由を説明していこうと思う。

もし、例えば、月の満ち欠けの計算などである数値計算を行う関数を実装していたとしよう。通常なら、引数として浮動小数点数を指定したいと思うだろう。しかし、Pythonでは型宣言がないため、呼び出し側が整数の引数を渡して呼び出すことを妨げることはできない。C言語のような静的な型を持つ言語であれば、コンパイラが強制的に型変換をしてfloatにするが、Pythonではそのようなことはない。数値を混ぜるルールによって中間結果がfloat型に変換されるまでは整数型で計算されることになる。

割り算以外の演算子の場合は、整数は浮動小数点数と同じ振る舞いをする。例えば、1 + 1は2になるし、1.0 + 1.0は2.0になる。そのため、引数が整数か浮動小数点数かに関わらず、数値計算のアルゴリズムが問題なく動作するという誤解が簡単に生じてしまうのである。しかし、計算に割り算が含まれていて、入力される数値が両方とも整数になる場合には、暗黙のうちに結果の切り捨てが発生する。そのため、計算結果に大きな問題が入り込む可能性が本質的に含まれるのである。すべての引数を入力の時点で浮動小数点数に変換するという防衛的なコードを書くのも面倒な作業であるし、コードの可読性とメンテナンス性を下げることになる。かなり特殊なケースではあるが、それに加え、同じアルゴリズムに対して、複素数を入力して計算することを妨げることにもなってしまうのである。

繰り返すと、Pythonが、宣言された型への引数の型変換を自動的に行わないのが問題の原因である。例えば、文字列のような適切でない引数が渡されると、掛け算以外の演算は文字列と数値を混合して扱うことができないため、すぐに問題の発生が特定できる。しかし、浮動小数点数が期待されているアルゴリズムに整数が渡された場合には、正解に近いがエラーを含む結果を返すことになる。中途半端に正確なため、デバッグをしたり、問題に気づくのが難しいのである。最近、アナログ時計を描くプログラムで、切り捨てのために針の位置の計算がおかしくなるという問題が発生した。しかし、一日に何回かしか問題に気づくことはできなかった。

整数の割り算を修正するのは簡単なタスクではない。というのも、整数の割り算の結果が整数になるというのを期待して作成されたプログラムもあるからである。今までの割り算と同じ機能を提供する演算子(//)もPythonに追加された。それに加え、新しい整数の割り算の機能を追加する("from __future__ import division")というメカニズムも提供され、使用できるようになっている。振る舞いを変更し、プログラムの変換の手助けをするコマンドラインフラグの(-Qxx)が提供されている。さらに幸運なことに、この正しい振る舞いは、Python3000では標準的な動作となるのである。

0 件のコメント:

コメントを投稿