目的
- リストをfor文で回しながら特定条件で前記リストを変更する場合に、バグってしまったため原因を調べる。
- 上記対策のベストプラクティスを記述する。
結論
- 公式嫁!
注釈 ループ中でのシーケンスの変更には微妙な問題があります (これはミュータブルなシーケンスのみ、例えばリストで起こり得ます)。 どの要素が次に使われるかを追跡するために、内部的なカウンタが使われており、このカウンタは反復のたびに加算されます。 このカウンタがシーケンスの長さに達すると、ループは終了します。 このことから、スイートの中でシーケンスから現在の (または以前の) 要素を除去すると、(次の要素の位置が、既に処理済みの現在の要素のインデックスになるために) 次の要素が飛ばされることになります。 同様に、スイートの中でシーケンス中の現在の要素以前に要素を挿入すると、現在の要素がループの次の週で再度扱われることになります。 こうした仕様は、厄介なバグにつながります。 これは、シーケンス全体のスライスを使って一時的なコピーを作ることで避けられます。
for x in a[:]:
if x < 0: a.remove(x)
経緯
- AtCoderでARC006C-積み重ねを解いている際にfor文でバグってしまった。
- for文で与えられたリストを回しながら、リストを弄ろうとしてみたがうまくいかない。
- 対処を考えたもののスマートな方法が思いつかなかったため調べてみたら、公式にしっかりと書いてあった。
- 公式8.3 for文を参照した。
どう書くとバグる?どうすればよい?
これはバグる
for文で回しているfoo_list
の要素をシーケンス中に削除してしまっている。
これにより、リストが最後まで回されない場合がある。
for i in foo_list:
if thres >= i:
foo_list.remove(i)
バグを回避しようと思うとこう書きたくなる
まず、for文全部回す。消したい要素をメモする。メモした内容をforで回して削除する。
めんどくさい
memo = []
for i in range(len(foo_list)):
if thres >= foo_list[i]:
memo.append(foo_list[i])
for item in memo:
foo_list.remove(item)
公式を参照して書くとすっきり書ける
for文で指定するリストを全範囲スライスすることで、新規にリストオブジェクトを作る
copyしたいときにa = foo_list[:]
ってするのと同じですね
for i in foo_list[:]:
if thres >= i:
foo_list.remove(i)
ちなみに、上記の様に単にフィルタするたけなら、下記@shiracamusさんのご指摘のように内包表記のほうが良いです。
じゃあいつ使うねんって話は改めて加筆しようと思います。
@shiracamus
リストからremoveするよりも、内包表記で新しいリストを作った方が短時間で処理が終わりませんか?
foo_list = [i for i in foo_list if thres >= i]