Modificar um dicionário Python durante a iteração sobre ele

87

Digamos que temos um dicionário Python de estamos iterando sobre ele assim:

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

( fe gsão apenas algumas transformações de caixa preta.)

Em outras palavras, tentamos adicionar / remover itens ddurante a iteração usando iteritems.

Isso está bem definido? Você poderia fornecer algumas referências para apoiar sua resposta?

(É bastante óbvio como consertar isso se estiver quebrado, então este não é o ângulo que procuro.)

NPE
fonte
Eu tentei fazer isso e parece que se você deixar o tamanho do dicionário inicial inalterado - por exemplo, substituir qualquer chave / valor em vez de removê-los, este código não gerará exceção
Artsiom Rudzenka
Não concordo que seja “bastante óbvio como consertar isso se estiver quebrado” para todos que procuram esse tópico (incluindo eu) e gostaria que a resposta aceita pelo menos tocasse nisso.
Alex Peters

Respostas:

53

É explicitamente mencionado na página de documentação do Python (para Python 2.7 ) que

Usar iteritems()ao adicionar ou excluir entradas no dicionário pode gerar a RuntimeErrorou falhar na iteração de todas as entradas.

Da mesma forma para Python 3 .

O mesmo vale para iter(d), d.iterkeys()e d.itervalues(), e vou mais longe dizendo que serve para for k, v in d.items():(não me lembro exatamente o que forserve, mas não ficaria surpreso se a implementação fosse chamada iter(d)).

Raphaël Saint-Pierre
fonte
48
Vou me envergonhar pelo bem da comunidade, afirmando que usei o próprio trecho de código. Pensando que como não recebi um RuntimeError, achei que estava tudo bem. E foi, por um tempo. Os testes de unidade analiticamente retentivos estavam me dando sinal de positivo e estava até funcionando bem quando foi lançado. Então, comecei a ter um comportamento bizarro. O que estava acontecendo é que os itens do dicionário estavam sendo ignorados e, portanto, nem todos os itens do dicionário estavam sendo lidos. Crianças, aprendam com os erros que cometi na minha vida e apenas digam não! ;)
Alan Cabrera
3
Posso ter problemas se estiver alterando o valor na chave atual (mas não adicionando ou removendo nenhuma chave?) Gostaria de imaaaagine que isso não deve causar problemas, mas gostaria de saber!
Gershom
@GershomMaes Não sei de nenhum, mas você ainda pode estar entrando em um campo minado caso seu corpo de loop faça uso do valor e não espere que ele mude.
Raphaël Saint-Pierre de
3
d.items()deve ser seguro no Python 2.7 (o jogo muda com o Python 3), pois faz o que é essencialmente uma cópia d, então você não está modificando o que está iterando.
Paul Price
Seria interessante saber se isso também é verdade paraviewitems()
jlh
50

Alex Martelli pesa sobre isso aqui .

Pode não ser seguro mudar o recipiente (por exemplo, dict) enquanto faz um loop sobre o recipiente. Portanto, del d[f(k)]pode não ser seguro. Como você sabe, a solução alternativa é usar d.items()(para fazer um loop em uma cópia independente do contêiner) em vez de d.iteritems()(que usa o mesmo contêiner subjacente).

É normal modificar o valor em um índice existente do dicionário, mas inserir valores em novos índices (por exemplo d[g(k)]=v) pode não funcionar.

unutbu
fonte
3
Eu acho que esta é uma resposta chave para mim. Muitos casos de uso terão um processo inserindo coisas e outro limpando / excluindo, então o conselho para usar d.items () funciona. Python 3 advertências não suportadas
easytiger
4
Mais informações sobre as advertências do Python 3 podem ser encontradas no PEP 469, onde os equivalentes semânticos dos métodos dict do Python 2 mencionados anteriormente são enumerados.
Lionel Brooks,
1
"Não há problema em modificar o valor em um índice existente do dict" - você tem uma referência para isso?
Jonathon Reinhart de
1
@JonathonReinhart: Não, não tenho uma referência para isso, mas acho que é bastante padrão em Python. Por exemplo, Alex Martelli foi um desenvolvedor de núcleo Python e demonstra seu uso aqui .
unutbu
27

