Tea break

ちょっとした息抜きに

Pythonでやらかした話とNaNの話

近畿大学 Advent Calendar 2019の10日目 軽い気持ちで始めたアドベントカレンダー、全日埋まってとてもビックリしています。 参加していただいた方々には感謝の気持ちでいっぱいです、ありがとうございます。

この記事では、私がPythonを触っていてやらかしたところを公開します。あと、NaNについてハマった時に調べたものも一応残しておきます。 自分の振り返りと皆さんの反面教師になれればと思います。

==とis

PythonになれてきてからPythonicな書き方を心がけていく中、知り合いに「Pythonではisで等価判定したほうがオシャレ!」と言われ、==isに全て置換したことがあります。 そうすると一見正しそうに動くのですが...

==とisの挙動の違い ==は値が等しいかどうかを判定する isはオブジェクトidが等しいかどうかを判定する

Pythonの文字列やリストは同値でも、オブジェクトidが違う場合があります。(文字列は日本語が入るとisではおかしくなる気がする)なので、isで判定をすると同値でもFalseが返ってきてハマります。

left = "pythonやらかし"
# ==判定
left == "pythonやらかし"
# > True
# is判定
left is "pythonやらかし"
# > False

2次元配列の参照コピー

Pythonは文字列や配列を*で掛けることができます。

"オラ" * 10
# 'オラオラオラオラオラオラオラオラオラオラ'
[0] * 10
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

しかし、これで二次元配列で生成すると参照コピーの罠にハマります。 これ気づかずにめっちゃ時間溶かしました... 横着せずにリスト内包表記などで書くと良いと思います。

two_d_list = [[0] * 10] * 10
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

# [0,0]の値だけ1足したい
two_d_list[0][0] += 1
# [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]

for文

PythonのCなどと違って、for文はシーケンス型を反復する仕組みです。なので、以下のようなことをするとハマります。 1. iのようなターゲットリストを途中で変更しても反映されない 2. ループ中のシーケンスオブジェクトに内部からappendすると無限ループになる

1つ目に関しては諦めるしかないと思います... 2つ目はスライス表記を用いることで解決できます

num_list = list(range(1, 4))
# ターゲットリストを途中でいじっても意味がない
for i in num_list:
  print(i)
  i += 1
# 1
# 2
# 3

# スライスを用いてループ中にappendする
for i in num_list[:]:
  num_list.append(i)
# [1, 2, 3, 1, 2, 3]

Pythonインタプリタ

ちょっとした計算や何か試したいことがあれば、ターミナルからPythonのインタプリンタを呼び出すことがよくあります。 しかし、一度エンターを押してしまうと、後戻りが出来ず、「あっ、変数名間違えた」「あっ、インデント間違えた」といったやらかしがよくあります。なのでipythonを使うようになりました。ipythonはjupyter notebookでおなじみのセルでコードを実行する環境でコマンドで実行できる対話環境としてデフォルトのインタプリンタより優れているのでおすすめです。

バージョン3.4以下のgcd関数のモジュール

これは主にAtCoderに関連する話なのですが、Pythonのバージョンが3.4以下だとgcd関数がmathモジュールにありません(現在のAtCoderのPython3のバージョンは3.4)

mathモジュールにgcd関数が置かれるのは3.5以降で、それ以前のバージョンだとfractionsモジュールにあるのでそれを使用しなければなりません。些細なやらかしかもしれませんが、一分一秒の時間が惜しい競プロでは結構なやらかしでした。

# 3.4以下はこれが使えない
from math import gcd
# 3.4以前はこっちを使う(3.5以降は非推奨)
from fractions import gcd

NaN

a bit pattern of a single-precision or double-precision real number data type that is a result of an invalid floating point operation. 訳: 無効な浮動小数点演算の結果である単精度または倍精度の実数データ型のビットパターン IEEE 754より

PandasでNaNを判定する関数をずっとdf.isnan()だと思っていてハマりました(?) 実際にはdf.isnull()です。個別で判別する場合は、math.isnan(), numpy.isnan()も有効です。

おまけ(むしろ本題)

np.nan == np.nan # is always False! Use special numpy functions instead.

NaNはあらゆるものとの数値比較、等価演算でFalseを返すのは特性がありますが(自分自身との比較でFalseを返すのもこの特性から)、参照先で比較するisは数値比較ではないので、この原則に当てはまりません。 しかし、Pythonにはmath.nanとnumpy.nanの二種類のNaNがあり、これらはオブジェクトidが違います。

# isを使ったNaN同士の比較
math.nan is math.nan
# True

# mathモジュールのNaN
id(math.nan)
# 4538852576
# numpyライブラリのNaN
id(np.nan)
# 4569389960

ですが、これらはそれぞれのNaNを別々のライブラリの関数で正しく判定できています。

# それぞれのisnan()関数でNaNが正しく判定できる
math.isnan(np.nan) and np.isnan(math.nan)
# True

どのような実装になっているのか気になったので調べてみました。

math.isnanの実装

math.nanの実装は、cpythonより、Cの実装に従っていると思われます。 そのCの実装はこちらのサイトを参照しました。 - https://ja.cppreference.com/w/c/numeric/math/isnan)

#define Py_IS_NAN(X) isnan(X)

numpy.nanの実装

NumPy core libraries よりC99の実装に従うことがわかる

.. c:function:: int npy_isnan(x) This is a macro, and is equivalent to C99 isnan: works for single, double and extended precision, and return a non 0 value is x is a NaN.

つまりオブジェクトidは違うが元の実装がCで同じなので、同じように動くということでした。なんとなく予想はついていましたが、実際に確認できるとやはり嬉しいですね!

おわりに

アドカレ期限ギリギリになってしまいましたが、これも全てバックアップを阻害して、25G程度のバックアップに3日もかけるようになったiCouldが悪い