Modyfikowanie dyktu w Pythonie podczas iteracji po nim


87

Powiedzmy, że mamy słownik Pythona di iterujemy go w ten sposób:

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

( fi gto tylko niektóre transformacje czarnej skrzynki).

Innymi słowy, próbujemy dodawać / usuwać elementy do dpodczas iteracji po nim za pomocą iteritems.

Czy to dobrze zdefiniowane? Czy mógłbyś podać jakieś odniesienia na poparcie swojej odpowiedzi?

(Jest całkiem oczywiste, jak to naprawić, jeśli jest zepsute, więc nie o to mi chodzi.)




Próbowałem to zrobić i wygląda na to, że jeśli pozostawisz początkowy rozmiar dyktu niezmieniony - np. Zastąp dowolny klucz / wartość zamiast je usunąć, to ten kod nie zgłosi wyjątku
Artsiom Rudzenka

Nie zgadzam się, że „jest całkiem oczywiste, jak to naprawić, jeśli jest zepsute” dla wszystkich szukających tego tematu (w tym dla mnie) i chciałbym, aby zaakceptowana odpowiedź przynajmniej dotknęła tego.
Alex Peters

Odpowiedzi:


54

Jest to wyraźnie wspomniane na stronie dokumentacji Pythona (dla Pythona 2.7 ), że

Używanie iteritems()podczas dodawania lub usuwania wpisów w słowniku może spowodować RuntimeErrorlub nie wykonać iteracji po wszystkich wpisach.

Podobnie dla Pythona 3 .

To samo dotyczy iter(d), d.iterkeys()i d.itervalues(), i pójdę tak daleko, jak mówi, że robi dla for k, v in d.items():(nie pamiętam dokładnie, co forrobi, ale nie zdziwiłbym się, gdyby realizacja nazywa iter(d)).


51
Dla dobra społeczności zawstydzę się stwierdzeniem, że użyłem samego fragmentu kodu. Myśląc, że skoro nie dostałem błędu RuntimeError, pomyślałem, że wszystko jest w porządku. I tak było przez chwilę. Testy jednostkowe zapamiętywane analnie dały mi kciuki do góry i nawet działały dobrze, kiedy zostały wydane. Potem zacząłem zachowywać się dziwnie. Dzieje się tak, że pozycje w słowniku były pomijane i nie wszystkie pozycje w słowniku były skanowane. Dzieci, uczcie się na błędach, które popełniłam w życiu i po prostu powiedzcie nie! ;)
Alan Cabrera

3
Czy mogę napotkać problemy, jeśli zmieniam wartość w bieżącym kluczu (ale nie dodam ani nie usuwam żadnych kluczy?) Wyobrażam sobie, że nie powinno to powodować żadnych problemów, ale chciałbym wiedzieć!
Gershy

@GershomMaes Nie znam żadnego, ale nadal możesz wpaść na pole minowe, jeśli twoja pętla użyje wartości i nie spodziewa się, że się zmieni.
Raphaël Saint-Pierre

3
d.items()powinno być bezpieczne w Pythonie 2.7 (gra zmienia się wraz z Pythonem 3), ponieważ tworzy to, co w zasadzie jest kopią d, więc nie modyfikujesz tego, nad czym iterujesz.
Paul Price

Byłoby interesujące wiedzieć, czy dotyczy to równieżviewitems()
jlh

51

Alex Martelli rozważa to tutaj .

Zmiana kontenera (np. Dykt) podczas wykonywania pętli nad kontenerem może nie być bezpieczna. Więc del d[f(k)]może nie być bezpieczne. Jak wiesz, obejściem jest użycie d.items()(aby zapętlić niezależną kopię kontenera) zamiast d.iteritems()(który używa tego samego podstawowego kontenera).

Modyfikowanie wartości w istniejącym indeksie dyktatu jest w porządku , ale wstawianie wartości w nowych indeksach (np. d[g(k)]=v) Może nie działać.


3
Myślę, że to dla mnie kluczowa odpowiedź. Wiele przypadków użycia będzie wymagało jednego procesu wstawiania rzeczy, a innego czyszczenia / usuwania ich, więc porada dotycząca używania d.items () działa.
Zastrzeżenia do

4
Więcej informacji na temat zastrzeżeń Python 3 można znaleźć w PEP 469, w którym wyliczono semantyczne odpowiedniki wyżej wymienionych metod dyktowania w Pythonie 2.
Lionel Brooks

1
„Można zmodyfikować wartość w istniejącym indeksie dyktu” - czy masz do tego odniesienie?
Jonathon Reinhart

