Як видалити елементи зі словника, повторюючи його?


295

Чи правомірно видаляти елементи зі словника в Python під час ітерації над ним?

Наприклад:

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

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

Це гарне рішення? Чи є більш елегантні / ефективні способи?


1
Пов'язаний з цим питання дуже цікаві відповіді: stackoverflow.com/questions/9023078 / ... .
макс

Можна було б спробувати легко. Якщо це не вдасться, це не є законним.
Триларіон

26
@Trilarion Можна було б спробувати легко ... і легко дізнатися нічого цінного. Якщо це досягає успіху, це не обов'язково легітимно. Корпуси кромки та несподівані застереження мають багато. Це питання не цікавить усіх майбутніх пітоністів. Звільнення з розмахуванням рукою за наказом "Можна було б спробувати легко!" є безкорисним і суперечить допитливому духу запиту стаціонарного потоку.
Сесіль Карі

Ознайомившись макс «S пов'язане питання , я повинен погодитися. Напевно, ви просто хочете вивчити це тривожно поглиблене запитання та замість нього добре написані відповіді. Ваш пітонічний розум буде роздутий.
Сесіль Карі

1
@CecilCurry Тестування ідеї для себе, перш ніж представити її тут, начебто в дусі stackoverflow, якщо я не помиляюся. Це було все, що я хотів передати. Вибачте, якщо через це виникли якісь порушення. Також я вважаю, що це гарне питання, і я його не спростовував. Найбільше мені подобається відповідь Йочен Рітцел . Я не думаю, що при видаленні на другому кроці потрібно все, що потрібно видалити під час видалення, набагато простіше. На мій погляд, це повинен бути кращим способом.
Триларіон

Відповіді:


305

Редагувати:

Ця відповідь не підійде для Python3 і дасть відповідь RuntimeError.

RuntimeError: словник змінив розмір під час ітерації.

Це відбувається тому, mydict.keys()що ітератор повертає не список. Як зазначається в коментарях, просто перетворіться mydict.keys()на список, list(mydict.keys())і він повинен працювати.


Простий тест в консолі показує, що ви не можете змінювати словник під час ітерації над ним:

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

Як зазначено у відповіді Делнан, видалення записів спричиняє проблеми, коли ітератор намагається перейти до наступного запису. Замість цього скористайтеся keys()методом, щоб отримати список ключів і працювати з цим:

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

Якщо вам потрібно видалити на основі значень елементів, items()замість цього скористайтеся методом:

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

53
Зауважте, що в Python 3 dict.items () повертає ітератор (і dict.iteritems () немає).
Тім Лешер

83
Детальніше про коментар @TimLesher ... Це НЕ буде працювати в Python 3.
макс

99
Щоб отримати детальну розробку @ max, вона буде спрацьовувати, якщо ви перетворите вищевказаний код за допомогою 2to3. Один із фіксаторів за замовчуванням зробить цикл таким, for k, v in list(mydict.items()):який добре працює в Python 3. Те ж саме для keys()становлення list(keys()).
Вальтер Мундт

8
Це не працює. Я отримую помилку:RuntimeError: dictionary changed size during iteration
Томаш Зато - Поновіть Моніку

14
@ TomášZato, як зазначив Уолтер, для python3 вам потрібно використовувати, for k in list(mydict.keys()): оскільки python3 робить метод ключів () ітератором, а також забороняє видалення елементів dict під час ітерації. Додавши виклик списку (), ви перетворюєте ітератор клавіш () у список. Тож, перебуваючи в тілі циклу for, ви більше не переглядаєте словник.
Джефф Кромптон

89

Ви також можете зробити це в два етапи:

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

Мій улюблений підхід, як правило, просто скласти новий диктант:

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

11
@senderle: з 2.7 фактично.
Jochen Ritzel

5
Підхід до розуміння дикту робить копію словника; на щастя, значення принаймні не отримують глибоко скопійованих, а просто пов'язаних. Але якщо у вас багато ключів, це може бути погано. З цієї причини мені більше подобається removeциклічний підхід.
макс

1
Ви також можете комбінувати кроки:for k in [k for k in mydict if k == val]: del mydict[k]
AXO

Перше рішення є єдиним ефективним на сьогодні великим диктом у цій темі - оскільки воно не робить копію в повну довжину.
kxr

21

