rcmdnk's blog

正確なフォルムコントロールのためのスライス徹底マスター (HAIR MODE URESTA!人気スタイリストへの近道シ)

Pythonでリストに対してfor文を回す場合、 そのfor文の中でリストを操作してしまうとバグを生む可能性が高い、という話。

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からcremoveしているので、 この時点でdは繰り上がってmy_list[2]の位置になります。

一方で、カウンタはcの時点で2、ループ終了時に3に移るので、 次に見るのはmy_list[3]、でこの時点でmy_list[3]eに変わっています。

これはremoveだけでなく、insertとかで途中に追加した場合でも同じです。

いずれにしろfor文とかで回すとき、回しているリストをfor文のなかで操作するのは バグになるので辞めた方が良いです。

8. 複合文 (compound statement) — Python 3.8.1 ドキュメント

公式ドキュメントにも注記があって、この様な事をしたい場合には

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の中身を気にしないでも 勝手にアップデートされて勝手に管理される、ということを目指して作ったものなので。

とはいえ、バグはバグなので直しました。

実はもう一つ、引数の処理のところでも同じ様なことをしていて、 こちらは結構クリティカルで、複数の引数を渡した場合、正しく動作してなかった場合があったと思います。

それほど複数の引数を渡したり複雑なことをするものではないのですが、 確かになにか変だな、と思うところはあって、でも普段支障がないのでちゃんと見てませんでしたが、 違和感の原因はこれ。

Sponsored Links
Sponsored Links

« GoogleスライドはPowerPointの代わりになるか LinuxにHomebrewでShellCheckをインストールしたい(が、諦める) »