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の使い方をよくわかっていない、かわいそうな人のたたき台ということでご寛恕いただきたい。