イテレート中のリスト変更で要素が飛ばされる
forループでリストを走査しながら同じリストからremove()すると、要素の位置がずれて一部が処理から漏れます。
なぜエラーが出ないのか
出力: [1, 2, 3]
(エラーなし)- Pythonは何も報告しません。文法として正しいためです
出力: [1, 2, 3]- 実際の挙動 — 期待した結果と食い違っている箇所
見つけ方- エラーが出ないので、出力を目で確かめるしかありません。この種の誤りが最も発見が遅れます
このエラーが出る典型パターン
パターン1
1 nums = [1, 2, 2, 3] 2 for n in nums : ^ 3 if n == 2: 4 nums.remove(n) 5 print(nums)
リストを変更しながら同じリストをイテレートすると、要素がずれて一部が処理から漏れます。nums[:]でコピーを作ってからイテレートすれば元のリストを安全に変更できます。
直し方: nums を nums[:] にします。
パターン2
1 fruits = ["apple", "banana", "banana", "cherry"] 2 for f in fruits : ^ 3 if f == "banana": 4 fruits.remove(f) 5 print(fruits)
同じ値が連続している場合に特に発覚しやすいパターンです。1つ消えると、残りの1つが繰り上がって判定をすり抜けます。
直し方: fruits を fruits[:] にします。
パターン3
1 scores = [0, 0, 5, 10] 2 for s in scores : ^ 3 if s == 0: 4 scores.remove(s) 5 print(scores)
0点を除外するつもりが1つ残ってしまう典型例です。件数が多いデータほど発覚が遅れます。
直し方: scores を scores[:] にします。
パターン4
1 nums = [5, -1, -2, 3, 8] 2 for n in nums : ^ 3 if n < 0: 4 nums.remove(n) 5 print(nums)
負の数を取り除くような処理でも同じです。連続して条件に合う要素があると、削除のたびに後続がずれて判定が1つ飛ばしになります。
直し方: nums を nums[:] にします。
パターン5
1 words = ["aa", "b", "c", "dd"] 2 for w in words : ^ 3 if len(w) == 1: 4 words.remove(w) 5 print(words)
文字数で絞り込んで削除する処理でも同様です。連続して条件に合う要素があると一部が処理から漏れます。
直し方: words を words[:] にします。
よくある誤解
例外は一切出ません。リストにはサイズ変化を検出する仕組みが無いため、静かに間違った結果になります。
実務での勘所
要素が飛ばされる仕組みを分解すると、リストのイテレータは内部で「今どの位置を見ているか」という添字を1つずつ進めているだけです。ある要素をremoveすると、それより後ろの要素は1つずつ前に詰められますが、イテレータの添字はそれを知らずに単純に+1されるため、詰められてちょうどその位置に来た要素を読み飛ばしてしまいます。安全に書くには、for x in list(original):のように元のリストのコピーに対してループしながらoriginal.remove(x)を呼ぶか、新しいリストを作る内包表記に書き換えるのが定石です。