Comment supprimer des éléments d'un dictionnaire tout en itérant dessus?

295

Est-il légitime de supprimer des éléments d'un dictionnaire en Python tout en itérant dessus?

Par exemple:

for k, v in mydict.iteritems():
   if k == val:
     del mydict[k]

L'idée est de supprimer les éléments qui ne remplissent pas une certaine condition du dictionnaire, au lieu de créer un nouveau dictionnaire qui est un sous-ensemble de celui qui est itéré.

Est-ce une bonne solution? Existe-t-il des moyens plus élégants / efficaces?

Trilarion
la source
1
Une question connexe avec des réponses très intéressantes: stackoverflow.com/questions/9023078/… .
max
On aurait pu essayer facilement. S'il échoue, ce n'est pas légitime.
Trilarion
26
@Trilarion On aurait pu essayer facilement ... et facilement rien appris de valeur. S'il réussit, ce n'est pas nécessairement légitime. Les cas marginaux et les avertissements inattendus abondent. Cette question est d'un intérêt non trivial pour tous les Pythonistas potentiels. Licenciement de la main sur l'ordre de "On aurait pu essayer facilement!" est inutile et contraire à l'esprit curieux de l'enquête stackoverflow.
Cecil Curry du
Après avoir lu la question connexe de max , je dois être d'accord. Vous voulez probablement juste parcourir cette question profondément troublante et ses réponses bien écrites à la place. Votre esprit pythonique sera époustouflé.
Cecil Curry
1
@CecilCurry Tester une idée par vous-même avant de la présenter ici est un peu dans l'esprit de stackoverflow si je ne me trompe pas. C'est tout ce que je voulais transmettre. Désolé s'il y a eu des perturbations à cause de cela. Je pense aussi que c'est une bonne question et je ne l'ai pas dévalorisée. J'aime beaucoup la réponse de Jochen Ritzel . Je ne pense pas qu'il soit nécessaire de faire tout cela pour supprimer à la volée lors de la suppression dans une deuxième étape est beaucoup plus simple. À mon avis, ce devrait être la voie préférée.
Trilarion

Réponses:

305

ÉDITER:

Cette réponse ne fonctionnera pas pour Python3 et donnera un RuntimeError.

RuntimeError: la taille du dictionnaire a changé pendant l'itération.

Cela se produit car mydict.keys()renvoie un itérateur et non une liste. Comme indiqué dans les commentaires, il suffit de convertir mydict.keys()en liste list(mydict.keys())et cela devrait fonctionner.


Un simple test dans la console montre que vous ne pouvez pas modifier un dictionnaire en itérant dessus:

>>> mydict = {'one': 1, 'two': 2, 'three': 3, 'four': 4}
>>> for k, v in mydict.iteritems():
...    if k == 'two':
...        del mydict[k]
...
------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython console>", line 1, in <module>
RuntimeError: dictionary changed size during iteration

Comme indiqué dans la réponse de delnan, la suppression d'entrées provoque des problèmes lorsque l'itérateur essaie de passer à l'entrée suivante. Utilisez plutôt la keys()méthode pour obtenir une liste des clés et travaillez avec:

>>> for k in mydict.keys():
...    if k == 'two':
...        del mydict[k]
...
>>> mydict
{'four': 4, 'three': 3, 'one': 1}

Si vous devez supprimer en fonction de la valeur des éléments, utilisez items()plutôt la méthode:

>>> for k, v in mydict.items():
...     if v == 3:
...         del mydict[k]
...
>>> mydict
{'four': 4, 'one': 1}
Blair
la source
53
Notez qu'en Python 3, dict.items () renvoie un itérateur (et dict.iteritems () a disparu).
Tim Lesher
83
Pour développer le commentaire @TimLesher ... Cela ne fonctionnera PAS en Python 3.
max
99
Pour élaborer sur l'élaboration de @ max, cela fonctionnera si vous convertissez le code ci-dessus avec 2to3. L'un des fixateurs par défaut fera ressembler la boucle à celle for k, v in list(mydict.items()):qui fonctionne bien en Python 3. Idem pour keys()devenir list(keys()).
Walter Mundt
8
Ça ne marche pas. Je reçois une erreur:RuntimeError: dictionary changed size during iteration
Tomáš Zato - Rétablir Monica
15
@ TomášZato comme Walter l'a souligné, pour python3 vous devez utiliser for k in list(mydict.keys()): car python3 fait de la méthode keys () un itérateur, et interdit également la suppression des éléments dict pendant l'itération. En ajoutant un appel list (), vous transformez l'itérateur keys () en liste. Ainsi, lorsque vous êtes dans le corps de la boucle for, vous n'itérez plus le dictionnaire lui-même.
Geoff Crompton
89

Vous pouvez également le faire en deux étapes:

remove = [k for k in mydict if k == val]
for k in remove: del mydict[k]

Mon approche préférée est généralement de simplement faire un nouveau dict:

# Python 2.7 and 3.x
mydict = { k:v for k,v in mydict.items() if k!=val }
# before Python 2.7
mydict = dict((k,v) for k,v in mydict.iteritems() if k!=val)
Jochen Ritzel
la source
11
@senderle: Depuis 2.7 en fait.
Jochen Ritzel
5
L'approche de compréhension de dict fait une copie du dictionnaire; heureusement, les valeurs au moins ne sont pas copiées en profondeur, juste liées. Pourtant, si vous avez beaucoup de clés, cela pourrait être mauvais. Pour cette raison, j'aime removedavantage l'approche en boucle.
max
1
Vous pouvez également combiner les étapes:for k in [k for k in mydict if k == val]: del mydict[k]
AXO
la première solution est la seule efficace sur les gros dict dans ce fil à ce jour - car elle ne fait pas de copie complète.
kxr
21

