読者です 読者をやめる 読者になる 読者になる

Pythonのfor文のリストの評価は一度だけ

python

pythonのfor文を使う時に

for i in xrange(len(list)):

って書き方をすると思うのですが、
len()が毎回実行されたりするのかなぁ?と思って気になりました。
何か関数とかをここで実行した時にパフォーマンスに関わるのかな?という疑問ですね。



確認するためにはfor文の中でlist要素を削除してみて、イテレーションがおかしくなるかどうかを見れば良さそう。
元の要素数分反復していればOKということで以下のように実行。

>>> list = ['a', 'b', 'c', 'd', 'e', 'f']
>>> for i in xrange(len(list)):   # 6回ループしてくれればOK。
...   print i
...   if i / 2:
...     del(list[0])
... 
0
1
2
3
4
5
>>> list
['e', 'f']

要素が削除され、それとは関係なく6回実行されたので問題ないのですね。



念のためリファレンス確認してみたら書いてありました。

式リストは一度だけ評価されます;

そうなんですね。そうなんですね。よかった。



と、思って最後まで読んだところ、

どの要素が次に使われるかを追跡するために、内部的なカウンタが使われており、このカウンタは反復処理を行うごとに加算されます。 このカウンタがシーケンスの長さに達すると、ループは終了します。このことは、スイート中でシーケンスから現在の (または以前の) 要素を 除去すると、(次の要素のインデクスは、すでに取り扱った要素のインデクスになるために) 次の要素が飛ばされることを意味します。

という警告がありました。



確認してみると、

>>> list = ['a', 'b', 'c', 'd', 'e', 'f']
>>> for x in list:
...   print x
...   if True:
...     del list[0]
... 
a
c
e
>>> list
['b', 'd', 'f']

おぉ。確かに。
b, d, fは要素が減ったタイミングでindexがズレてスキップされたのね。



さらに続き読んでみると、ちゃんと対策も書いてありました。

シーケンス全体に相当するスライスを使って一時的なコピーを作ると、これを避けることができます。

for x in a[:]:
    if x < 0: a.remove(x)

こういうところでリストのコピーを使ったりするのですね。