Jak usunąć elementy ze słownika podczas iteracji?

295

Czy legalne jest usuwanie elementów ze słownika w Pythonie podczas iteracji?

Na przykład:

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

Chodzi o to, aby usunąć ze słownika elementy niespełniające określonych warunków, zamiast tworzyć nowy słownik, który jest podzbiorem iterowanego.

Czy to dobre rozwiązanie? Czy istnieją bardziej eleganckie / wydajne sposoby?

Trilarion
źródło
1
Powiązane pytanie z bardzo ciekawych odpowiedzi: stackoverflow.com/questions/9023078/... .
maks.
Można było łatwo spróbować. Jeśli zawiedzie, nie jest to uzasadnione.
Trilarion
26
@Trilarion Można było łatwo spróbować ... i łatwo nauczyć się niczego wartościowego. Jeśli to się uda, to nie koniecznie uzasadnione. Mnóstwo skrzynek i nieoczekiwane zastrzeżenia. To pytanie jest nietrywialne dla wszystkich niedoszłych Pythonistów. Zwolnienie z machania ręką na polecenie „Można było łatwo spróbować!” jest nieprzydatny i sprzeczny z dociekliwym duchem zapytania o przepełnienie stosu.
Cecil Curry
Po zapoznaniu się z pismem max „s pokrewne pytanie , muszę zgodzić. Prawdopodobnie chcesz po prostu przejrzeć to niepokojąco szczegółowe pytanie i jego dobrze napisane odpowiedzi. Twój pytonowy umysł zostanie wysadzony w powietrze.
Cecil Curry
1
@CecilCurry Testowanie pomysłu przed zaprezentowaniem go tutaj jest rodzajem ducha przepełnienia stosu, jeśli się nie mylę. To wszystko, co chciałem przekazać. Przepraszamy, jeśli z tego powodu wystąpiły jakieś zakłócenia. Myślę też, że to dobre pytanie i nie głosowałem za nim. Najbardziej podoba mi się odpowiedź Jochena Ritzela . Nie sądzę, że trzeba robić wszystkie te rzeczy, aby usunąć je w locie, gdy usuwanie w drugim kroku jest o wiele prostsze. Moim zdaniem powinien to być preferowany sposób.
Trilarion

Odpowiedzi:

305

EDYTOWAĆ:

Ta odpowiedź nie będzie działać dla Python3 i da RuntimeError.

RuntimeError: słownik zmienił rozmiar podczas iteracji.

Dzieje się tak, ponieważ mydict.keys()zwraca iterator, a nie listę. Jak wskazano w komentarzach, wystarczy przekonwertować mydict.keys()na listę list(mydict.keys())i powinna działać.


Prosty test w konsoli pokazuje, że nie można modyfikować słownika podczas iteracji:

>>> 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

Jak stwierdzono w odpowiedzi delnana, usuwanie wpisów powoduje problemy, gdy iterator próbuje przejść do następnego wpisu. Zamiast tego użyj keys()metody, aby uzyskać listę kluczy i pracować z tym:

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

Jeśli chcesz usunąć na podstawie wartości pozycji, użyj items()metody:

>>> for k, v in mydict.items():
...     if v == 3:
...         del mydict[k]
...
>>> mydict
{'four': 4, 'one': 1}
Blair
źródło
53
Zauważ, że w Pythonie 3 funkcja dict.items () zwraca iterator (a dict.iteritems () zniknął).
Tim Lesher
83
Aby rozwinąć komentarz na @TimLesher ... NIE zadziała to w Pythonie 3.
maks.
99
Aby rozwinąć opracowanie @ max, będzie działać, jeśli przekonwertujesz powyższy kod za pomocą 2to3. Jeden z domyślnych programów naprawiających spowoduje, że pętla będzie wyglądać tak, for k, v in list(mydict.items()):która działa dobrze w Pythonie 3. To samo dla keys()stawania się list(keys()).
Walter Mundt,
8
To nie działa Pojawia się błąd:RuntimeError: dictionary changed size during iteration
Tomáš Zato - Przywróć Monikę
14
@ TomášZato, jak zauważył Walter, dla python3 musisz użyć, for k in list(mydict.keys()): ponieważ python3 sprawia, że ​​metoda keys () jest iteratorem, a także zabrania usuwania elementów dict podczas iteracji. Dodając wywołanie list (), zmieniasz iterator keys () w listę. Więc kiedy jesteś w ciele pętli for, nie iterujesz już samego słownika.
Geoff Crompton
89

Możesz to zrobić w dwóch krokach:

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

Moim ulubionym podejściem jest zwykle po prostu nowy dykt:

# 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
źródło
11
@senderle: Właściwie od wersji 2.7.
Jochen Ritzel
5
Podejście polegające na zrozumieniu słownika stanowi kopię słownika; na szczęście wartości przynajmniej nie są głęboko kopiowane, tylko łączone. Mimo to, jeśli masz dużo kluczy, może być źle. Z tego powodu bardziej podoba mi się removepodejście do pętli.
maks.
1
Możesz także łączyć kroki:for k in [k for k in mydict if k == val]: del mydict[k]
AXO
pierwsze rozwiązanie jest jak dotąd jedynym skutecznym rozwiązaniem dla dużych nagrań w tym wątku - ponieważ nie tworzy pełnej długości kopii.
kxr 28.04.17
21

