Pythonのどうしてこうなるの?トリッキーコード集 (part 1)

satwikkansal/wtfPython: A collection of subtle and tricky Python examples

原文のライセンスは煮るなり焼くなり自由なアレ

前文雑要約

  • Pythonはいい言語。
  • でも初心者には一見わかりにくい挙動をすることもある。
  • ここでは古典的でトリッキーな例を集めた。
  • トリッキーといっても大したことのない例もわりとある。
  • 経験を積んだPythonプログラマならだいたい知っているはず。

目次

使い方

(ここだけ敬体)

だいたい上から順に読むとよいでしょう。

  • 答えを見る前に(出力も見ずに)予測しながらコードを読んでください。
    • 経験豊かなPythonistaなら何が起きるか見抜けるでしょう。
  • それから出力が予想と正しいか確かめましょう。
  • どうしてそのような出力になったか説明してください。

代入を無視するインタプリタ

>>> 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)はハッシュ値を計算する際にfloat5.0に変換されている。

これは等しい値(5 == 5.0)のハッシュは等しくなること(hash(5) == hash(5.0))を要請することから来る問題である*2Python固有の問題ではない。

詳しくは以下。

python - Why can a floating point dictionary key overwrite an integer key with the same value? - Stack Overflow

評価タイミングの食い違い

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節以降は実行時に評価される。

この例だと、ジェネレータ内部のx1, 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]という出力はなんなのか?

リストの反復はインデックスごとに行われる。1list_2またはlist_4から削除したとき、リストの中身は[2, 3, 4]となっている。つまり残った要素はシフトダウンされ、2はインデックス0に、3はインデックス1より…といった具合になる。次のループではインデックスは1となり、これは3を削除する。2に対する操作は完全にスキップされる。以下同様に4への操作もスキップされる。

Stack Overflowのこのスレに辞書に関する同様の例が説明されている。

日記

まだたくさんあるが今日はここまで。正直翻訳というほどのものでもないし、やる気もないのだが、プチプチを潰すような暇つぶし感覚でやっている。というか本家がまだ頻繁に更新されているのでGitHubで管理したほうがいいというのはある。ブログでやってるのはgitの使い方をよくわかっていない、かわいそうな人のたたき台ということでご寛恕いただきたい。

*1:原文は"Python" destroyed the existence of “JavaScript”?という小洒落た煽りだが、無視で。

*2:9月2日現在説明が不十分というIssueが立っている

*3:実装依存。Yes, it runs for exactly eight times and stopsと述べられているが、私の環境では5回のループで止まった