Por que os conjuntos Python não preservam a ordem de inserção?

12

Fiquei surpreso ao descobrir recentemente que, embora os dict tenham a garantia de preservar a ordem de inserção no Python 3.7+, os conjuntos não são:

>>> d = {'a': 1, 'b': 2, 'c': 3}
>>> d
{'a': 1, 'b': 2, 'c': 3}
>>> d['d'] = 4
>>> d
{'a': 1, 'b': 2, 'c': 3, 'd': 4}
>>> s = {'a', 'b', 'c'}
>>> s
{'b', 'a', 'c'}
>>> s.add('d')
>>> s
{'d', 'b', 'a', 'c'}

Qual é a justificativa para essa diferença? As mesmas melhorias de eficiência que levaram a equipe do Python a alterar a implementação do dict também não se aplicam aos conjuntos?

Não estou procurando ponteiros para implementações de conjuntos ordenados ou maneiras de usar dicts como substitutos de conjuntos. Só estou me perguntando por que a equipe do Python não fez os conjuntos internos preservarem a ordem ao mesmo tempo em que os ditavam.

Bart Robinson
fonte
11
Isso responde sua pergunta? O Python tem um conjunto ordenado?
Mihai Chelaru 24/04
11
Não, eu entendo que o Python não possui um conjunto ordenado incorporado. Estou apenas me perguntando por que esse é o caso, já que os dicts agora estão ordenados.
Bart Robinson
4
Os padrões de uso são diferentes, portanto, eles são otimizados para diferentes casos de uso. É um equívoco comum que os conjuntos sejam apenas dictos com valores nulos no CPython, isso é totalmente incorreto: as implementações são diferentes. Se sua pergunta não for fechada, posso postar uma resposta detalhada.
wim 24/04
11
"Os padrões de uso são diferentes, portanto, eles são otimizados para diferentes casos de uso." Uma boa resposta para a pergunta seria elaborada sobre isso, eu acho. A questão é sobre o que torna as duas abordagens diferentes ideais para os casos de uso correspondentes.
Karl Knechtel
Observe que o PyPy usa a mesma ordem para ambos dicte setdesde o 2.7.
MisterMiyagi 28/04

Respostas:

10

Conjuntos e dicts são otimizados para diferentes casos de uso. O uso principal de um conjunto é o teste rápido de associação, que é independente da ordem. Para ditados, o custo da pesquisa é a operação mais crítica e é mais provável que a chave esteja presente. Com conjuntos, a presença ou ausência de um elemento não é conhecida antecipadamente e, portanto, a implementação do conjunto precisa otimizar para o caso encontrado e o não encontrado. Além disso, algumas otimizações para operações comuns de conjuntos, como união e interseção, dificultam manter a ordem dos conjuntos sem prejudicar o desempenho.

Embora ambas as estruturas de dados sejam baseadas em hash, é um equívoco comum que os conjuntos sejam implementados apenas como ditados com valores nulos. Mesmo antes da implementação compacta do dict no CPython 3.6, as implementações set e dict já diferiam significativamente, com pouca reutilização de código. Por exemplo, os dictos usam sondagem aleatória, mas os conjuntos usam uma combinação de sondagem linear e endereçamento aberto, para melhorar a localidade do cache. O probe linear inicial (padrão 9 etapas no CPython) verificará uma série de pares de chave / hash adjacentes, melhorando o desempenho, reduzindo o custo do manuseio de colisão de hash - o acesso consecutivo à memória é mais barato que os probes dispersos.

Seria possível, em teoria, alterar a implementação do conjunto do CPython para ser semelhante ao ditado compacto, mas na prática existem desvantagens, e os principais desenvolvedores notáveis ​​se opuseram a fazer essa alteração.

Os conjuntos permanecem desordenados. (Por quê? Os padrões de uso são diferentes. Além disso, implementação diferente.)

- Guido van Rossum

Os conjuntos usam um algoritmo diferente que não é tão alterável para reter a ordem de inserção. As operações de conjunto a conjunto perdem sua flexibilidade e otimizações, se necessário. A matemática dos conjuntos é definida em termos de conjuntos não ordenados. Em resumo, a ordenação de conjuntos não está no futuro imediato.

- Raymond Hettinger

Uma discussão detalhada sobre compactar conjuntos para 3.7 e respostas sobre por que foi decidido não pode ser encontrada nas listas de discussão python-dev.

Em resumo, os pontos principais são que os padrões de uso são diferentes (dados de ordem de inserção como ** kwargs são úteis , menos para conjuntos), a economia de espaço para conjuntos de compactação é menos significativa (porque há apenas matriz de hash e chave) densify, ao contrário de chaves, hashes e valores), e a otimização de sondagem linear acima mencionada em conjuntos é incompatível com uma implementação compacta.

Vou reproduzir o post de Raymond abaixo, que aborda os pontos mais importantes.

Em 14 de setembro de 2016, às 15:50, Eric Snow escreveu:

Então, eu vou fazer o mesmo para conjuntos.

A menos que eu tenha entendido errado, Raymond se opôs a fazer uma mudança semelhante para definir.

Está certo. Aqui estão algumas reflexões sobre o assunto antes de as pessoas começarem a correr soltas.

  • Para o ditado compacto, a economia de espaço foi uma vitória líquida, com o espaço adicional consumido pelos índices e a alocação geral para as matrizes de chave / valor / hash sendo mais do que compensada pela densidade aprimorada das matrizes de chave / valor / hash. No entanto, para os conjuntos, a rede era muito menos favorável, porque ainda precisamos dos índices e da alocação geral, mas só podemos compensar o custo do espaço densificando apenas duas das três matrizes. Em outras palavras, a compactação faz mais sentido quando você desperdiça espaço para chaves, valores e hashes. Se você perder um desses três, ele deixará de ser atraente.

  • O padrão de uso para conjuntos é diferente dos dict. O primeiro tem mais pesquisas de acerto ou erro. O último tende a ter menos pesquisas de chave ausentes. Além disso, algumas das otimizações para as operações configuradas tornam difícil manter a ordem dos conjuntos sem afetar o desempenho.

  • Eu segui um caminho alternativo para melhorar o desempenho do conjunto. Em vez de compactar (que não ganhava muito espaço e incorria no custo de uma indireção adicional), adicionei a análise linear para reduzir o custo de colisões e melhorar o desempenho do cache. Essa melhoria é incompatível com a abordagem de compactação que eu advoguei para dicionários.

  • Por enquanto, o efeito colateral da encomenda nos dicionários não é garantido, portanto é prematuro começar a insistir que os conjuntos também sejam encomendados. Os documentos já apontam para uma receita para a criação de um OrderedSet ( https://code.activestate.com/recipes/576694/ ), mas parece que a aceitação foi quase zero. Além disso, agora que Eric Snow nos deu um rápido OrderedDict, é mais fácil do que nunca criar um OrderedSet a partir de MutableSet e OrderedDict, mas, novamente, eu não observei nenhum interesse real, porque as análises típicas de dados conjunto a conjunto realmente não precisa ou se preocupa com a encomenda. Da mesma forma, o uso principal de testes rápidos de associação é independente de ordem.

  • Dito isso, acho que há espaço para adicionar implementações de conjuntos alternativos ao PyPI. Em particular, existem alguns casos especiais interessantes para dados solicitáveis, nos quais as operações de configuração podem ser aceleradas através da comparação de intervalos de chaves inteiros (consulte https://code.activestate.com/recipes/230113-implementation-of- define-usando-ordenado-listas para um ponto de partida). IIRC, o PyPI já possui código para filtros de flor tipo set e hash de cuco.

  • Entendo que é empolgante ter um grande bloco de código aceito no núcleo do Python, mas que não deve abrir portas para envolver-se em reescritas mais importantes de outros tipos de dados, a menos que tenhamos certeza de que isso se justifica.

- Raymond Hettinger

No [Python-Dev], o dict do Python 3.6 se torna compacto e obtém uma versão privada; e as palavras-chave serão ordenadas , setembro de 2016.

wim
fonte
2

Discussões

Sua pergunta é pertinente e já foi amplamente discutida em python-devs há pouco tempo. R. Hettinger compartilhou uma lista de razões nesse segmento . O estado do problema parece aberto agora, logo após esta resposta detalhada de T. Peters.

Em suma, a implementação de ditados modernos que preserva a ordem de inserção é única e não é considerada adequada para os conjuntos. Em particular, dicts são usados ​​em todos os lugares para executar o Python (por exemplo, __dict__nos namespaces de objetos). Uma grande motivação por trás do ditado moderno era reduzir o tamanho, tornando o Python mais eficiente em termos de memória. Por outro lado, os conjuntos são menos prevalentes que os ditados no núcleo do Python e, assim, dissuadem essa refatoração. Veja também a palestra de R. Hettinger sobre a implementação de ditados modernos.


Perspectivas

A natureza não ordenada dos conjuntos no Python é paralela ao comportamento dos conjuntos matemáticos . O pedido não é garantido.

O conceito matemático correspondente é desordenado e seria estranho impor a ordem - R. Hettinger

Se qualquer ordem fosse introduzida em conjuntos no Python, esse comportamento obedeceria a uma estrutura matemática completamente separada, a saber, um conjunto ordenado (ou Oset). Osets desempenham um papel separado na matemática, particularmente na combinatória. Uma aplicação prática de Osets é observada na troca de sinos .

Ter conjuntos não ordenados é consistente com uma estrutura de dados muito genérica e onipresente que revela a maioria das matemáticas modernas, ou seja, a Teoria dos Conjuntos . Eu submeto, é bom ter conjuntos não ordenados em Python.

Veja também postagens relacionadas que se expandem sobre este tópico:

pylang
fonte