Nie można modyfikować kolekcji podczas iteracji. W ten sposób leży szaleństwo - przede wszystkim, jeśli pozwolono by ci usunąć i usunąć bieżący element, iterator musiałby przejść dalej (+1), a następne wezwanie nextzabrałoby cię poza to (+2), więc w końcu pomijam jeden element (ten zaraz za tym, który usunąłeś). Masz dwie opcje:

  • Skopiuj wszystkie klucze (lub wartości lub oba, w zależności od potrzeb), a następnie powtórz je. Możesz do tego użyć .keys()et al (w Pythonie 3 przekaż wynikowy iterator list). Może to być bardzo marnotrawne pod względem przestrzennym.
  • Powtarzaj mydictjak zwykle, zapisując klucze do usunięcia w osobnej kolekcji to_delete. Po zakończeniu iteracji mydictusuń wszystkie elementy to_deletez mydict. Oszczędza trochę (w zależności od tego, ile kluczy zostało usuniętych i ile pozostało) miejsca w pierwszym podejściu, ale wymaga również kilku dodatkowych wierszy.

źródło
You can't modify a collection while iterating it.jest to poprawne w przypadku nagrań i znajomych, ale podczas iteracji możesz modyfikować listy: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 Nie zgłasza wyjątku, ale nadal jest niepoprawny. Przestrzegać: codepad.org/Yz7rjDVT - patrz np stackoverflow.com/q/6260089/395760 o wyjaśnienie
Mam cię tutaj. Nadal can'tjest poprawny tylko dla nagrań i przyjaciół, podczas gdy powinien być shouldn'tdla list.
Nils Lindemann,
20

Zamiast tego powtarzaj kopię, taką jak ta zwrócona przez items():

for k, v in list(mydict.items()):
Ignacio Vazquez-Abrams
źródło
1
To nie ma większego sensu - wtedy nie możesz del vbezpośrednio, więc zrobiłeś kopię każdego v, którego nigdy nie będziesz używać i i tak musisz uzyskać dostęp do elementów według klucza. dict.keys()jest lepszym wyborem.
jscs
2
@Josh: Wszystko zależy od tego, ile musisz użyć vjako kryterium do usunięcia.
Ignacio Vazquez-Abrams
3
W Pythonie 3 dict.items()zwraca iterator zamiast kopii. Zobacz komentarze do Blair „s odpowiedzi , który (niestety) przyjmuje również Python 2 semantykę.
Cecil Curry
10

Najczystszy w użyciu 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}

Odpowiada to równoległej strukturze list:

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

Oba działają w python2 i python3.

rsanden
źródło
Nie jest to dobre, jeśli twój zestaw danych jest duży. To jest kopiowanie wszystkich obiektów w pamięci, prawda?
AFP_555
1
@ AFP_555 Tak - moim celem jest czysty, równoległy kod pythonowy. Jeśli potrzebujesz wydajności pamięci, najlepszym znanym mi podejściem jest iteracja i zbudowanie listy kluczy do usunięcia lub nowego nagrania elementów do zapisania. Piękno jest moim priorytetem w Pythonie; dla dużych zestawów danych używam Go lub Rust.
rsanden
9

Możesz użyć słownika.

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

Aaron
źródło
To jest najbardziej Pythonic.
Yehosef
Ale tworzy nowy słownik zamiast modyfikować dw miejscu.
Aristide
9

W przypadku python3, iteracja na dic.keys () podniesie błąd rozmiaru słownika. Możesz użyć tego alternatywnego sposobu:

Testowany z python3, działa dobrze i nie pojawia się komunikat o błędzie „ słownik zmienił rozmiar podczas iteracji ”:

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 )
# {}
blask
źródło
4

Możesz najpierw zbudować listę kluczy do usunięcia, a następnie iterować tę listę, usuwając je.

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
źródło
Jest to raczej kopia pierwszego rozwiązania @ Ritzel (efektywne w przypadku dużych nagrań bez pełnej kopii). Chociaż „długa lektura” bez zrozumienia listy. Czy jednak jest to prawdopodobnie szybsze?
kxr 28.04.17
3

Istnieje sposób, który może być odpowiedni, jeśli elementy, które chcesz usunąć, zawsze znajdują się na „początku” iteracji nagrania

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

„Początek” jest gwarantowany tylko w przypadku niektórych wersji / implementacji języka Python. Na przykład z Co nowego w Pythonie 3.7

natura zachowywania kolejności wstawiania obiektów dict została uznana za oficjalną część specyfikacji języka Python.

W ten sposób unika się kopii dyktu, którą sugeruje wiele innych odpowiedzi, przynajmniej w Pythonie 3.

Michał Charemza
źródło
1

Wypróbowałem powyższe rozwiązania w Python3, ale wydaje się, że to jedyne, które działa dla mnie podczas przechowywania obiektów w nagraniu. Zasadniczo tworzysz kopię swojego dict () i iterujesz ją, usuwając wpisy z oryginalnego słownika.

        tmpDict = realDict.copy()
        for key, value in tmpDict.items():
            if value:
                del(realDict[key])
JasonLandbridge
źródło