Pythonでlistからfor loopでremoveする際の注意
Pythonでlistに対してfor文を回して条件によって削除したい、と思った場合、
my_list = ['a', 'b', 'c', 'd', 'e']
for x in my_list:
print(x)
if x in ('c', 'd'):
my_list.remove(x)
print(my_list)
とすると予期せぬことが起こります。
結果は
a
b
c
e
['a', 'b', 'd', 'e']
となり、d
が抜けてません。途中でprint
してるところでも出てないので
d
がskipされています。
これはfor文が、my_list
の中身を順番に見ていく、のではなく、
中でカウンタを利用し、そのカウンタを1回ずつ上げながら
my_list
というリストからそのカウンタの数に対応する順番のものを抜き出す、という作業をしているからです。
つまり、d
は元々my_list[3]
の位置にあるわけですが、
c
(この時点でmy_list[2]
のもの)のときにループが終わる前に
my_list
からc
をremove
しているので、
この時点でd
は繰り上がってmy_list[2]
の位置になります。
一方で、カウンタはc
の時点で2
、ループ終了時に3
に移るので、
次に見るのはmy_list[3]
、でこの時点でmy_list[3]
はe
に変わっています。
これはremove
だけでなく、insert
とかで途中に追加した場合でも同じです。
いずれにしろfor文とかで回すとき、回しているリストをfor文のなかで操作するのは バグになるので辞めた方が良いです。
公式ドキュメントにも注記があって、この様な事をしたい場合には
my_list = ['a', 'b', 'c', 'd', 'e']
for x in my_list[:]:
print(x)
if x in ('c', 'd'):
my_list.remove(x)
print(my_list)
とすべき、とのこと。これだと
a
b
c
d
e
['a', 'b', 'e']
とちゃんと予期したとおりになります。
リストのコピーに関して
違いはfor文で回すリストをmy_list
からmy_list[:]
に変えてるだけですが、
これによって、新しいリストを作成して、その上でfor文を回しているので、
my_list
への操作はループに対しては影響せず、
きちんとd
も含めて全部に対して処理が出来ている、ということです。
ちょっと注意が必要なのは単に
my_list_copy = my_list
for x in my_list_copy:
...
みたいなことをしても最初と同じ問題が起こります。
基本的にPythonでは参照渡しが行われるので
=
で渡した場合には同じオブジェクトを見るようになるだけです。
なので上の場合だとmy_list
を変更するとmy_list_copy
も変更される状態になっています。
my_list[<start>:<end>]
はスライスと呼ばれるもので、
start
の位置からend
の前までを切り出す、というものです。
end
の方はその位置まで、ではなく、その前まで、なのでちょっと注意。
指定しないとそれぞれ最初と最後を指定したことになります。
>>> my_list = ['a', 'b', 'c', 'd', 'e']
>>> my_list[1:3]
['b', 'c']
>>> my_list[2:]
['c', 'd', 'e']
>>> my_list[:2]
['a', 'b']
>>> my_list[:]
['a', 'b', 'c', 'd', 'e']
で、[:]
だとそのまま同じものを返すわけですが、これは違うオブジェクトになっています。
>>> my_list = ['a', 'b', 'c', 'd', 'e']
>>> my_list_copy = my_list
>>> my_list_slice = my_list[:]
>>> id(my_list)
4434973232
>>> id(my_list_copy)
4434973232
>>> id(my_list_slice)
4434935472
>>> my_list == my_list_copy
True
>>> my_list is my_list_copy
True
>>> my_list == my_list_slice
True
>>> my_list is my_list_slice
False
な感じで単なる=
は同じidを持っていて、全く同じであることがわかります。
一方、スライスで渡したものは別のidを持っています。
中身は一緒なので、==
でチェックするとTrue
を返しますが、
is
でチェックすると違うオブジェクトなのでFalse
を返します。
関数の引数に関しても注意
この辺はfor文だけではなく、関数の引数として渡す場合にも 予期せぬことが起こり得るので注意が必要です。
単に削除したいだけのとき
上の作業ではfor文を回して削除しているだけですが、想定としては
これ以外にも色々とx
を使って作業することを想定しています。
もし、単に削除したいだけなら
my_list = [x for x in my_list if x not in ('c', 'd')]
の様な内包表記を使ったほうがちゃんと思ったとおりになり 見た目的にもすっきりするので 良いかと思います。
Homebrew-fileのバグ
今回これを書いたのは自分でやらかしてたからです。
Brewfile gets reordered making it difficult to see what changed · Issue #87 · rcmdnk/homebrew-file
すいません、バグですがbrew-fileを使っていた場合、
cask
のリストが一つ新しいものを追加しただけで大幅に変更されたりしていたと思います。
中身自体は全てBrewfileに書き出されるのでBrewfileそのものを見たりしない限り問題はないのですが、 Gitで管理したりしていると意味がわからない変更が何度も入ってたりするかもしれません。
これは上のfor文ループのバグで、本来書かれるべき場所とは別の場所に書き出されてしまっていたからです。
履歴を見てみると、すぐ上にbrew
のパッケージに関してはちゃんと[:]
を使うようになっていて、
この辺2016年に書いてました。
その際にcask
の方は見てなかったのでずーっとこの状態が続いていたようです。
自分でもBrewfileに関しては中身を直接弄ったり見たりすることはほとんどないので 気づいてませんでした。
まあ、brew-fileは思想的に、自分でBrewfileの中身を気にしないでも 勝手にアップデートされて勝手に管理される、ということを目指して作ったものなので。
とはいえ、バグはバグなので直しました。
実はもう一つ、引数の処理のところでも同じ様なことをしていて、 こちらは結構クリティカルで、複数の引数を渡した場合、正しく動作してなかった場合があったと思います。
それほど複数の引数を渡したり複雑なことをするものではないのですが、 確かになにか変だな、と思うところはあって、でも普段支障がないのでちゃんと見てませんでしたが、 違和感の原因はこれ。