Модифікація дикту 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.

Це чітко визначено? Не могли б Ви надати посилання на підтримку Вашої відповіді?

(Цілком очевидно, як це виправити, якщо воно зламане, тому це не той кут, за яким я переслідую.)




Я спробував це зробити, і, здається, якщо залишити початковий розмір дикту незмінним - наприклад, замінити будь-який ключ / значення замість того, щоб їх видаляти, тоді цей код не викличе виключення
Артьом Рудзенка

Я не згоден з тим, що для всіх, хто шукає цю тему (включаючи мене самого), "досить очевидно, як це виправити, якщо вона зламана", і хотів би, щоб прийнята відповідь принаймні торкнулася цього.
Алекс Пітерс,

Відповіді:


53

Про це прямо згадується на сторінці документа Python (для Python 2.7 )

Використання iteritems()під час додавання чи видалення записів у словнику може викликати RuntimeErrorабо не виконати ітерацію над усіма записами.

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

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


48
Я збентежу себе заради спільноти, заявивши, що використовував той самий фрагмент коду. Думаючи, що оскільки я не отримав RuntimeError, я думав, що все добре. І це було, деякий час. Анально-утримуючі модульні тести давали мені великий палець, і навіть він працював добре, коли його випустили. Потім я почав химерно поводитися. Те, що відбувалося, було те, що елементи у словнику пропускали, тому не всі елементи у словнику сканувались. Діти, вчіться на помилках, які я допустив у своєму житті, і просто скажіть «ні»! ;)
Алан Кабрера

3
Чи можу я зіткнутися з проблемами, якщо я змінюю значення за поточним ключем (але не додаю чи видаляю жодні ключі?), Я міг уявити, що це не повинно викликати проблем, але я хотів би знати!
Гершом

@GershomMaes Я не знаю жодного, але ви все одно можете зіткнутися з мінним полем, якщо ваше тіло циклу використовує значення і не очікує, що воно зміниться.
Рафаель Сен-П'єр,

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) може не спрацювати.


3
Я думаю, що це ключова відповідь для мене. У багатьох випадках використання один процес буде вставляти речі, а інший чистити речі / видаляти їх, тому порада щодо використання d.items () працює.
Застереження

4
Більше інформації про застереження щодо Python 3 можна знайти в PEP 469, де перераховані семантичні еквіваленти згаданих вище методів диктування 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(). Це також дозволить нам виконати ітерацію над копією словника, щоб уникнути модифікації структури даних, яку ми переглядаємо.


2
Я додав Python 3 до своєї відповіді.
murgatroid99

2
FYI, дослівний переклад (як, наприклад, використовується 2to3) Py2 d.items()на Py3 є list(d.items()), хоча d.copy().items(), ймовірно, порівнянної ефективності.
Søren Løvborg

2
Якщо об’єкт dict дуже великий, чи є ecopiet 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

Я усвідомлюю, що це не занурюється у філософську сферу внутрішньої роботи Пітона, як відповіді вище, але воно надає практичне рішення заявленої проблеми.


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

Так, моя помилка, я боюся - очікуваний результат був {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

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

У підсумку я створив таку процедуру, яку також можна знайти у 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()вищезазначеної, щоб мати бажаний ефект.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.