Python não faz promessas sobre quando (se alguma vez) esse loop terminará. Modificar um conjunto durante a iteração pode levar a elementos ignorados, elementos repetidos e outras estranhezas. Nunca confie em tal comportamento.
Tudo o que estou prestes a dizer são detalhes da implementação, sujeitos a alterações sem aviso prévio. Se você escrever um programa que se baseia em nada disso, seu programa poderá ser interrompido por qualquer combinação de implementação e versão do Python que não seja o CPython 3.8.2.
A breve explicação de por que o loop termina em 16 é que 16 é o primeiro elemento que é colocado em um índice de tabela de hash mais baixo que o elemento anterior. A explicação completa está abaixo.
A tabela de hash interna de um conjunto Python sempre tem uma capacidade de tamanho 2. Para uma tabela de tamanho 2 ^ n, se nenhuma colisão ocorrer, os elementos serão armazenados na posição na tabela de hash correspondente aos n bits menos significativos de seu hash. Você pode ver isso implementado em set_add_entry
:
mask = so->mask;
i = (size_t)hash & mask;
entry = &so->table[i];
if (entry->key == NULL)
goto found_unused;
A maioria das pequenas entradas Python se mistura; particularmente, todos os ints do seu teste se misturam. Você pode ver isso implementado em long_hash
. Como o seu conjunto nunca contém dois elementos com bits baixos iguais em seus hashes, nenhuma colisão ocorre.
Um iterador de conjunto Python controla sua posição em um conjunto com um índice inteiro simples na tabela de hash interna do conjunto. Quando o próximo elemento é solicitado, o iterador procura uma entrada preenchida na tabela de hash iniciando nesse índice, depois define seu índice armazenado imediatamente após a entrada encontrada e retorna o elemento da entrada. Você pode ver isso em setiter_iternext
:
while (i <= mask && (entry[i].key == NULL || entry[i].key == dummy))
i++;
si->si_pos = i+1;
if (i > mask)
goto fail;
si->len--;
key = entry[i].key;
Py_INCREF(key);
return key;
Seu conjunto começa inicialmente com uma tabela de tamanho 8 e um ponteiro para um 0
objeto int no índice 0 na tabela de valores. O iterador também é posicionado no índice 0. À medida que você itera, os elementos são adicionados à tabela de hash, cada um no índice seguinte, porque é onde o hash diz para colocá-los, e esse é sempre o próximo índice que o iterador analisa. Os elementos removidos têm um marcador fictício armazenado em sua posição antiga, para fins de resolução de colisão. Você pode ver isso implementado em set_discard_entry
:
entry = set_lookkey(so, key, hash);
if (entry == NULL)
return -1;
if (entry->key == NULL)
return DISCARD_NOTFOUND;
old_key = entry->key;
entry->key = dummy;
entry->hash = -1;
so->used--;
Py_DECREF(old_key);
return DISCARD_FOUND;
Quando 4
é adicionado ao conjunto, o número de elementos e manequins no conjunto se torna alto o suficiente para set_add_entry
disparar uma reconstrução da tabela de hash, chamando set_table_resize
:
if ((size_t)so->fill*5 < mask*3)
return 0;
return set_table_resize(so, so->used>50000 ? so->used*2 : so->used*4);
so->used
é o número de entradas não simuladas preenchidas na tabela de hash, que é 2, e set_table_resize
recebe 8 como seu segundo argumento. Com base nisso, set_table_resize
decide que o novo tamanho da tabela de hash deve ser 16:
/* Find the smallest table size > minused. */
/* XXX speed-up with intrinsics */
size_t newsize = PySet_MINSIZE;
while (newsize <= (size_t)minused) {
newsize <<= 1; // The largest possible value is PY_SSIZE_T_MAX + 1.
}
Ele recria a tabela de hash com tamanho 16. Todos os elementos ainda terminam nos índices antigos na nova tabela de hash, pois não tinham bits altos definidos em seus hashes.
À medida que o loop continua, os elementos continuam sendo colocados no próximo índice que o iterador terá. Outra reconstrução da tabela de hash é acionada, mas o novo tamanho ainda é 16.
O padrão é interrompido quando o loop adiciona 16 como um elemento. Não há índice 16 para colocar o novo elemento em. Os 4 bits mais baixos de 16 são 0000, colocando 16 no índice 0. O índice armazenado do iterador é 16 neste momento e, quando o loop solicita o próximo elemento do iterador, o iterador vê que passou do final do tabela de hash.
O iterador termina o loop neste ponto, deixando apenas 16
no conjunto.
s.add(i+1)
(e possivelmente a chamada paras.remove(i)
) pode alterar a ordem de iteração do conjunto, afetando o que o iterador de conjunto que o loop for criado verá em seguida. Não modifique um objeto enquanto você tiver um iterador ativo.