Vous ne pouvez pas modifier une collection pendant son itération. De cette façon réside la folie - plus particulièrement, si vous étiez autorisé à supprimer et à supprimer l'élément en cours, l'itérateur devrait passer à (+1) et le prochain appel à vous nextmènerait au-delà (+2), vous finissent par sauter un élément (celui juste derrière celui que vous avez supprimé). Vous avez deux options:

  • Copiez toutes les clés (ou les valeurs, ou les deux, selon vos besoins), puis parcourez-les. Vous pouvez utiliser .keys()et al pour cela (en Python 3, passez l'itérateur résultant à list). Cela pourrait être très gaspillant en termes d'espace.
  • Répétez mydictcomme d'habitude, en enregistrant les clés à supprimer dans une collection séparée to_delete. Une fois l'itération terminée mydict, supprimez tous les éléments to_deletede mydict. Économise de l'espace (en fonction du nombre de clés supprimées et du nombre restant) sur la première approche, mais nécessite également quelques lignes supplémentaires.

la source
You can't modify a collection while iterating it.c'est juste correct pour les dict et les amis, mais vous pouvez modifier les listes pendant l'itération:L = [1,2,None,4,5] <\n> for n,x in enumerate(L): <\n\t> if x is None: del L[n]
Nils Lindemann
3
@Nils Il ne lève pas d'exception mais il est toujours incorrect. Observez: codepad.org/Yz7rjDVT - voir par exemple stackoverflow.com/q/6260089/395760 pour une explication
Tu m'as ici. can'tEst toujours correct uniquement pour dict et amis, alors qu'il devrait l'être shouldn'tpour les listes.
Nils Lindemann
21

Répétez plutôt une copie, comme celle retournée par items():

for k, v in list(mydict.items()):
Ignacio Vazquez-Abrams
la source
1
Cela n'a pas beaucoup de sens - alors vous ne pouvez pas del vdirectement, vous avez donc fait une copie de chaque v que vous n'allez jamais utiliser et vous devez de toute façon accéder aux éléments par clé. dict.keys()est un meilleur choix.
jscs
2
@Josh: Tout dépend de combien vous allez avoir besoin d'utiliser vcomme critère de suppression.
Ignacio Vazquez-Abrams
3
Sous Python 3, dict.items()renvoie un itérateur plutôt qu'une copie. Voir le commentaire pour la réponse de Blair , qui (malheureusement) suppose également la sémantique de Python 2.
Cecil Curry
11

C'est le plus propre à utiliser list(mydict):

>>> mydict = {'one': 1, 'two': 2, 'three': 3, 'four': 4}
>>> for k in list(mydict):
...     if k == 'three':
...         del mydict[k]
... 
>>> mydict
{'four': 4, 'two': 2, 'one': 1}

Cela correspond à une structure parallèle pour les listes:

>>> mylist = ['one', 'two', 'three', 'four']
>>> for k in list(mylist):                            # or mylist[:]
...     if k == 'three':
...         mylist.remove(k)
... 
>>> mylist
['one', 'two', 'four']

Les deux fonctionnent en python2 et python3.

rsanden
la source
Ce n'est pas bon si votre jeu de données est volumineux. C'est copier tous les objets en mémoire, non?
AFP_555
1
@ AFP_555 Oui - mon objectif ici est de créer du code pythonique propre et parallèle. Si vous avez besoin d'efficacité de la mémoire, la meilleure approche que je connaisse est d'itérer et de créer une liste de clés à supprimer ou une nouvelle dictée d'éléments à enregistrer. La beauté est ma priorité avec Python; pour les grands ensembles de données que j'utilise Go ou Rust.
rsanden
9

Vous pouvez utiliser une compréhension de dictionnaire.

d = {k:d[k] for k in d if d[k] != val}

Aaron
la source
C'est le plus pythonique.
Yehosef
Mais il crée un nouveau dictionnaire au lieu de le modifier dsur place.
Aristide
9

Avec python3, itérer sur dic.keys () augmentera l'erreur de taille du dictionnaire. Vous pouvez utiliser cette méthode alternative:

Testé avec python3, cela fonctionne bien et l'erreur "La taille du dictionnaire a changé pendant l'itération " n'est pas déclenchée:

my_dic = { 1:10, 2:20, 3:30 }
# Is important here to cast because ".keys()" method returns a dict_keys object.
key_list = list( my_dic.keys() )

# Iterate on the list:
for k in key_list:
    print(key_list)
    print(my_dic)
    del( my_dic[k] )


print( my_dic )
# {}
glihm
la source
4

Vous pouvez d'abord créer une liste de clés à supprimer, puis parcourir cette liste en les supprimant.

dict = {'one' : 1, 'two' : 2, 'three' : 3, 'four' : 4}
delete = []
for k,v in dict.items():
    if v%2 == 1:
        delete.append(k)
for i in delete:
    del dict[i]
Pob
la source
C'est plutôt un doublon de la 1ère solution de @ Ritzel (efficace sur les gros dits sans copie complète). Bien qu'une "longue lecture" sans compréhension de liste. Mais est-ce peut-être quand même plus rapide?
kxr
3

Il existe un moyen qui peut convenir si les éléments que vous souhaitez supprimer sont toujours au "début" de l'itération du dict

while mydict:
    key, value = next(iter(mydict.items()))
    if should_delete(key, value):
       del mydict[key]
    else:
       break

Le «début» n'est garanti que pour être cohérent pour certaines versions / implémentations Python. Par exemple de Quoi de neuf dans Python 3.7

la nature de préservation de l'ordre d'insertion des objets dict a été déclarée comme faisant officiellement partie de la spécification du langage Python.

De cette façon, vous évitez une copie du dict que la plupart des autres réponses suggèrent, au moins en Python 3.

Michal Charemza
la source
1

J'ai essayé les solutions ci-dessus en Python3 mais celle-ci semble être la seule qui fonctionne pour moi lors du stockage d'objets dans un dict. Fondamentalement, vous faites une copie de votre dict () et vous répétez cela en supprimant les entrées de votre dictionnaire d'origine.

        tmpDict = realDict.copy()
        for key, value in tmpDict.items():
            if value:
                del(realDict[key])
JasonLandbridge
la source