satwikkansal/wtfPython: A collection of subtle and tricky Python examples
原文のライセンスは煮るなり焼くなり自由なアレ
前文雑要約
- Pythonはいい言語。
- でも初心者には一見わかりにくい挙動をすることもある。
- ここでは古典的でトリッキーな例を集めた。
- トリッキーといっても大したことのない例もわりとある。
- 経験を積んだPythonプログラマならだいたい知っているはず。
目次
- 前文雑要約
- 目次
- 使い方
- 代入を無視するインタプリタ?
- うさんくさいコード(Python 2.x系限定)
- ちょっとこのハッシュヤバくね?
- 評価タイミングの食い違い
- 辞書をイテレート中に変更する
- リストの要素をイテレート中に削除する
使い方
(ここだけ敬体)
だいたい上から順に読むとよいでしょう。
- 答えを見る前に(出力も見ずに)予測しながらコードを読んでください。
- 経験豊かなPythonistaなら何が起きるか見抜けるでしょう。
- それから出力が予想と正しいか確かめましょう。
- どうしてそのような出力になったか説明してください。
- わからない場合は解説を読み、それでもわからない場合はissueを立てて下さい。
代入を無視するインタプリタ?
>>> value = 11 >>> valuе = 32 >>> value 11
解説
2行目のvaluеのеはキリル文字(unicode)である。つまり1行目と2行目は違う変数である。
うさんくさいコード(Python 2.x系限定)
def square(x): sum_so_far = 0 for counter in range(x): sum_so_far = sum_so_far + x return sum_so_far print(square(10)) # => 10
3系では動かないというのがヒント。
解説
タブ文字とスペースが混ざっている。具体的にはreturn
部のインデントがタブになっている。
Pythonではタブ文字は8つのスペースに置き換えられる。なのでreturn sum_so_far
はループ内部に入る。
Python 3.xは、タブ文字とスペースの混用に対してエラーを出す。
ちょっとこのハッシュヤバくね?
some_dict = {} some_dict[5.5] = "Ruby" some_dict[5.0] = "JavaScript" some_dict[5] = "Python"
出力
>>> some_dict[5.5] "Ruby" >>> some_dict[5.0] "Python" >>> some_dict[5] "Python"
なぜ"JavaScript"
が消えてしまうのか?*1
解説
キー5
(int
)はハッシュ値を計算する際にfloat
の5.0
に変換されている。
これは等しい値(5 == 5.0
)のハッシュは等しくなること(hash(5) == hash(5.0)
)を要請することから来る問題である*2。Python固有の問題ではない。
詳しくは以下。
評価タイミングの食い違い
array = [1, 8, 15] g = (x for x in array if array.count(x) > 0) array = [2, 8, 22]
出力
>>> print(list(g)) [8]
解説
ジェネレータ内包では、in
節は宣言時に評価され、if
節以降は実行時に評価される。
この例だと、ジェネレータ内部のx
は1, 8, 15
と評価される。しかしif節のarray
は[2, 8, 22]
に再割り当てされているので、8
だけが1つカウントされる。ゆえにジェネレータは8だけを返す。
辞書をイテレート中に変更する
x = {0: None} for i in x: del x[i] x[i+1] = None print(i)
出力
0 1 2 3 4 5 6 7
8回ループして止まる*3。
解説
- まず辞書をイテレーション中に編集してはいけない。
- 8回エントリを削除した時点で、より多くのキーを保持するために辞書をリサイズしている。実装詳細の話であり言語仕様ではない。
- 類似例についてStack Overflowのスレで解説されている。
(興味ぶかいが、なんだかよくわからない…)
リストの要素をイテレート中に削除する
list_1 = [1, 2, 3, 4] list_2 = [1, 2, 3, 4] list_3 = [1, 2, 3, 4] list_4 = [1, 2, 3, 4] for idx, item in enumerate(list_1): del item for idx, item in enumerate(list_2): list_2.remove(item) for idx, item in enumerate(list_3[:]): list_3.remove(item) for idx, item in enumerate(list_4): list_4.pop(idx)
出力
>>> list_1 [1, 2, 3, 4] >>> list_2 [2, 4] >>> list_3 [] >>> list_4 [2, 4]
特にlist_2
, list_4
の挙動を推察できるだろうか?
解説
大前提として、イテレート中にリストを変更するのはよくない。こうしたことをしたい場合、スライス記法some_list[:]
でリストをコピーすべきである。
del
, remove
, pop
の挙動の違い
remove
は最初にマッチした値を削除する。特定のインデックスを指し示すものではないので、値が存在しないとValueError
を発生させる。del
は特定のインデックスを削除する(これがlist_1
が影響を受けない理由である)。不正なインデックスが指定されたときはIndexError
を発生させる。pop
は特定のインデックスの値を削除しその値を返す。不正なインデックスが指定されたときはIndexError
を発生させる。
[2, 4]
という出力はなんなのか?
リストの反復はインデックスごとに行われる。1
をlist_2
またはlist_4
から削除したとき、リストの中身は[2, 3, 4]
となっている。つまり残った要素はシフトダウンされ、2
はインデックス0に、3
はインデックス1より…といった具合になる。次のループではインデックスは1となり、これは3
を削除する。2
に対する操作は完全にスキップされる。以下同様に4
への操作もスキップされる。
Stack Overflowのこのスレに辞書に関する同様の例が説明されている。
日記
まだたくさんあるが今日はここまで。正直翻訳というほどのものでもないし、やる気もないのだが、プチプチを潰すような暇つぶし感覚でやっている。というか本家がまだ頻繁に更新されているのでGitHubで管理したほうがいいというのはある。ブログでやってるのはgitの使い方をよくわかっていない、かわいそうな人のたたき台ということでご寛恕いただきたい。