web-dev-qa-db-ja.com

反復中にリストとディクショナリを変更しますが、dictで失敗するのはなぜですか?

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が辞書のように発生しないのですか?この動作に正当な理由はありますか?

30
ducminh

その理由は簡単だと思います。 listsは順序付けされており、dicts(Python 3.6/3.7より前)およびsetsは順序付けられていません。したがって、listsを繰り返すときに、ベストプラクティスとは言えないかもしれませんが、一貫性があり、再現性があり、保証されていますの動作になります。

これを使用できます。たとえば、偶数の要素を持つlistを半分に分割し、後半を逆にしたいとします。

>>> lst = [0,1,2,3]
>>> lst2 = [lst.pop() for _ in lst]
>>> lst, lst2
([0, 1], [3, 2])

もちろん、この操作を実行するためのはるかに優れた直感的な方法がありますが、要はそれが機能することです。

対照的に、dictsとsetsの動作は、ハッシュに応じて反復順序が変わる可能性があるため、完全に実装固有です。

おそらくRunTimeError動作との整合性のために、collections.OrderedDictdictを取得します。 dict動作の変更は、Python 3.6(ここでdictsは挿入順序を維持することが保証されています)の後で発生する可能性が高いと思います。実際のユースケースとの互換性はありません。

この場合、collections.dequeは、順序付けされているにもかかわらず、RuntimeErrorも発生させることに注意してください。

29
Chris_Rands

下位互換性を損なうことなく、このようなチェックをリストに追加することは不可能でした。辞書に関しては、そのような問題はありませんでした。

以前の事前イテレータの設計では、forループは、IndexErrorが発生するまで整数インデックスを増やしながらシーケンス要素検索フックを呼び出すことで機能していました。 (私は__getitem__と言いますが、これは型/クラスの統合の前に戻ったので、C型には__getitem__がありませんでした。)lenもこの設計に関与しておらず、変更をチェックする場所はありません。

イテレータが導入されたとき、dictイテレータのサイズ変更チェックは イテレータを言語に導入した最初のコミット でした。それ以前は、dictsはまったく反復可能ではなかったため、破壊する下位互換性はありませんでした。リストはまだ古い反復プロトコルを通過しました。

list.__iter__が導入されたとき 、これは純粋に速度の最適化であり、動作の変更を意図したものではなく、変更チェックを追加すると、古い動作に依存する既存のコードとの下位互換性が失われます。

7
user2357112

ディクショナリは、追加のレベルの間接指定で挿入順序を使用します。これにより、キーが削除されて再挿入されている間に反復するとしゃっくりが発生し、それによってディクショナリの順序と内部ポインタが変更されます。

そして、この問題はdの代わりにd.keys()を反復しても修正されません。なぜなら、Python 3の場合、d.keys()は、同じ問題が発生するdictのキー。代わりにlist(d)を反復すると、反復中に変更されない辞書のキーからリストが生成されます

2
AB Abhi