Os conjuntos não são ordenados, ou melhor, sua ordem é um detalhe de implementação. Estou interessado nesse detalhe. E vi um caso que me surpreendeu:
print({2, 3, 10})
x = 2
print({x, 3, 10})
Saída ( tente fazer isso online! ):
{3, 10, 2}
{10, 2, 3}
Apesar de elementos idênticos escritos em ordem idêntica, eles são ordenados de forma diferente. Como isso acontece, e isso é feito intencionalmente por algum motivo, por exemplo, para otimizar a velocidade de pesquisa?
Meu sys.version
e sys.implementation
:
3.13.0 (main, Nov 9 2024, 10:04:25) [GCC 14.2.1 20240910]
namespace(name='cpython', cache_tag='cpython-313', version=sys.version_info(major=3, minor=13, micro=0, releaselevel='final', serial=0), hexversion=51183856, _multiarch='x86_64-linux-gnu')
É uma função de algumas coisas:
Colisões de hash bucket - Para o menor
set
tamanho,8
(detalhe de implementação do CPython),2
e10
colidem em seus códigos hash de corte (que, novamente detalhe de implementação, são2
e10
; mod8
, ambos são 2). Qualquer um que for inserido primeiro "ganha" e obtém o índice de bucket 2, o outro é movido pela operação de sondagem. A operação de sondagem (novamente, detalhe de implementação do CPython) verifica inicialmente os buckets adjacentes linearmente para um bucket vazio (porque geralmente encontra um, e uma melhor localidade de memória melhora o desempenho do cache), e somente se não encontrar um, ele inicia o algoritmo de salto aleatório para encontrar um bucket vazio (ele não pode fazer sondagem linear pura, porque isso tornaria muito fácil disparar casos patológicos que alteramset
as operações de caso médio amortizadoO(1)
paraO(n)
).Otimizações em tempo de compilação: No CPython moderno,
set
s elist
s de literais constantes que têm pelo menos três elementos de comprimento são construídos em tempo de compilação como um contêiner imutável (frozenset
etuple
respectivamente). Em tempo de execução, ele constrói umset
/ vaziolist
, entãoupdate
s/extend
s com o contêiner imutável, em vez de executar cargas individuais eadd
s/append
s para cada elemento. Isso significa que quando você constrói coms = {2, 3, 10}
, você está na verdade fazendos = set()
,s.update(frozenset({2, 3, 10}))
(com ofrozenset
puxado do cache), enquantos = {x, 3, 10}
está construindo carregandox
,3
e10
na pilha, então construindo oset
como uma única operação.Os dois significam que você está realmente construindo de forma diferente;
{x, 3, 10}
está inserindo2
, então3
, então10
, então os buckets2
e3
são preenchidos, e10
são realocados (a estratégia de sondagem claramente o coloca em bucket0
ou1
, antes de bucket2
). Quando você faz{2, 3, 10}
, em tempo de compilação ele está criando umfrozenset({3, 10, 2})
, então em tempo de execução, ele está criando o vazioset
, então atualizando-o iterando quefrozenset
, que já reordenou os elementos , então agora eles não estão mais sendo adicionados em2
,3
,10
ordem, e a corrida por buckets "preferidos" é vencida por elementos diferentes.Em resumo, o comportamento de
{x, 3, 10}
é equivalente a:o que previsivelmente dá aos baldes 2 e 3
2
e3
eles mesmos,10
sendo deslocados para o balde 0 ou 1.Por outro lado,
{2, 3, 10}
constrói umfrozenset({3, 10, 2})
(nota: está nessa ordem após a conversão parafrozenset
; se você tentasse executar essa linha exata eprint
ele, você veria uma ordem diferente), entãoupdate
s um vazioset
com ele. Há um caminho de código otimizado para preencher um vazioset
de outroset
/frozenset
que apenas copia o conteúdo diretamente (em vez de iterar e inserir aos poucos), então a{3, 10, 2}
ordem no cachedfrozenset
é preservada em cadaset
criado a partir dele, o mesmo que se você executasse:mas com melhor desempenho (porque
frozenset
é criado uma vez em tempo de compilação e carregado de forma barata para cada novoset
a ser inicializado).