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.