Изменение словаря Python при его повторении

87

Допустим, у нас есть словарь Python d, и мы повторяем его так:

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

( fи gэто просто некоторые преобразования черного ящика.)

Другими словами, мы пытаемся добавлять / удалять элементы в dпроцессе итерации, используя iteritems.

Это хорошо определено? Не могли бы вы привести несколько ссылок в поддержку своего ответа?

(Довольно очевидно, как исправить это, если он сломан, так что это не тот угол, который мне нужен.)

NPE
источник
1
См. Stackoverflow.com/questions/5384914/…
Тодд Мозес
Я попытался сделать это, и кажется, что если вы оставите исходный размер дикта без изменений - например, замените любой ключ / значение вместо их удаления, тогда этот код не будет генерировать исключение
Артем Рудзенка
Я не согласен с тем, что «довольно очевидно, как это исправить, если он сломан» для всех, кто ищет эту тему (включая меня), и я бы хотел, чтобы принятый ответ хотя бы коснулся этого.
Alex Peters

Ответы:

53

На странице документации Python (для Python 2.7 ) явно указано, что

Использование iteritems()при добавлении или удалении записей в словаре может вызвать RuntimeErrorили не выполнить итерацию по всем записям.

Аналогично для Python 3 .

То же самое справедливо для iter(d), d.iterkeys()и d.itervalues(), и я скажу, что это действительно так for k, v in d.items():(я не могу точно вспомнить, что forделает, но я не удивлюсь, если вызовет реализацию iter(d)).

Рафаэль Сен-Пьер
источник
49
Я поставлю себя в неловкое положение ради сообщества, заявив, что я использовал тот самый фрагмент кода. Думая, что, поскольку я не получил RuntimeError, я думал, что все в порядке. И так было какое-то время. Модульные тесты с сохранением анальной памяти вызвали у меня большой палец вверх, и, когда он был выпущен, он даже работал хорошо. Затем я стал вести себя странно. Происходило то, что элементы в словаре пропускались, и поэтому не все элементы в словаре сканировались. Дети, учитесь на ошибках, которые я сделал в своей жизни, и просто говорите «нет»! ;)
Алан Кабрера
3
Могу ли я столкнуться с проблемами, если я изменяю значение в текущем ключе (но не добавляю и не удаляю какие-либо ключи?) Я бы сказал, что это не должно вызвать никаких проблем, но я хотел бы знать!
Gershom,
@GershomMaes Я не знаю ни одного, но вы все равно можете столкнуться с минным полем, если ваше тело цикла использует значение и не ожидает его изменения.
Raphaël Saint-Pierre
3
d.items()должен быть безопасным в Python 2.7 (игра меняется с Python 3), поскольку он делает то, что по сути является копией d, поэтому вы не изменяете то, что повторяете.
Пол Прайс
Было бы интересно узнать, верно ли это и дляviewitems()
jlh
50

Алекс Мартелли весит на этом здесь .

Может быть небезопасно менять контейнер (например, dict) во время обхода контейнера. Так что del d[f(k)]может быть небезопасно. Как вы знаете, обходной путь заключается в использовании d.items()(для перебора независимой копии контейнера) вместо d.iteritems()(который использует тот же базовый контейнер).

Можно изменить значение в существующем индексе словаря, но вставка значений в новые индексы (например d[g(k)]=v) может не работать.

Unutbu
источник
3
Думаю, это ключевой ответ для меня. Во многих случаях использования один процесс будет вставлять вещи, а другой очищать / удалять их, поэтому совет по использованию d.items () работает. Python 3 предостережения не выдерживает
easytiger
4
Дополнительную информацию о предостережениях Python 3 можно найти в PEP 469, где перечислены семантические эквиваленты вышеупомянутых методов dict Python 2.
Лайонел Брукс,
1
«Можно изменить значение в существующем индексе словаря» - у вас есть ссылка для этого?
Джонатон Рейнхарт
1
@JonathonReinhart: Нет, у меня нет ссылки на это, но я думаю, что это довольно стандартно для Python. Например, Алекс Мартелли был разработчиком ядра Python и здесь демонстрирует его использование .
unutbu
27