Ви не можете змінювати колекцію під час її повторення. Таким чином лежить божевілля - головне, якщо б вам було дозволено видалити та видалити поточний елемент, ітератор повинен був би рухатися далі (+1), а наступний дзвінок переведе nextвас за межі цього (+2), тож ви в кінцевому підсумку пропускаючи один елемент (той, що знаходиться позаду того, який ви видалили). У вас є два варіанти:

  • Скопіюйте всі ключі (або значення, або обидва, залежно від того, що вам потрібно), а потім повторіть їх. Ви можете використовувати .keys()et al. Для цього (у Python 3 передайте отриманий ітератор list). Незважаючи на те, що це може бути дуже марно з простору.
  • Повторіть повторення, mydictяк правило, збереження ключів для видалення в окремій колекції to_delete. Закінчивши ітерацію mydict, видаліть усі елементи to_deleteз mydict. Економить деякий (залежно від того, скільки клавіш видалено та скільки залишилось) місця на першому підході, але також потрібно ще кілька рядків.

You can't modify a collection while iterating it.це правильно для диктів та друзів, але ви можете змінювати списки під час ітерації: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 Це не кидає винятку, але воно все-таки неправильне. Зверніть увагу: codepad.org/Yz7rjDVT - дивись , наприклад , stackoverflow.com/q/6260089/395760 для пояснення

Потрапив сюди. Все-таки can'tправильно лише для dict та друзів, тоді як це має бути shouldn'tдля списків.
Нілс Ліндеманн

20

Ітерація замість копії, наприклад, тієї, яку повернув items():

for k, v in list(mydict.items()):

1
Це не має особливого сенсу - тоді ви не можете del vбезпосередньо, тому ви зробили копію кожного v, який ви ніколи не будете використовувати, і вам доведеться отримувати доступ до елементів за ключем. dict.keys()- кращий вибір.
jscs

2
@Josh: Все залежить від того, скільки потрібно буде використовувати vяк критерій для видалення.
Ігнасіо Васкес-Абрамс

3
Під Python 3 dict.items()повертає ітератор, а не копію. Дивіться коментар для Блера «S відповіді , який ( до жаль) також приймає на себе Python 2 семантику.
Сесіль Карі

10

Найчистіше використовувати 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}

Це відповідає паралельній структурі списків:

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

Обидва працюють у python2 та python3.


Це не добре, якщо ваш набір даних великий. Це копіювання всіх об'єктів у пам'ять, правда?
AFP_555

1
@ AFP_555 Так - моя мета тут - чистий, паралельний, пітонічний код. Якщо вам потрібна ефективність пам’яті, найкращий підхід, який я знаю, - це повторити і скласти список клавіш для видалення або новий набір елементів для збереження. Краса - це мій пріоритет з Python; для великих наборів даних я використовую Go або Rust.
rsanden

9

Можна використовувати розуміння словника.

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


Це найбільше піфонічне.
Йозеф

Але він створює новий словник замість того, щоб змінювати його dна місці.
Арістід

9

З python3 ітерація dic.keys () призведе до помилки розміру словника. Ви можете використовувати цей альтернативний спосіб:

Тестований на python3, він працює чудово, і помилка " словник змінив розмір під час ітерації " не підвищується:

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 )
# {}

4

Ви можете спочатку скласти список ключів, які потрібно видалити, а потім повторити цей список, видаливши їх.

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]

Це досить копія першого рішення @ Ritzel (ефективне на великих диктатах без повної копії). Хоча розуміння списку без "довгого читання". І все-таки це можливо швидше?
kxr

3

Є спосіб, який може бути підходящим, якщо елементи, які ви хочете видалити, завжди знаходяться на "початку" ітерації диктанту

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

«Початок» гарантовано є послідовним лише для певних версій / реалізації Python. Наприклад, що нового в Python 3.7

характер збереження порядку вставки об’єктів dict був оголошений офіційною частиною специфікації мови Python.

Таким чином можна уникнути копії диктату, що підказує багато інших відповідей, принаймні в Python 3.


1

Я спробував вищевикладені рішення в Python3, але це, здається, єдине, що працює для мене при зберіганні об'єктів у dict. В основному ви робите копію свого dict () і повторюєте це, видаляючи записи в оригінальному словнику.

        tmpDict = realDict.copy()
        for key, value in tmpDict.items():
            if value:
                del(realDict[key])
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.