Como excluir itens de um dicionário enquanto itera sobre ele?

295

É legítimo excluir itens de um dicionário em Python enquanto itera sobre ele?

Por exemplo:

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

A idéia é remover elementos que não atendem a uma determinada condição do dicionário, em vez de criar um novo dicionário que seja um subconjunto do que está sendo iterado.

Esta é uma boa solução? Existem maneiras mais elegantes / eficientes?

Trilarion
fonte
1
Uma pergunta relacionada com respostas muito interessantes: stackoverflow.com/questions/9023078/… .
max
Alguém poderia ter tentado facilmente. Se falhar, não é legítimo.
precisa saber é o seguinte
26
@ Trilarion Alguém poderia ter tentado facilmente ... e facilmente aprendido nada de valor. Se for bem sucedido, é não necessariamente legítimo. Casos extremos e advertências inesperadas são abundantes. Esta questão é de interesse não trivial para todos os possíveis Pythonistas. Despedida de mão na ordem de "Alguém poderia ter tentado facilmente!" é inútil e contrário ao espírito inquisitivo da investigação sobre o fluxo de pilha.
Cecil Curry
Depois de ler a pergunta relacionada de max , devo concordar. Você provavelmente só quer ler essa pergunta perturbadora e profunda e suas respostas bem escritas. Sua mente pitônica será explodida.
22616 Cecil Curry
1
@CecilCurry Testar uma idéia para si mesmo antes de apresentá-la aqui é uma espécie de espírito do stackoverflow, se não me engano. Era tudo o que eu queria transmitir. Desculpe se houve algum distúrbio por causa disso. Também acho que é uma boa pergunta e não a diminuiu. Eu gosto mais da resposta de Jochen Ritzel . Não acho que seja necessário fazer tudo isso para excluir rapidamente quando excluir em um segundo passo é muito mais simples. Essa deve ser a maneira preferida na minha opinião.
Trilarion

Respostas:

305

EDITAR:

Esta resposta não funcionará para Python3 e fornecerá a RuntimeError.

RuntimeError: o dicionário mudou de tamanho durante a iteração.

Isso acontece porque mydict.keys()retorna um iterador, não uma lista. Conforme indicado nos comentários, basta converter mydict.keys()em uma lista por list(mydict.keys())e deve funcionar.


Um teste simples no console mostra que você não pode modificar um dicionário enquanto itera sobre ele:

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

Conforme indicado na resposta de delnan, a exclusão de entradas causa problemas quando o iterador tenta passar para a próxima entrada. Em vez disso, use o keys()método para obter uma lista das chaves e trabalhar com isso:

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

Se você precisar excluir com base no valor dos itens, use o items()método:

>>> for k, v in mydict.items():
...     if v == 3:
...         del mydict[k]
...
>>> mydict
{'four': 4, 'one': 1}
Blair
fonte
53
Observe que no Python 3, dict.items () retorna um iterador (e dict.iteritems () se foi).
Tim Lesher
83
Para elaborar comentário @TimLesher ... Isso não vai funcionar em Python 3.
max
99
Para elaborar a elaboração do @ max, funcionará se você converter o código acima com 2to3. Um dos fixadores padrão fará com que o loop se pareça com o for k, v in list(mydict.items()):que funciona bem no Python 3. O mesmo para keys()se tornar list(keys()).
Walter Mundt
8
Isso não funciona. Eu recebo um erro:RuntimeError: dictionary changed size during iteration
Tomáš Zato - Restabelece Monica
15
@ TomášZato como Walter apontou, para python3 você precisa usar for k in list(mydict.keys()): como python3 faz do método keys () um iterador e também não permite excluir itens de dict durante a iteração. Ao adicionar uma chamada de lista (), você transforma o iterador de chaves () em uma lista. Portanto, quando você está no corpo do loop for, não está mais iterando sobre o próprio dicionário.
Geoff Crompton 22/03
89

Você também pode fazer isso em duas etapas:

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

Minha abordagem favorita é geralmente fazer um novo ditado:

# 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
fonte
11
@ remetente: Desde 2,7, na verdade.
Jochen Ritzel 22/03
5
A abordagem de compreensão de ditado faz uma cópia do dicionário; felizmente, os valores pelo menos não são copiados em profundidade, apenas vinculados. Ainda assim, se você tiver muitas chaves, pode ser ruim. Por esse motivo, gosto removemais da abordagem de loop.
max
1
Você também pode combinar as etapas:for k in [k for k in mydict if k == val]: del mydict[k]
AXO
a primeira solução é a única eficiente em dictos grandes neste segmento até agora - já que não faz uma cópia completa.
Kxr # 28/17
21