1
@JonathonReinhart: Nie, nie mam do tego odniesienia, ale myślę, że jest to dość standardowe w Pythonie. Na przykład Alex Martelli był głównym programistą Pythona i tutaj demonstruje jego zastosowanie .
unutbu

27

Nie możesz tego zrobić, przynajmniej z d.iteritems(). Próbowałem, ale Python zawodzi

RuntimeError: dictionary changed size during iteration

Jeśli zamiast tego użyjesz d.items(), to działa.

W Pythonie 3 d.items()jest to widok do słownika, podobnie jak d.iteritems()w Pythonie 2. Aby to zrobić w Pythonie 3, użyj zamiast tego d.copy().items(). Pozwoli to nam podobnie na iterację kopii słownika, aby uniknąć modyfikowania struktury danych, nad którą iterujemy.


2
Dodałem Python 3 do mojej odpowiedzi.
murgatroid99

2
FYI, dosłowne tłumaczenie (jak np. Używane przez 2to3) Py2 na d.items()Py3 jest list(d.items()), chociaż d.copy().items()prawdopodobnie ma porównywalną wydajność.
Søren Løvborg

2
Jeśli obiekt dict jest bardzo duży, czy d.copy (). Items () jest skuteczny?
Dragonfly

12

Mam duży słownik zawierający tablice Numpy, więc dict.copy (). Keys () sugerowane przez @ murgatroid99 nie było wykonalne (chociaż zadziałało). Zamiast tego po prostu przekonwertowałem keys_view na listę i działało dobrze (w Pythonie 3.4):

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

Zdaję sobie sprawę, że nie zagłębia się to w filozoficzną sferę wewnętrznego działania Pythona, jak powyższe odpowiedzi, ale zapewnia praktyczne rozwiązanie przedstawionego problemu.


6

Poniższy kod pokazuje, że nie jest to dobrze zdefiniowane:

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

Pierwszy przykład wywołuje g (k) i zgłasza wyjątek (słownik zmienił rozmiar podczas iteracji).

Drugi przykład wywołuje h (k) i nie zgłasza wyjątku, ale wyświetla:

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

Co, patrząc na kod, wydaje się błędne - spodziewałbym się czegoś takiego:

{11: 'ax', 12: 'bx', 13: 'cx'}

Rozumiem, dlaczego możesz się spodziewać, {11: 'ax', 12: 'bx', 13: 'cx'}ale 21,22,23 powinno dać ci wskazówkę, co się właściwie wydarzyło: twoja pętla przeszła przez pozycje 1, 2, 3, 11, 12, 13, ale nie udało się odebrać drugiej runda nowych elementów, które zostały wstawione przed elementami, które już przeszedłeś. Zmień, h()aby powrócić, x+5a otrzymasz kolejny x: 'axxx'itd. Lub 'x + 3' i otrzymasz wspaniały'axxxxx'
Duncan

Tak, obawiam się, że to mój błąd - oczekiwany wynik był {11: 'ax', 12: 'bx', 13: 'cx'}taki, jak powiedziałeś, więc zaktualizuję o tym mój post. Tak czy inaczej, nie jest to wyraźnie określone zachowanie.
Combatdave,

1

Mam ten sam problem i zastosowałem następującą procedurę, aby rozwiązać ten problem.

Lista Pythona może być iterowana, nawet jeśli modyfikujesz ją podczas iteracji. więc dla następującego kodu będzie drukować jedynki w nieskończoność.

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

Więc używając listy i dyktuj wspólnie, możesz rozwiązać ten problem.

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

Nie jestem pewien, czy modyfikowanie listy podczas iteracji jest bezpieczne (chociaż w niektórych przypadkach może to zadziałać). Zobacz na przykład to pytanie ...
Roman

@Roman Jeśli chcesz usunąć elementy listy, możesz bezpiecznie iterować po niej w odwrotnej kolejności, ponieważ w normalnej kolejności indeks następnego elementu zmieniłby się po usunięciu. Zobacz ten przykład.
mbomb007

1

Python 3 powinieneś po prostu:

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

albo użyj:

t2 = t1.copy()

Nigdy nie należy modyfikować oryginalnego słownika, prowadzi to do nieporozumień, a także potencjalnych błędów lub błędów RunTimeErrors. Chyba że po prostu dołączysz do słownika nowe nazwy kluczy.


0

Dzisiaj miałem podobny przypadek użycia, ale zamiast po prostu materializować klucze w słowniku na początku pętli, chciałem, aby zmiany w dyktandzie wpływały na iterację dyktu, który był nakazem uporządkowanym.

Skończyło się na stworzeniu następującej procedury, którą można również znaleźć w 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)

Dokumentacja ilustruje użycie. Funkcji tej można użyć zamiast d.iteritems()powyższej, aby uzyskać pożądany efekt.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.