魂の生命の領域

AWS とか Python とか本読んだ感想とか哲学とか書きます

こいついつも Python の話してるな pt.2

どうでも良い話

お仕事では Python しか触らないので、プライベートではフロントエンドのお勉強したいと常に漠然と思い続けています。 ですが、休日はだいたい仕事中に遭遇する、「これ以上深掘りしている時間はないけど気になるなぁ…」という事象をピックアップして検証用のスクリプトを書いたりドキュメントを読んだりすることに費やしています。 まぁその類のお話をします。

本題

for 文でループを回しながら append() でリストをどんどん伸ばしていく処理って割合によく書くと思います。 append() でめちゃ長いリストを作ると遅い、というのはよく聞く話ですが今回はそれではありません。

次のコードをみてください。

result = []
params = {}
for i in list(range(3)):
    params['index'] = i
    result.append(params)

print(result)

これを見て「いや、その書き方はダメだよ」ってなった方はもう読まなくても大丈夫だと思います。

i = 0 から i = 2 までループします。その中で {'index': 0} みたいな dict をどんどん追加していくイメージです。 想定ではこんなリストが出てくるはずでした。

[{'index': 0}, {'index': 1}, {'index': 2}]

実際に実行するとこんな結果になります。

[{'index': 2}, {'index': 2}, {'index': 2}]

なんか変なことになってますね。 ステップ実行するとわかるのですが、こんな感じで result というリストの各要素がループごとに毎回更新されてしまいます。

[{'index': 0}]
[{'index': 1}, {'index': 1}]
[{'index': 2}, {'index': 2}, {'index': 2}]

わかりやすくするためにちょっと処理を足しました。

result = []
id_list = []  # オブジェクトの id を格納するリスト
params = {}
for i in list(range(3)):
    params['index'] = i
    result.append(params)
    id_list.append(id(params))  # params の id を格納する
    print(id_list)  # 都度 print する

実行するとこうなります。

[4319395648]
[4319395648, 4319395648]
[4319395648, 4319395648, 4319395648]

そうです。リストの各要素は同じオブジェクトだったのです。 なので、params['index'] = i の箇所(一番最初のコードの4行目)で params の中身を書き換えると既に result に突っ込んだリストの要素も書き換えられてしまっていた、というオチでした。

よくよく考えれば params という変数は for 文の外で定義しているので、いくら中身を書き換えようとオブジェクト自体は同じものを使ってますもんね。

ちゃんとしたやつ

というわけで、ちゃんと想定した通りの動きにしたい場合はこう書きます。

result = []
for i in list(range(3)):
    params = {'index': i}
    result.append(params)

print(result)

ループごとに毎回 params という変数を宣言して代入しています。 結果はもちろんこうなります。

[{'index': 0}, {'index': 1}, {'index': 2}]

オブジェクトの id を毎回 print するようにしてみましょう。

result = []
id_list = []
for i in list(range(3)):
    params = {'index': i}
    result.append(params)
    id_list.append(id(params))
    print(id_list)

これが結果です。

[4438957952]
[4438957952, 4439990592]
[4438957952, 4439990592, 4439890560]

いいですね。ちゃんと毎回違うオブジェクトを生成しているようです。

まとめ

ちゃんとした版のコードを見ると当然それしか思いつかないように思えて、序盤に書いたようなコードなんで誰も書かんだろ、って気がしてしまうのですが、いきなり序盤のコードを見るとなんとなく問題なく動くような気がしてしまうので注意が必要そうですね。

実際直前の記事 再翻訳で遊びたい 〜AWS Translate 編その2〜 のためのスクリプトを書いてるときに気づかずに踏み抜いてしまってしばらく詰まってました。

補足

ちなみに Python の for 文はスコープを作らないので、こんなことができます。

for i in range(3):
    print(f'inner: {i}')

print(f'outer: {i}')

エラーになりそうですが、 Python の場合はこうなります。

inner: 0
inner: 1
inner: 2
outer: 2

つまり for 文の最後で i = 2 だったのがループを抜けても普通に見に行けます。 ここまで書いて気付きましたが今回の話とは全然関係ありませんでした。 なので終わりにします。 さようなら。