2.10. Conjuntos

Um conjunto (set) é uma coleção não ordenada de itens únicos. Adicionar um valor que já está presente não tem efeito; a iteração produz cada valor exatamente uma vez. Os conjuntos são a ferramenta certa quando pertinência e deduplicação importam, e a ordenação não.

2.10.1. Criando um conjunto

Use chaves para um conjunto não vazio, ou set() para um vazio:

colours = {"red", "green", "blue"}
empty = set()

As chaves se parecem com um literal de dict; {} por si só é um dict vazio, não um conjunto vazio – um dos acidentes históricos do Python. Use set() para o caso vazio.

set() também constrói um conjunto a partir de qualquer iterável, que é a maneira padrão de eliminar duplicatas de uma sequência:

nums = [1, 2, 2, 3, 1, 4]
unique = set(nums)
print(unique)

Saída:

{1, 2, 3, 4}

A ordem de impressão pode variar – conjuntos não prometem iterar em nenhuma ordem específica.

2.10.2. Set vs dict

Tanto conjuntos quanto dicts armazenam itens únicos em uma tabela hash. O que cada item carrega consigo é a diferença:

  • Um dict armazena pares chave-valor. Consultar uma chave retorna seu valor.

  • Um set armazena apenas os itens. Consultar um item informa se ele está lá.

A escolha entre os dois é sobre se o valor ao lado de cada item significa algo:

  • Recorra a um set quando nenhum valor pertence ao lado de cada item – você só se importa se o item está presente, ou está combinando grupos de itens únicos com união / interseção.

  • Recorra a um dict quando cada item está emparelhado com dados que a consulta deve recuperar – um mapa de configuração, um cache, um contador indexado por nome.

Os dois tipos compartilham muita sintaxe superficial, que é de onde vem a maior parte da confusão. As diferenças em um único bloco:

set

dict

contém

itens únicos

chaves únicas, cada uma com um valor

literal preenchido

{1, 2, 3}

{"a": 1, "b": 2}

literal vazio

set()

{}

teste de pertinência

x in s

k in d (apenas chaves)

buscar um valor

n/d

d[k]

adicionar um item

s.add(x)

d[k] = v

iterar

produz itens

produz chaves (use d.items() para pares)

A assimetria entre os literais preenchido e vazio é a pegadinha que vale a pena destacar:

  • Chaves com itens dentro{1, 2, 3} – são um literal de conjunto; chaves com pares chave-valor{"a": 1} – são um literal de dict. O parser os distingue pelo que há dentro.

  • Chaves com nada dentro{} – são um dict vazio, não um conjunto vazio. Os dicts vieram primeiro; o literal vazio pertence a eles. Um conjunto vazio não tem literal com chaves e deve ser escrito como set().

Um padrão comum quando apenas as chaves de um dict são lidas é trocar por um conjunto – isso torna a intenção óbvia e elimina os valores não utilizados da memória.

2.10.3. Adicionando e removendo

s = {1, 2, 3}
s.add(4)
s.discard(99)            # silent: 99 not in s
s.remove(2)
print(s)

Saída:

{1, 3, 4}

2.10.4. Pertinência

O operador in testa a pertinência. Em um conjunto, ele é aproximadamente de tempo constante independentemente do tamanho – o que é a principal razão para escolher um conjunto em vez de uma list quando você só precisa perguntar “este valor está aí”:

if "red" in colours:
    print("colour is allowed")

Uma list com o mesmo conteúdo varreria a partir do início a cada vez, o que é aceitável para dez itens mas lento para dez mil.

2.10.5. Operações de conjunto

Dois conjuntos podem ser combinados com as operações matemáticas usuais. Cada uma tem tanto uma forma de operador quanto uma forma de método:

  • a | b ou a.union(b) – tudo em qualquer um dos conjuntos.

  • a & b ou a.intersection(b) – apenas o que aparece em ambos.

  • a - b ou a.difference(b) – o que está em a mas não em b.

  • a ^ b ou a.symmetric_difference(b) – o que está em um mas não em ambos.

a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
print(a | b)
print(a & b)
print(a - b)
print(a ^ b)

Saída:

{1, 2, 3, 4, 5, 6}
{3, 4}
{1, 2}
{1, 2, 5, 6}

As formas de operador são somente leitura; as formas de método aceitam qualquer iterável à direita, não apenas outro conjunto (a.union([5, 6])). Escolha a que ler melhor no contexto.

2.10.6. O que pode entrar em um conjunto

Os elementos de um conjunto devem ser hashable – a mesma restrição das chaves de dict. int, float, str, bool, bytes e tuple (quando seu conteúdo é, por sua vez, hashable) todos funcionam. list e dict não; tentar adicionar um deles gera TypeError.

2.10.7. frozenset

Um set comum é mutável: toda chamada a add / remove / discard altera o objeto no próprio local. Essa mutabilidade o desqualifica de ser hashable, então um conjunto não pode ser usado como chave de dict nem como membro de outro conjunto.

frozenset é o equivalente imutável. Ele tem as mesmas consultas e operadores (in, |, &, -, ^) que set, mas não tem add / remove nem métodos que mutam. Como nada pode jamais alterar seu conteúdo, o hash de um frozenset é bem definido – então ele é hashable:

primary = frozenset({"red", "green", "blue"})
secondary = frozenset({"yellow", "purple", "orange"})

palettes = {
    primary: "RGB",
    secondary: "mixed",
}

print(palettes[primary])

Saída:

RGB

Construa um frozenset a partir de qualquer iterável – frozenset() para o caso vazio, frozenset(some_set) para tirar um snapshot imutável de um conjunto existente:

snapshot = frozenset(s)         # immutable copy of s
s.add("new")                    # snapshot does not change

Duas razões comuns para recorrer a ele:

  • Uso como chave de dict ou membro de conjunto. Em qualquer lugar em que um único valor não consegue capturar o que você precisa, um frozenset de valores consegue – “o conjunto de características suportadas por este driver”, “o conjunto de pinos que este perfil usa”.

  • Travar uma constante. Um frozenset de nomes permitidos em nível de módulo não pode ser mutado acidentalmente por um chamador; um set comum pode. Prefira frozenset para qualquer coisa que deva ser somente leitura após a construção.