Os dicionários são ordenados no Python 3.6+?

469

Os dicionários são ordenados no Python 3.6 (pelo menos na implementação do CPython), diferente das encarnações anteriores. Parece uma mudança substancial, mas é apenas um pequeno parágrafo na documentação . Ele é descrito como um detalhe de implementação do CPython, e não como um recurso de linguagem, mas também implica que isso pode se tornar padrão no futuro.

Como a implementação do novo dicionário funciona melhor que a anterior, preservando a ordem dos elementos?

Aqui está o texto da documentação:

dict()agora usa uma representação "compacta" pioneira em PyPy . O uso de memória do novo dict () é entre 20% e 25% menor comparado ao Python 3.5. O PEP 468 (preservando a ordem de ** kwargs em uma função.) É implementado por isso. O aspecto de preservação de pedidos dessa nova implementação é considerado um detalhe da implementação e não deve ser considerado (isso pode mudar no futuro, mas é desejável que essa nova implementação de ditado no idioma seja liberada algumas vezes antes de alterar as especificações do idioma. para exigir a semântica de preservação de pedidos para todas as implementações atuais e futuras do Python, isso também ajuda a preservar a compatibilidade com versões anteriores da linguagem em que a ordem de iteração aleatória ainda está em vigor, por exemplo, Python 3.5). (Contribuição de INADA Naoki emedição 27350 . Idéia originalmente sugerida por Raymond Hettinger .)

Atualização em dezembro de 2017: dicta retenção de pedidos de inserção é garantida para Python 3.7

Chris_Rands
fonte
2
Veja este tópico na lista de discussão do Python-Dev: mail.python.org/pipermail/python-dev/2016-September/146327.html, se você não o viu; é basicamente uma discussão sobre esses assuntos.
mgc 11/10
1
Se os kwargs agora devem ser pedidos (o que é uma boa ideia) e os kwargs são dict, não OrderedDict, acho que alguém poderia supor que as chaves dict permanecerão ordenadas na versão futura do Python, apesar da documentação dizer o contrário.
Dmitriy Sintsov
4
@DmitriySintsov Não, não faça essa suposição. Esse foi um problema levantado durante a redação do PEP, que define o recurso de preservação de pedidos **kwargse, **kwargsportanto , o texto usado é diplomático: em uma função, a assinatura agora é garantida como um mapeamento de preservação de pedidos de inserção . Eles usaram o termo mapeamento para não forçar outras implementações a ordenar o ditado (e usar um OrderedDictinternamente) e como uma maneira de sinalizar que isso não deve depender do fato de que dictnão foi ordenado.
Dimitris Fasarakis Hilliard 4/17/17
7
Uma boa explicação de vídeo a partir de Raymond Hettinger
Alex
1
@wazoox, a ordem e a complexidade do hashmap não foram alteradas. A alteração torna o hashmap menor, desperdiçando menos espaço, e o espaço economizado é (geralmente?) Mais do que a matriz auxiliar ocupa. Mais rápido, menor, ordenou - você começa a pegar todos os 3.
John La Rooy

Respostas:

512

Os dicionários são ordenados no Python 3.6+?

Eles são ordenados por inserção [1] . A partir do Python 3.6, para a implementação do Python no CPython, os dicionários lembram a ordem dos itens inseridos . Isso é considerado um detalhe de implementação no Python 3.6 ; você precisa usá- OrderedDictlo se quiser ordenar por inserção garantida em outras implementações do Python (e outro comportamento ordenado [1] ).

A partir do Python 3.7 , isso não é mais um detalhe de implementação e, em vez disso, se torna um recurso de linguagem. De uma mensagem python-dev da GvR :

Faça assim. "Dict mantém ordem de inserção" é a decisão. Obrigado!

Isso significa simplesmente que você pode depender disso . Outras implementações do Python também devem oferecer um dicionário ordenado por inserção, se desejarem ser uma implementação em conformidade do Python 3.7.


Como a 3.6implementação do dicionário Python funciona melhor [2] que a anterior, preservando a ordem dos elementos?