Você não pode fazer isso, pelo menos com d.iteritems(). Eu tentei, e Python falhou com

RuntimeError: dictionary changed size during iteration

Se você usar d.items(), então funciona.

No Python 3, d.items()é uma visão do dicionário, como d.iteritems()no Python 2. Para fazer isso no Python 3, use d.copy().items(). Isso nos permitirá, da mesma forma, iterar sobre uma cópia do dicionário para evitar a modificação da estrutura de dados que estamos iterando.

murgatroid99
fonte
2
Eu adicionei Python 3 à minha resposta.
murgatroid99
2
FYI, a tradução literal (como, por exemplo, usada por 2to3) de Py2 d.items()para Py3 é list(d.items()), embora d.copy().items()seja provavelmente de eficiência comparável.
Søren Løvborg
2
Se o objeto dict for muito grande, d.copy (). Items () é eficiente?
libélula
11

Eu tenho um grande dicionário contendo matrizes Numpy, então a coisa dict.copy (). Keys () sugerida por @ murgatroid99 não era viável (embora funcionasse). Em vez disso, acabei de converter keys_view em uma lista e funcionou bem (no Python 3.4):

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

Sei que isso não mergulha no reino filosófico do funcionamento interno do Python, como as respostas acima, mas fornece uma solução prática para o problema declarado.

2cynykyl
fonte
6

O código a seguir mostra que isso não está bem definido:

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

O primeiro exemplo chama g (k) e lança uma exceção (o dicionário mudou de tamanho durante a iteração).

O segundo exemplo chama h (k) e não lança nenhuma exceção, mas resulta:

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

O que, olhando para o código, parece errado - eu esperava algo como:

{11: 'ax', 12: 'bx', 13: 'cx'}
combatdave
fonte
Eu posso entender porque você pode esperar, {11: 'ax', 12: 'bx', 13: 'cx'}mas o 21,22,23 deve lhe dar uma pista sobre o que realmente aconteceu: seu loop passou pelos itens 1, 2, 3, 11, 12, 13, mas não conseguiu pegar o segundo rodada de novos itens à medida que são inseridos na frente dos itens que você já havia iterado. Mude h()para voltar x+5e você obterá outro x: 'axxx'etc. ou 'x + 3' e obterá o magnífico'axxxxx'
Duncan
Sim, meu erro, infelizmente - minha saída esperada era {11: 'ax', 12: 'bx', 13: 'cx'}como você disse, então vou atualizar meu post sobre isso. De qualquer forma, esse comportamento claramente não está bem definido.
combatdave
1

Eu tive o mesmo problema e usei o seguinte procedimento para resolver esse problema.

A lista Python pode ser iterada mesmo se você modificar durante a iteração sobre ela. então, para seguir o código, ele imprimirá 1's infinitamente.

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

Então, usando list e dict colaborativamente, você pode resolver esse problema.

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))
Zeel Shah
fonte
Não tenho certeza se é seguro modificar uma lista durante a iteração (embora possa funcionar em alguns casos). Veja esta pergunta, por exemplo ...
Romano
@Roman Se você quiser deletar elementos de uma lista, você pode iterar com segurança sobre ela na ordem reversa, já que na ordem normal o índice do próximo elemento mudaria na deleção. Veja este exemplo.
mbomb007
1

Python 3, você deve apenas:

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

ou use:

t2 = t1.copy()

Você nunca deve modificar o dicionário original, isso leva à confusão, bem como a possíveis bugs ou RunTimeErrors. A menos que você apenas anexe ao dicionário novos nomes de chave.

Dexter
fonte
0

Hoje eu tive um caso de uso semelhante, mas em vez de simplesmente materializar as chaves no dicionário no início do loop, eu queria que as alterações no dict afetassem a iteração do dict, que era um dict ordenado.

Acabei construindo a seguinte rotina, que também pode ser encontrada no 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)

A docstring ilustra o uso. Esta função pode ser usada no lugar das d.iteritems()acima para obter o efeito desejado.

Jason R. Coombs
fonte