2.10. Conjuntos

Um conjunto é 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 importa a pertença e a eliminação de duplicados, e a ordem não interessa.

2.10.1. Criar um conjunto

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

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

As chavetas parecem um literal de dict; {} por si só é um dicionário 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 forma padrão de eliminar duplicados de uma sequência:

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

Resultado:

{1, 2, 3, 4}

A ordem de impressão pode variar – os conjuntos não garantem iterar em nenhuma ordem particular.

2.10.2. Conjunto vs dicionário

Os conjuntos e os dicionários armazenam ambos itens únicos numa tabela de hash. A diferença está no que cada item carrega consigo:

  • Um dict armazena pares chave-valor. Procurar uma chave devolve o seu valor.

  • Um set armazena apenas os itens. Procurar um item diz-lhe se está presente.

A escolha entre os dois prende-se com saber se o valor ao lado de cada item tem significado:

  • Opte por um conjunto quando nenhum valor pertence a cada item – apenas quer saber se o item está presente, ou está a combinar grupos de itens únicos com união / interseção.

  • Opte por um dicionário quando cada item está emparelhado com dados que a consulta deve devolver – um mapa de configuração, uma cache, um contador indexado por nome.

Os dois tipos partilham muita sintaxe de superfície, que é de onde vem grande parte da confusão. As diferenças num bloco:

conjunto

dicionário

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 pertença

x in s

k in d (apenas chaves)

obter 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 é o ponto que vale a pena realçar:

  • Chavetas com itens{1, 2, 3} – são um literal de conjunto; chavetas com pares chave-valor{"a": 1} – são um literal de dicionário. O interpretador distingue-os pelo que está no interior.

  • Chavetas sem nada{} – são um dicionário vazio, não um conjunto vazio. Os dicionários vieram primeiro; o literal vazio pertence-lhes. Um conjunto vazio não tem nenhum literal com chavetas e tem de ser escrito como set().

Um padrão comum, quando apenas as chaves de um dicionário são alguma vez lidas, é mudar para um conjunto – torna a intenção óbvia e elimina da memória os valores não utilizados.

2.10.3. Adicionar e remover

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

Resultado:

{1, 3, 4}

2.10.4. Pertença

O operador in testa a pertença. Num conjunto é aproximadamente de tempo constante independentemente do tamanho – que é a principal razão para escolher um conjunto em vez de uma list quando só se precisa de perguntar «este valor está aqui»:

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

Uma list com o mesmo conteúdo percorreria desde o início de cada vez, o que está bem para dez itens mas é lento para dez mil.

2.10.5. Operações sobre conjuntos

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

  • a | b ou a.union(b) – tudo o que está em qualquer dos conjuntos.

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

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

  • a ^ b ou a.symmetric_difference(b) – num 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)

Resultado:

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

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

2.10.6. O que pode estar num conjunto

Os elementos de um conjunto têm de ser hasháveis – a mesma restrição que as chaves de dict. int, float, str, bool, bytes e tuple (quando o seu conteúdo é ele próprio hashável) funcionam todos. list e dict não; tentar adicionar um deles levanta TypeError.

2.10.7. frozenset

Um set regular é mutável: cada chamada a add / remove / discard altera o objeto no próprio lugar. Essa mutabilidade impede-o de ser hashável, pelo que um conjunto não pode ser usado como chave de dict nem como membro de outro conjunto.

frozenset é a contraparte imutável. Tem as mesmas consultas e operadores (in, |, &, -, ^) que set, mas sem add / remove nem métodos que mutam. Como nada pode alguma vez alterar o seu conteúdo, o hash de um frozenset está bem definido – pelo que é hashável:

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

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

print(palettes[primary])

Resultado:

RGB

Construa um frozenset a partir de qualquer iterável – frozenset() para o caso vazio, frozenset(some_set) para tirar uma fotografia 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 o usar:

  • Usar como chave de dicionário ou membro de conjunto. Em qualquer lugar em que um único valor não consegue capturar o que se precisa, um frozenset de valores consegue – «o conjunto de funcionalidades suportadas por este controlador», «o conjunto de pinos que este perfil utiliza».

  • Fixar uma constante. Um frozenset de nomes permitidos ao nível do módulo não pode ser acidentalmente mutado por quem o chama; um set regular pode. Prefira frozenset para tudo o que se destina a ser apenas de leitura após a construção.