Essencialmente, mantendo duas matrizes .

  • A primeira matriz,, dk_entriescontém as entradas ( do tipoPyDictKeyEntry ) para o dicionário na ordem em que foram inseridas. A ordem de preservação é alcançada por ser uma matriz apenas de acréscimo em que novos itens são sempre inseridos no final (ordem de inserção).

  • O segundo, dk_indicescontém os índices para a dk_entriesmatriz (ou seja, valores que indicam a posição da entrada correspondente em dk_entries). Essa matriz atua como a tabela de hash. Quando uma chave é hash, ela leva a um dos índices armazenados dk_indicese a entrada correspondente é buscada pela indexação dk_entries. Como apenas os índices são mantidos, o tipo dessa matriz depende do tamanho geral do dicionário (variando de tipo int8_t( 1byte) a int32_t/ int64_t( 4/ 8bytes) em compilações 32/ 64bit)

Na implementação anterior, uma matriz esparsa de tipo PyDictKeyEntrye tamanho dk_sizeprecisava ser alocada; infelizmente, isso também resultou em muito espaço vazio, uma vez que não foi permitido que essa matriz estivesse mais do que 2/3 * dk_sizecheia por motivos de desempenho . (e o espaço vazio ainda tinha PyDictKeyEntrytamanho!).

Não é o caso agora, pois apenas as entradas necessárias são armazenadas (aquelas que foram inseridas) e uma matriz esparsa do tipo intX_t( Xdependendo do tamanho do ditado) 2/3 * dk_sizeé mantida cheia. O espaço vazio foi alterado de tipo PyDictKeyEntrypara intX_t.

Portanto, obviamente, criar uma matriz esparsa do tipo PyDictKeyEntryexige muito mais memória do que uma matriz esparsa para armazenar ints.

Você pode ver a conversa completa no Python-Dev sobre esse recurso, se estiver interessado, é uma boa leitura.


Na proposta original feita por Raymond Hettinger , pode-se ver uma visualização das estruturas de dados utilizadas, que captura a essência da ideia.

Por exemplo, o dicionário:

d = {'timmy': 'red', 'barry': 'green', 'guido': 'blue'}

está atualmente armazenado como [keyhash, key, value]:

entries = [['--', '--', '--'],
           [-8522787127447073495, 'barry', 'green'],
           ['--', '--', '--'],
           ['--', '--', '--'],
           ['--', '--', '--'],
           [-9092791511155847987, 'timmy', 'red'],
           ['--', '--', '--'],
           [-6480567542315338377, 'guido', 'blue']]

Em vez disso, os dados devem ser organizados da seguinte maneira:

indices =  [None, 1, None, None, None, 0, None, 2]
entries =  [[-9092791511155847987, 'timmy', 'red'],
            [-8522787127447073495, 'barry', 'green'],
            [-6480567542315338377, 'guido', 'blue']]

Como você pode ver visualmente agora, na proposta original, muito espaço está essencialmente vazio para reduzir colisões e tornar as pesquisas mais rápidas. Com a nova abordagem, você reduz a memória necessária movendo a dispersão onde realmente é necessária nos índices.


[1]: Eu digo "inserção ordenada" e não "ordenada", pois, com a existência de OrderedDict, "ordenada" sugere um comportamento adicional que o dictobjeto não fornece . OrderedDicts são reversíveis, fornecem métodos sensíveis à ordem e, principalmente, fornecem testes de igualdade sensíveis à ordem ( ==, !=). dicts atualmente não oferecem nenhum desses comportamentos / métodos.


[2]: As novas implementações de dicionário têm melhor desempenho em termos de memória ao serem projetadas de forma mais compacta; esse é o principal benefício aqui. Em termos de velocidade, a diferença não é tão drástica, há lugares em que o novo ditado pode introduzir pequenas regressões ( pesquisas de teclas, por exemplo ), enquanto em outros (iteração e redimensionamento vêm à mente) um aumento de desempenho deve estar presente.

No geral, o desempenho do dicionário, especialmente em situações da vida real, melhora devido à compacidade introduzida.