Вы не можете этого сделать, по крайней мере, с помощью d.iteritems(). Я попробовал, и Python не справился с

RuntimeError: dictionary changed size during iteration

Если вы вместо этого используете d.items() , то работает.

В Python 3 d.items()это представление в словарь, как d.iteritems()в Python 2. Для этого в Python 3 вместо этого используйте d.copy().items(). Это также позволит нам перебирать копию словаря, чтобы избежать изменения структуры данных, которую мы перебираем.

murgatroid99
источник
2
Я добавил к своему ответу Python 3.
murgatroid99
2
К вашему сведению, буквальный перевод (например, используемый 2to3) Py2 d.items()на Py3 есть list(d.items()), хотя d.copy().items(), вероятно, имеет сопоставимую эффективность.
Søren Løvborg
2
Если объект dict очень большой, эффективен ли d.copy (). Items ()?
стрекоза
11

У меня есть большой словарь, содержащий массивы Numpy, поэтому вещь dict.copy (). Keys (), предложенная @ murgatroid99, была невозможна (хотя она работала). Вместо этого я просто преобразовал keys_view в список, и он работал нормально (в Python 3.4):

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

Я понимаю, что это не касается философской области внутренней работы Python, как ответы выше, но это дает практическое решение заявленной проблемы.

2циныкил
источник
6

Следующий код показывает, что это не совсем точно:

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

Первый пример вызывает g (k) и выдает исключение (размер словаря изменился во время итерации).

Второй пример вызывает h (k) и не выдает исключения, но выводит:

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

Что, глядя на код, кажется неправильным - я ожидал чего-то вроде:

{11: 'ax', 12: 'bx', 13: 'cx'}
боевой
источник
Я могу понять, почему вы могли ожидать, {11: 'ax', 12: 'bx', 13: 'cx'}но 21,22,23 должны дать вам представление о том, что на самом деле произошло: ваш цикл прошел через пункты 1, 2, 3, 11, 12, 13, но не смог подобрать второй раунд новых элементов по мере их вставки перед элементами, которые вы уже повторяли. Измените, h()чтобы вернуться, x+5и вы получите еще один x: 'axxx'и т. Д. Или «x + 3», и вы получите великолепное'axxxxx'
Дункан
Да, боюсь, моя ошибка - мои ожидаемые результаты были {11: 'ax', 12: 'bx', 13: 'cx'}такими, как вы сказали, поэтому я обновлю свой пост об этом. В любом случае, это явно не определенное поведение.
Combatdave
1

У меня та же проблема, и я использовал следующую процедуру для решения этой проблемы.

Список Python можно перебирать, даже если вы изменяете его во время итерации. поэтому для следующего кода он будет печатать 1 бесконечно.

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

Таким образом, используя list и dict вместе, вы можете решить эту проблему.

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))
Зил Шах
источник
Я не уверен, безопасно ли изменять список во время итерации (хотя в некоторых случаях это может сработать). См. Этот вопрос для примера ...
Роман
@Roman Если вы хотите удалить элементы списка, вы можете безопасно перебирать его в обратном порядке, поскольку в обычном порядке индекс следующего элемента изменится при удалении. См. Этот пример.
mbomb007
1

Python 3 вам следует просто:

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

или используйте:

t2 = t1.copy()

Вы никогда не должны изменять исходный словарь, это приведет к путанице, а также к возможным ошибкам или ошибкам RunTimeErrors. Если только вы не добавите в словарь новые имена ключей.

Декстер
источник
0

Сегодня у меня был аналогичный вариант использования, но вместо того, чтобы просто материализовать ключи в словаре в начале цикла, я хотел, чтобы изменения в dict повлияли на итерацию dict, которая была упорядоченным dict.

В итоге я создал следующую процедуру, которую также можно найти в 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)

Строка документации иллюстрирует использование. Эту функцию можно использовать вместо d.iteritems()указанной выше для достижения желаемого эффекта.

Джейсон Р. Кумбс
источник