Arantium Maestum

プログラミング、囲碁、読書の話題

PythonのNaN周りの挙動とIEEE754についてのメモ

こういうツイートがあった

「まあ大体IEEE 754のせい」でほぼ終わる話ではあるのだけど、少し深掘りもしたのでちょびっと備忘録メモ。

問題を(少し順番を変えた上で)再掲すると:

x = float('nan')
x == x # False
[x] == [x] # True

y = float('nan')
x == y # False
[x] == [y] # False
[x] == [copy.deepcopy(x)] # True

NaN != NaN

まず

x = float('nan')
x == x # False

に関して。

NaN != NaN浮動小数点数の国際規格であるIEEE 754で決まっている挙動。これがNaN周りの怪しげな挙動の諸悪の根源である。そもそも自己同一性が保たれていないので、直観的な挙動にならなくても仕方ない。

なんでそんな定義になっているのか、という話がStack Overflowに載っていた:

stackoverflow.com

回答者はIEEE 754の委員会メンバーだったとのこと。この挙動を決めた委員会メンバーであるKahanに直接聞いたところ、最大の理由は

there was no isnan( ) predicate at the time that NaN was formalized in the 8087 arithmetic; it was necessary to provide programmers with a convenient and efficient means of detecting NaN values that didn’t depend on programming languages providing something like isnan( ) which could take many years.

というわけで、この仕様が現実的にすぐに使われるためにはどうにかしてNaNチェックができるようにする必要があり、x != xはそのチェックのためのハックだったようだ。isnanがサポートされてしまった今となっては単なる歴史的経緯で、その結果として数十年後もこの微妙な挙動がサポートされ続けているので複雑な気持ちになる。

NaNの入ったリスト

ではなぜ

x == x # False
[x] == [x] # True

のような挙動になるのかというと、Pythonではリスト(というかcontainer)の比較は、要素の値を比較する前にポインタを比較するから。

この挙動に関しては2006年にバグトラッカーで少し議論があった模様:

bugs.python.org

All your example shows is that the oddball NaN is in-fact odd.

I do not want the rest of Python mucked-up just because NaNs are designed to not follow the most basic definitions of equality

とのこと。この判断はまあ妥当に思える。実行速度面から言っても、もし[NaN] != [NaN]を認めるならx = [NaN]; x != xも認める必要が生じ、その結果すべての等価性の判定でデータ構造のすべてを触ってNaNが含まれていないかを調べる必要が生じる。「もしかしたらNaNが含まれているかもしれない」という可能性のために、すべてのデータ構造の等価性チェックにおいて「ノードが同じオブジェクトである」とわかってもチェックを打ち切れなくなる、というのはあまりにも大きなコストだろう。

ちなみにRubyでもNaN入りのリストの挙動は同じようだ:

Rubyのバグトラッカーのチケット:

bugs.ruby-lang.org

こちらの方が様々なオプションを比較して議論していて面白い。結論は同様。

NaNをdeepcopy

だったら

y = float('nan')
x == y # False
[x] == [y] # False
[x] == [copy.deepcopy(x)] # True

という挙動はどういう理屈なのか。

x == yがFalseなのはIEEE 754から。

[x] == [y]がFalseなのはまずポインタが比較され、NaNはインターンされないのでメモリロケーションが違う、そして値の比較もIEEE 754準拠で当然False。

[x] == [copy.deepcopy(x)]で新しくコピーしているのだからポインタ比較はFalseであるべきでは?と思うかもしれないが、copy.deepcopyがコピーするのはミュータブルな構造のみでイミュータブルだと判定できるものに関してはコピーしない。id(x) == id(copy.deepcopy(x))なのでリスト比較はTrueを返す。これも当然イミュータブルな値であれば問題ない挙動なはずなのだが、NaN != NaNのせいで歪みが出る。

終わりに

Pythonバグトラッカーで"nan"と検索すると「minやmaxやsortの挙動がおかしい」などいろいろ面白い話題がある。

bugs.python.org

von Neumannを剽窃して

Any one who considers comparing IEEE 754 floating point numbers in the possible presence of NaNs is, of course, in a state of sin.

と言いたくなる。