listを反復処理しながら、反復ごとに項目を削除するこのコードを考えてみましょう。
x = list(range(5))
for i in x:
print(i)
x.pop()
0, 1, 2
。リストの最後の2つの要素が最初の2つの反復によって削除されたため、最初の3つの要素のみが出力されます。
しかし、dictで同様のことを試した場合:
y = {i: i for i in range(5)}
for i in y:
print(i)
y.pop(i)
0
、次にRuntimeError: dictionary changed size during iteration
、繰り返し処理中に辞書からキーを削除するため。
もちろん、反復中にリストを変更することは悪いことです。しかし、なぜRuntimeError
が辞書のように発生しないのですか?この動作に正当な理由はありますか?
その理由は簡単だと思います。 list
sは順序付けされており、dict
s(Python 3.6/3.7より前)およびset
sは順序付けられていません。したがって、list
sを繰り返すときに、ベストプラクティスとは言えないかもしれませんが、一貫性があり、再現性があり、保証されていますの動作になります。
これを使用できます。たとえば、偶数の要素を持つlist
を半分に分割し、後半を逆にしたいとします。
>>> lst = [0,1,2,3]
>>> lst2 = [lst.pop() for _ in lst]
>>> lst, lst2
([0, 1], [3, 2])
もちろん、この操作を実行するためのはるかに優れた直感的な方法がありますが、要はそれが機能することです。
対照的に、dict
sとset
sの動作は、ハッシュに応じて反復順序が変わる可能性があるため、完全に実装固有です。
おそらくRunTimeError
動作との整合性のために、collections.OrderedDict
でdict
を取得します。 dict
動作の変更は、Python 3.6(ここでdict
sは挿入順序を維持することが保証されています)の後で発生する可能性が高いと思います。実際のユースケースとの互換性はありません。
この場合、collections.deque
は、順序付けされているにもかかわらず、RuntimeError
も発生させることに注意してください。
下位互換性を損なうことなく、このようなチェックをリストに追加することは不可能でした。辞書に関しては、そのような問題はありませんでした。
以前の事前イテレータの設計では、for
ループは、IndexErrorが発生するまで整数インデックスを増やしながらシーケンス要素検索フックを呼び出すことで機能していました。 (私は__getitem__
と言いますが、これは型/クラスの統合の前に戻ったので、C型には__getitem__
がありませんでした。)len
もこの設計に関与しておらず、変更をチェックする場所はありません。
イテレータが導入されたとき、dictイテレータのサイズ変更チェックは イテレータを言語に導入した最初のコミット でした。それ以前は、dictsはまったく反復可能ではなかったため、破壊する下位互換性はありませんでした。リストはまだ古い反復プロトコルを通過しました。
list.__iter__
が導入されたとき 、これは純粋に速度の最適化であり、動作の変更を意図したものではなく、変更チェックを追加すると、古い動作に依存する既存のコードとの下位互換性が失われます。
ディクショナリは、追加のレベルの間接指定で挿入順序を使用します。これにより、キーが削除されて再挿入されている間に反復するとしゃっくりが発生し、それによってディクショナリの順序と内部ポインタが変更されます。
そして、この問題はd
の代わりにd.keys()
を反復しても修正されません。なぜなら、Python 3の場合、d.keys()
は、同じ問題が発生するdict
のキー。代わりにlist(d)
を反復すると、反復中に変更されない辞書のキーからリストが生成されます