Modifier un dict Python tout en l'itérant

87

Disons que nous avons un dictionnaire Python d, et nous l'itérons comme suit:

for k,v in d.iteritems():
    del d[f(k)] # remove some item
    d[g(k)] = v # add a new item

( fet ne gsont que quelques transformations de boîte noire.)

En d'autres termes, nous essayons d'ajouter / supprimer des éléments dtout en l'itérant en utilisant iteritems.

Est-ce bien défini? Pourriez-vous fournir quelques références pour étayer votre réponse?

(Il est assez évident de résoudre ce problème s'il est cassé, donc ce n'est pas l'angle que je recherche.)

NPE
la source
J'ai essayé de le faire et il semble que si vous laissez la taille initiale du dict inchangée - par exemple, remplacez toute clé / valeur au lieu de les supprimer, ce code ne
lèvera
Je ne suis pas d'accord sur le fait qu'il est "assez évident comment résoudre ce problème s'il est cassé" pour tous ceux qui recherchent ce sujet (y compris moi-même), et j'aurais aimé que la réponse acceptée ait au moins abordé ce sujet.
Alex Peters

Réponses:

53

Il est explicitement mentionné sur la page de documentation Python (pour Python 2.7 ) que

L'utilisation iteritems()lors de l'ajout ou de la suppression d'entrées dans le dictionnaire peut déclencher une RuntimeErroritération ou échouer sur toutes les entrées.

De même pour Python 3 .

De même pour iter(d), d.iterkeys()et d.itervalues(), et je vais aller aussi loin que de dire que le fait pour for k, v in d.items():(je ne me souviens pas exactement ce que forfait, mais je ne serais pas surpris si la mise en œuvre appelée iter(d)).

Raphaël Saint-Pierre
la source
48
Je vais me mettre dans l'embarras pour le bien de la communauté en déclarant que j'ai utilisé l'extrait de code même. Pensant que puisque je n'ai pas eu de RuntimeError, je pensais que tout allait bien. Et ça l'était, pendant un moment. Les tests unitaires anaux rétentifs me donnaient le pouce en l'air et cela fonctionnait même bien quand il a été publié. Ensuite, j'ai commencé à avoir un comportement bizarre. Ce qui se passait, c'est que les éléments du dictionnaire étaient ignorés et que tous les éléments du dictionnaire n'étaient donc pas analysés. Enfants, apprenez des erreurs que j'ai commises dans ma vie et dites simplement non! ;)
Alan Cabrera
3
Puis-je rencontrer des problèmes si je modifie la valeur de la clé actuelle (mais que je n'ajoute ou ne supprime aucune clé?) J'imagine que cela ne devrait pas causer de problèmes, mais j'aimerais savoir!
Gershom du
@GershomMaes Je n'en connais aucun, mais il se peut que vous soyez toujours en train de rencontrer un champ de mines si votre corps de boucle utilise la valeur et ne s'attend pas à ce qu'elle change.
Raphaël Saint-Pierre
3
d.items()devrait être en sécurité dans Python 2.7 (le jeu change avec Python 3), car il crée ce qui est essentiellement une copie de d, donc vous ne modifiez pas ce que vous itérez.
Paul Price
Il serait intéressant de savoir si cela est également vrai pourviewitems()
jlh
50

Alex Martelli y réfléchit ici .

Il peut ne pas être prudent de changer le conteneur (par exemple, dict) en boucle sur le conteneur. Alors del d[f(k)]peut-être pas sûr. Comme vous le savez, la solution de contournement consiste à utiliser d.items()(pour boucler sur une copie indépendante du conteneur) au lieu de d.iteritems()(qui utilise le même conteneur sous-jacent).

Il est normal de modifier la valeur à un index existant du dict, mais l'insertion de valeurs à de nouveaux index (par exemple d[g(k)]=v) peut ne pas fonctionner.

unutbu
la source
3
Je pense que c'est une réponse clé pour moi. Dans de nombreux cas d'utilisation, un processus insère des choses et un autre nettoie / supprime les choses, donc le conseil d'utiliser d.items () fonctionne. Python 3 mises en garde non
respectées
4
Plus d'informations sur les mises en garde de Python 3 peuvent être trouvées dans PEP 469 où les équivalents sémantiques des méthodes Python 2 dict susmentionnées sont énumérés.
Lionel Brooks
1
"Il est normal de modifier la valeur à un index existant du dict" - avez-vous une référence pour cela?
Jonathon Reinhart
1
@JonathonReinhart: Non, je n'ai pas de référence pour cela, mais je pense que c'est assez standard en Python. Par exemple, Alex Martelli était un développeur principal de Python et démontre son utilisation ici .
unutbu
27

Vous ne pouvez pas faire cela, du moins avec d.iteritems(). Je l'ai essayé, et Python échoue avec

RuntimeError: dictionary changed size during iteration

Si vous utilisez à la place d.items(), cela fonctionne.

En Python 3, d.items()est une vue dans le dictionnaire, comme d.iteritems()en Python 2. Pour faire cela en Python 3, utilisez plutôt d.copy().items(). Cela nous permettra également de parcourir une copie du dictionnaire afin d'éviter de modifier la structure de données sur laquelle nous itérons.

murgatroid99
la source
2
J'ai ajouté Python 3 à ma réponse.
murgatroid99
2
Pour info, la traduction littérale (comme par exemple utilisée par 2to3) de Py2 d.items()en Py3 est list(d.items()), bien qu'elle d.copy().items()soit probablement d'une efficacité comparable.
Søren Løvborg
2
Si l'objet dict est très gros, d.copy (). Items () est-il efficiet?
libellule
11

J'ai un grand dictionnaire contenant des tableaux Numpy, donc la chose dict.copy (). Keys () suggérée par @ murgatroid99 n'était pas faisable (même si cela fonctionnait). Au lieu de cela, je viens de convertir le keys_view en une liste et cela a bien fonctionné (en Python 3.4):

for item in list(dict_d.keys()):
    temp = dict_d.pop(item)
    dict_d['some_key'] = 1  # Some value

Je me rends compte que cela ne plonge pas dans le domaine philosophique du fonctionnement interne de Python comme les réponses ci-dessus, mais cela fournit une solution pratique au problème déclaré.

2cynykyle
la source
6

Le code suivant montre que cela n'est pas bien défini:

def f(x):
    return x

def g(x):
    return x+1

def h(x):
    return x+10

try:
    d = {1:"a", 2:"b", 3:"c"}
    for k, v in d.iteritems():
        del d[f(k)]
        d[g(k)] = v+"x"
    print d
except Exception as e:
    print "Exception:", e

try:
    d = {1:"a", 2:"b", 3:"c"}
    for k, v in d.iteritems():
        del d[f(k)]
        d[h(k)] = v+"x"
    print d
except Exception as e:
    print "Exception:", e

Le premier exemple appelle g (k) et lève une exception (le dictionnaire a changé de taille pendant l'itération).

Le deuxième exemple appelle h (k) et ne lève aucune exception, mais renvoie:

{21: 'axx', 22: 'bxx', 23: 'cxx'}

Ce qui, en regardant le code, semble faux - je me serais attendu à quelque chose comme:

{11: 'ax', 12: 'bx', 13: 'cx'}
combatdave
la source
Je peux comprendre pourquoi vous pourriez vous attendre, {11: 'ax', 12: 'bx', 13: 'cx'}mais les 21,22,23 devraient vous donner un indice sur ce qui s'est réellement passé: votre boucle a traversé les éléments 1, 2, 3, 11, 12, 13 mais n'a pas réussi à récupérer le second ronde de nouveaux éléments au fur et à mesure qu'ils ont été insérés devant les éléments que vous aviez déjà itérés. Changez h()pour revenir x+5et vous obtenez un autre x: 'axxx'etc. ou 'x + 3' et vous obtenez le magnifique'axxxxx'
Duncan
Ouais, mon erreur, j'ai peur - ma sortie attendue était {11: 'ax', 12: 'bx', 13: 'cx'}comme vous l'avez dit, donc je vais mettre à jour mon article à ce sujet. Quoi qu'il en soit, ce comportement n'est clairement pas bien défini.
combatdave
1

J'ai eu le même problème et j'ai utilisé la procédure suivante pour résoudre ce problème.

La liste Python peut être itérée même si vous modifiez lors de l'itération dessus. donc pour le code suivant, il imprimera les 1 à l'infini.

for i in list:
   list.append(1)
   print 1

Donc, en utilisant list et dict en collaboration, vous pouvez résoudre ce problème.

d_list=[]
 d_dict = {} 
 for k in d_list:
    if d_dict[k] is not -1:
       d_dict[f(k)] = -1 # rather than deleting it mark it with -1 or other value to specify that it will be not considered further(deleted)
       d_dict[g(k)] = v # add a new item 
       d_list.append(g(k))
Zeel Shah
la source
Je ne sais pas s'il est prudent de modifier une liste pendant l'itération (bien que cela puisse fonctionner dans certains cas). Voir cette question par exemple ...
Roman
@Roman Si vous voulez supprimer des éléments d'une liste, vous pouvez l'itérer en toute sécurité dans l'ordre inverse, car dans l'ordre normal, l'index de l'élément suivant changerait lors de la suppression. Voyez cet exemple.
mbomb007
1

Python 3, vous devriez simplement:

prefix = 'item_'
t = {'f1': 'ffw', 'f2': 'fca'}
t2 = dict() 
for k,v in t.items():
    t2[k] = prefix + v

Ou utiliser:

t2 = t1.copy()

Vous ne devez jamais modifier le dictionnaire original, cela entraîne de la confusion ainsi que des bogues potentiels ou des RunTimeErrors. Sauf si vous ajoutez simplement au dictionnaire de nouveaux noms de clé.

Dexter
la source
0

Aujourd'hui, j'avais un cas d'utilisation similaire, mais au lieu de simplement matérialiser les clés du dictionnaire au début de la boucle, je voulais que les modifications du dict affectent l'itération du dict, qui était un dict ordonné.

J'ai fini par créer la routine suivante, qui peut également être trouvée dans jaraco.itertools :

def _mutable_iter(dict):
    """
    Iterate over items in the dict, yielding the first one, but allowing
    it to be mutated during the process.
    >>> d = dict(a=1)
    >>> it = _mutable_iter(d)
    >>> next(it)
    ('a', 1)
    >>> d
    {}
    >>> d.update(b=2)
    >>> list(it)
    [('b', 2)]
    """
    while dict:
        prev_key = next(iter(dict))
        yield prev_key, dict.pop(prev_key)

La docstring illustre l'utilisation. Cette fonction pourrait être utilisée à la place de d.iteritems()ci - dessus pour avoir l'effet souhaité.

Jason R. Coombs
la source