for in 文の処理について

思い込みによる間違いの可能性があったのでループを抜ける時の判定について追記しました。(1/21)

引き続きPythonの勉強をしているのですが、今日Listについて調べているとPython Lists  |  Python Education  |  Google Developers内に次のような記述がありました。
(for in 文についての説明のあと)"Do not add or remove from the list during iteration."
つまりループを回している最中にListに項目をくわえたり削除したりしてはいけません、ということですね。

いけません・・・と言われるとやりたくなりますね。
ましてやあのグーグル先生が言うのだからこれはやってみろという意味だとしか思えません。

というわけで以下のコードを試してみました

# coding: shift_jis
def loopTest():
  testList = ["ゼロ","イチ","ニ","サン","シ","ゴ","ロク","シチ"] #まずリストを作成して
  
  for key in testList:       #ループしながら
    print key                #項目を表示したあと
    testList.remove(key)     #その項目を削除

if __name__=="__main__":
  loopTest()

まぁエラーがでて動かないかな、と思ったら普通に動きます。
何の警告もでません。
実行結果は

ゼロ
ニ
シ
ロク

アレ!?
"イチ"、"サン"、"ゴ"、"シチ"はどこいったんだ!?
コードは表示した項目を削除するように書いたはず・・・・

もう一度確認のためコードを次のように変更してみます。

# coding: shift_jis
def loopTest():
  testList = ["ゼロ","イチ","ニ","サン","シ","ゴ","ロク","シチ"] #まずリストを作成して
  
  for key in testList:       #ループしながら
    print key                #項目を表示したあと
    testList.remove(key)     #その項目を削除
    
  for key2 in testList:      #もういちどループして
    print key2               #残っている項目を表示

if __name__=="__main__":
  loopTest()

実行結果はこちら

ゼロ
ニ
シ
ロク
イチ
サン
ゴ
シチ

やはり削除されているのは表示した項目でまちがいないようです。

このことから推測されるのはPythonのfor in 文は内部では「何番目を処理中か」で管理されているということでしょうか。
上の例ですと、まずfor in文に入ったら最初の"ゼロ"をkeyに代入
このときこれとは別に今リストの何番目の項目を処理しているかというフラグがたつ、この場合1番目(プログラム的に言えば0番目)
ループ内で"ゼロ"が削除されるためリストは1つずつ前にずれる。
ループが最初に戻りフラグをみて2番目の項目の処理を始める。
2番目の項目は"ニ"に変わっているので"ニ"が表示される、と。
その後これを順番に繰り返していき、n番目の処理を始めようとした際に
n番目が存在しなければループをぬける。
(1/21追記:Pythonのリストでは空の要素が認められていないため、n番目が存在しない=リストの終端という判断でループを抜け出ていると思い上のように書いていましたが、リストの長さとの比較で抜け出ているのかもしれません。)

さて、ここまで来てPythonのドキュメントを読んでみます。
日本語http://www.python.jp/doc/release/tutorial/controlflow.html#for
原文4. More Control Flow Tools — Python 3.7.2 documentation

ふむふむ、つまりやってはいけないわけではなくてnot safeなだけか。
この場合のnot safeは元のリストを破壊するので、という意味だと思いますが上の想像通りの処理だとするとループ内で項目を追加すると無限ループに陥る危険もありますね。コピーに対して処理を行う場合でもそこは注意した方がよさそうです。

一応確認のため次のコードを実行してみます。

# coding: shift_jis
def loopTest():
  testList = ["ゼロ","イチ","ニ","サン","シ","ゴ","ロク","シチ"] #まずリストを作成して
  
  for key in testList:       #ループしながら
    print key                #項目を表示したあと
    testList.insert(0,"マイナス")     #リストの0番目に"マイナス"を挿入
    
if __name__=="__main__":
  loopTest()

結果はまぁ予想通り(^O^)/

ゼロ
ゼロ
ゼロ
ゼロ
ゼロ
ゼロ
ゼロ
ゼロ
ゼロ
ゼロ
ゼロ
ゼロ
(以下略)

無限ループには気をつけましょう。