Você não pode modificar uma coleção enquanto a itera. Essa maneira é loucura - principalmente, se você tivesse permissão para excluir e excluir o item atual, o iterador teria que seguir em frente (+1) e a próxima chamada nextlevaria você além disso (+2), para que você acabam pulando um elemento (o logo atrás do que você excluiu). Você tem duas opções:

  • Copie todas as chaves (ou valores, ou ambos, dependendo do que você precisa) e, em seguida, itere sobre elas. Você pode usar .keys()et al para isso (no Python 3, passe o iterador resultante para list). Pode ser altamente desperdiçador em termos de espaço.
  • Faça a iteração mydictnormalmente, salvando as chaves para excluir em uma coleção separada to_delete. Ao concluir a iteração mydict, exclua todos os itens to_deletede mydict. Economiza espaço (dependendo de quantas chaves foram excluídas e quantas permanecem) na primeira abordagem, mas também requer mais algumas linhas.

fonte
You can't modify a collection while iterating it.isso é correto para dictos e amigos, mas você pode modificar as listas durante a iteração: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 Não lança uma exceção, mas ainda está incorreta. Observe: codepad.org/Yz7rjDVT - veja por exemplo stackoverflow.com/q/6260089/395760 para obter uma explicação
Me pegou aqui. Ainda can'té correto apenas para dict e amigos, enquanto deve ser shouldn'tpara listas.
Nils Lindemann
21

Itere uma cópia, como a retornada por items():

for k, v in list(mydict.items()):
Ignacio Vazquez-Abrams
fonte
1
Isso não faz muito sentido - então você não pode del vdiretamente, portanto, você fez uma cópia de cada v que você nunca vai usar e precisa acessar os itens por qualquer tecla. dict.keys()é uma escolha melhor.
JSCs
2
@ Josh: Tudo depende de quanto você precisará usar vcomo critério de exclusão.
Ignacio Vazquez-Abrams
3
No Python 3, dict.items()retorna um iterador em vez de uma cópia. Veja o comentário de Blair 's resposta , que (infelizmente) também assume Python 2 semântica.
22616 Cecil Curry
11

É mais limpo de usar 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}

Isso corresponde a uma estrutura paralela para listas:

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

Ambos funcionam em python2 e python3.

rsanden
fonte
Isso não é bom caso seu conjunto de dados seja grande. Isso está copiando todos os objetos na memória, certo?
AFP_555
1
@ AFP_555 Sim - meu objetivo aqui é o código python limpo, paralelo. Se você precisar de eficiência de memória, a melhor abordagem que conheço é iterar e criar uma lista de chaves para excluir ou um novo ditado de itens para salvar. A beleza é minha prioridade no Python; para grandes conjuntos de dados, estou usando Go ou Rust.
rsanden 10/02
9

Você pode usar uma compreensão de dicionário.

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

Aaron
fonte
Este é o mais pitonico.
Yehosef 13/01
Mas cria um novo dicionário em vez de modificar dno lugar.
Aristide
9

Com python3, iterar em dic.keys () aumentará o erro de tamanho do dicionário. Você pode usar esta maneira alternativa:

Testado com python3, ele funciona bem e o erro " tamanho do dicionário alterado durante a iteração " não é gerado:

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

Você pode primeiro criar uma lista de chaves para excluir e, em seguida, iterar sobre essa lista, excluindo-as.

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
fonte
É uma duplicata da 1ª solução do @ Ritzel (eficiente em dictos grandes sem cópia completa). Embora uma "leitura longa" sem compreensão da lista. No entanto, é possivelmente mais rápido, no entanto?
KXR
3

Existe uma maneira que pode ser adequada se os itens que você deseja excluir estiverem sempre no "início" da iteração de ditado

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

O "começo" é garantido apenas para ser consistente em determinadas versões / implementações do Python. Por exemplo, em Novidades no Python 3.7

a natureza da preservação da ordem de inserção dos objetos dict foi declarada parte oficial da especificação da linguagem Python.

Dessa forma, evita-se uma cópia do ditado que muitas das outras respostas sugerem, pelo menos no Python 3.

Michal Charemza
fonte
1

Eu tentei as soluções acima em Python3, mas esta parece ser a única que funciona para mim ao armazenar objetos em um ditado. Basicamente, você faz uma cópia do seu dict () e itera sobre ele enquanto exclui as entradas no seu dicionário original.

        tmpDict = realDict.copy()
        for key, value in tmpDict.items():
            if value:
                del(realDict[key])
JasonLandbridge
fonte