Dimitris Fasarakis Hilliard
fonte
15
Então, o que acontece quando um item é removido? a entrieslista é redimensionada? ou é mantido um espaço em branco? ou é comprimido de tempos em tempos?
Njzk2 11/11
18
@ njzk2 Quando um item é removido, o índice correspondente é substituído por DKIX_DUMMYum valor de -2e a entrada na entrymatriz é substituída porNULL , quando a inserção é realizada, os novos valores são anexados à matriz de entradas. Ainda não foi possível discernir, mas, com certeza, quando os índices 2/3estiverem acima do limite, o redimensionamento será realizado. Isso pode levar a um encolhimento, em vez de um aumento, se DUMMYexistirem muitas entradas.
Dimitris Fasarakis Hilliard 11/10
3
@ Chris_Rands Não, a única regressão real que eu já vi é no rastreador em uma mensagem de Victor . Além dessa marca de microbench, não vi nenhum outro problema / mensagem indicando uma séria diferença de velocidade nas cargas de trabalho da vida real. Há lugares em que o novo ditado pode introduzir pequenas regressões (pesquisas de teclas, por exemplo), enquanto em outros (iteração e redimensionamento vêm à mente) um aumento de desempenho estaria presente.
Dimitris Fasarakis Hilliard
3
Correção na parte de redimensionamento : os dicionários não são redimensionados quando você exclui itens, eles recalculam quando você insere novamente. Portanto, se um ditado for criado com d = {i:i for i in range(100)}você e .poptodos os itens sem inserção, o tamanho não será alterado. Quando você adiciona novamente, d[1] = 1o tamanho apropriado é calculado e o ditado é redimensionado.
Dimitris Fasarakis Hilliard 2/17/17
6
@Chris_Rands Tenho certeza que está ficando. O problema é que, e a razão pela qual mudei minha resposta para remover afirmações gerais sobre ' dictser ordenado', dictnão são ordenados no sentido em que OrderedDictsão. A questão notável é a igualdade. dicts têm ordem insensível ==, OrderedDicts têm ordem sensível. Despejar se OrderedDictmudar dictspara agora ter comparações sensíveis à ordem pode levar a muitas falhas no código antigo. Eu estou supondo que a única coisa que pode mudar sobre OrderedDicts é sua implementação.
Dimitris Fasarakis Hilliard
67

Abaixo está respondendo a primeira pergunta original:

Devo usar dictou OrderedDictno Python 3.6?

Eu acho que essa frase da documentação é suficiente para responder sua pergunta

O aspecto de preservação de pedidos dessa nova implementação é considerado um detalhe da implementação e não deve ser considerado

dictnão é explicitamente destinado a ser uma coleção ordenada; portanto, se você deseja manter a consistência e não confiar no efeito colateral da nova implementação, deve manter-se OrderedDict.

Faça seu código à prova do futuro :)

Há um debate sobre isso aqui .

EDIT: Python 3.7 manterá isso como um recurso, consulte

Maresh
fonte
1
Parece que se eles não quiseram que fosse um recurso real, mas apenas um detalhe de implementação, eles não deveriam colocá-lo na documentação.
Xji #
3
Não tenho certeza sobre sua advertência de edição; uma vez que a garantia se aplica somente para Python 3.7, eu assumo o conselho para Python 3.6 é inalterada, ou seja dicts são ordenados em CPython, mas não conte com isso
Chris_Rands
25

Atualização: Guido van Rossum anunciou na lista de discussão que, a partir do Python 3.7 dicts em todas as implementações do Python, deve preservar a ordem de inserção.

fjsj
fonte
2
Agora que a chave de pedidos é o padrão oficial, qual é o objetivo do OrderedDict? Ou agora é redundante?
Jonny Waffles
2
Eu acho que OrderedDict não será redundante porque ele tem o move_to_endmétodo e sua igualdade é sensível à ordem: docs.python.org/3/library/… . Veja a nota na resposta de Jim Fasarakis Hilliard.
fjsj
@JonnyWaffles ver a resposta de Jim e este Q & A stackoverflow.com/questions/50872498/...
Chris_Rands
3
Se você quer que seu código seja executado o mesmo em 2.7 e 3.6 / 3.7 +, você precisa usar OrderedDict
boatcoder
3
Provavelmente haverá um "UnorderedDict" em breve para pessoas que gostam de preocupar seus dicts por razões de segurança; p
ZF007
9

Eu queria acrescentar à discussão acima, mas não tenho reputação de comentar.

O Python 3.8 ainda não foi lançado, mas incluirá a reversed()função nos dicionários (removendo outra diferença de OrderedDict.

Dict e dictviews agora são iteráveis ​​na ordem de inserção reversa usando reversed (). (Contribuição de Rémi Lapeyre no bpo-33462.) Veja o que há de novo no python 3.8

Eu não vejo nenhuma menção ao operador de igualdade ou outros recursos, OrderedDictportanto eles ainda não são totalmente iguais.

rkengler
fonte