2010年9月21日火曜日

なぜPythonの割り算はC言語と違う方式なのか?

今日は何度も「なぜPythonの整数の割り算はC言語のようにゼロに向けて丸めるのではなく、切り捨てなのか?」と聞かれた。

数値が両方とも正の場合には、何も驚くようなことは発生しない。

>>> 5//2
2

しかし、どちらかの数値が負の場合、結果は切り捨てられる。言い換えると、ゼロから遠い方向(負の無限大方向)に向けて丸められる。

>>> -5//2
-3
>>> 5//-2
-3

この挙動は何人かの人を混乱させたようであるが、このような実装になっているのは数学的な根拠がある。整数の割り算の演算子(//)と、その弟の余りを計算する演算子(%)は、次のような数学的な関係を保っている。すべての変数は整数とする。

a/b = q あまり r

これは、次のような関係性を持っている。

b * q + r = a かつ 0 <= r < b
(ただし、a, bはともに0以上の整数)

もしも、この式の関係性を、負の数値まで拡張したいのであれば、2つの選択肢があります。もし商をゼロに向けて丸めるのであれば、付帯条件を0 <= abs(r)と変更する必要がある。それ以外には、qを負の無限大の方に向けて丸める方法がある。この場合、付帯条件は0 <= r < bのままである。

数学の整数論を考えると、数学者はいつも後者の選択を望ましいと考えてきた(Wikipedia参照)。この方式の割り算が役に立つ、おもしろい利用方法がいくつかあったので、Pythonでもこの後者の挙動を採用した。POSIXのタイムスタンプ(1970年からの経過の秒数)を取得して、時刻に変える、という場面を考えてみる。1日は24時間なので、24 * 3600 = 86400秒ある。これを計算するには、単にt % 86400とやれば良い。しかし、1970年依存のより前の時刻を計算したいと考えると、ゼロ方向に丸めてしまうと、無意味な結果が帰ってくることになる。負の無限大方向に丸めるというルールであれば、このような場面でも、そのまま適用できる。

コンピュータグラフィックスで、ピクセルの位置を計算する場面でも、同じ事が言える。こちらの方が良く発生するだろう。

もしbが負の値をであれば、すべてが逆さまになる。普遍条件は次のようになる。

0 >= r > b.

それでは、なぜC言語はこのような実装になっていないのだろうか?それは、Cが設計された当時のハードウェアがこのようにしていなかったからである。そして、現在では負の数は補数として表現されているが、当時のコンピュータは少なくとも整数に関しては、「符号+大きさ」という表現になっていた。私が最初に触れたコンピュータはデータを制御するメインフレームであったが、浮動小数点数と同じく、整数に補数を使うようになっていた。そのハードは負のゼロを表すパターンが60個もあったのである!

Pythonの浮動小数点数を完全に知り尽くしているTim Petersは、これらの規則を浮動小数点数の割り算にも広げたいという私の願望に対して、いくつか心配に思っていることを説明してくれた。彼の心配は正しかった。負の無限大の方向に丸めるというルールは、x%1.0という式があったときに、xが非常に小さい負の整数の時に精度が失われてしまうのである。しかし、これは整数の割り算を破壊するものではなく、//はそれに密に結合されている。

PS. 上記の説明では、/の代わりに//を使用した。これはPython 3の文法で、Python 2でも割り算が整数領域で行われるのを強調したいときに利用できる。Python 2の/演算子は、演算する数が両方整数か、一つ以上浮動小数点数が含まれているかで型が変わるという曖昧なところがある。しかし、これに関してはまったく別の話なので、ここでは説明しない。詳しくはPEP 238に書かれている。

0 件のコメント:

コメントを投稿