Python O Módulo collections

Listas, tuplas, dicionários e conjuntos resolvem quase tudo. Mas alguns trabalhos aparecem tanto que o Python já traz versões especializadas deles, prontas, no módulo collections ("coleções"): um dicionário que conta sozinho, um que cria a chave que falta, uma tupla com nomes nos campos e uma fila rápida dos dois lados. São quatro ferramentas, e cada uma economiza um punhado de linhas e evita um erro clássico.

Você vai aprender
  • Counter: contar palavras, vendas e letras, e achar os mais comuns com most_common.
  • defaultdict: agrupar e somar sem o KeyError da chave que ainda não existe.
  • namedtuple: uma tupla em que cada campo tem nome (produto.preco em vez de produto[1]).
  • deque: uma fila que entra por um lado e sai pelo outro, e que pode ter tamanho máximo.
  • Quando usar cada uma, e quando um dicionário ou uma lista comuns bastam.
  • Os erros reais: KeyError, can't set attribute, missing 1 required positional argument, pop from an empty deque, first argument must be callable.

A ideia: a caixa de ferramentas da padaria

Na padaria da Ana há uma faca comum, que corta tudo, e algumas facas especiais: a de pão (serrilhada), a de queijo, a espátula de bolo. Dá para cortar pão com a faca comum, mas a de pão faz melhor e mais rápido. O módulo collections é a gaveta das facas especiais:

FerramentaÉ parecida comA especialidadeExemplo da Ana
CounterdicionárioContar quantas vezes cada coisa apareceQual pão vendeu mais hoje?
defaultdictdicionárioCriar sozinho o valor de uma chave novaSeparar os alunos por turno
namedtupletuplaDar nome a cada posiçãoFicha de produto: nome, preço, estoque
dequelistaPôr e tirar rápido nas duas pontasA fila do caixa

Todas se importam do mesmo jeito (aula Módulos e import): from collections import Counter, ou várias de uma vez, from collections import Counter, deque.


1. Counter: contar coisas

Contar palavras de um texto é um exercício clássico. Com um dicionário comum, você precisa testar se a palavra já apareceu antes de somar 1:

texto = "o pão da padaria é o melhor pão do bairro e o café da padaria também"
palavras = texto.split()

contagem = {}
for palavra in palavras:
    if palavra in contagem:
        contagem[palavra] += 1
    else:
        contagem[palavra] = 1

print(contagem)
$ python3 contar_mao.py
{'o': 3, 'pão': 2, 'da': 2, 'padaria': 2, 'é': 1, 'melhor': 1, 'do': 1, 'bairro': 1, 'e': 1, 'café': 1, 'também': 1}

O Counter ("contador") faz o laço inteiro numa linha:

from collections import Counter

texto = "o pão da padaria é o melhor pão do bairro e o café da padaria também"
palavras = texto.split()

contagem = Counter(palavras)
print(contagem)
print(contagem["pão"])
print(contagem["bolo"])
print(contagem.most_common(3))
$ python3 contar_counter.py
Counter({'o': 3, 'pão': 2, 'da': 2, 'padaria': 2, 'é': 1, 'melhor': 1, 'do': 1, 'bairro': 1, 'e': 1, 'café': 1, 'também': 1})
2
0
[('o', 3), ('pão', 2), ('da', 2)]
texto.split()
Quebra o texto nos espaços e devolve a lista de palavras (aula Métodos de Texto).
Counter(palavras)
Percorre a lista e conta cada item. O resultado é um dicionário especial: a chave é o item, o valor é quantas vezes ele apareceu.
contagem["pão"]
Lê como num dicionário: 2.
contagem["bolo"]
Uma chave que não existe dá 0, e não KeyError. Faz sentido: "bolo" apareceu zero vezes.
most_common(3)
"Os mais comuns": uma lista de pares (item, contagem), do maior para o menor, só os 3 primeiros. Em caso de empate, vem primeiro quem apareceu primeiro no texto. Sem número, most_common() devolve todos.
Contar palavras de verdade

Num texto real, "Pão" e "pão," (com vírgula) seriam contados como palavras diferentes de "pão". Antes de contar, padronize: texto.lower() para minúsculas e tire a pontuação, por exemplo com as expressões regulares (re.findall(r"\w+", texto.lower())). O projeto Contador de Palavras faz isso passo a passo.

Somando contagens

Um Counter sabe somar com outro, ser atualizado e dizer o total. As vendas de dois dias da padaria:

from collections import Counter

segunda = Counter(["pão", "pão", "leite", "café"])
terca = Counter(["pão", "café", "café", "manteiga"])

print("Segunda:", segunda)
print("Terça:  ", terca)
print("Juntas: ", segunda + terca)
print("Total de itens vendidos na segunda:", segunda.total())

segunda.update(["leite", "leite"])
print("Segunda depois do update:", segunda)
print(Counter("banana"))
$ python3 counter_mais.py
Segunda: Counter({'pão': 2, 'leite': 1, 'café': 1})
Terça:   Counter({'café': 2, 'pão': 1, 'manteiga': 1})
Juntas:  Counter({'pão': 3, 'café': 3, 'leite': 1, 'manteiga': 1})
Total de itens vendidos na segunda: 4
Segunda depois do update: Counter({'leite': 3, 'pão': 2, 'café': 1})
Counter({'a': 3, 'n': 2, 'b': 1})
  • segunda + terca soma item por item: 2 + 1 pães, 1 + 2 cafés.
  • .total() soma todas as contagens (existe desde o Python 3.10; antes, sum(segunda.values())).
  • .update(lista) conta mais itens no mesmo contador: o leite foi de 1 para 3.
  • Counter("banana") conta as letras, porque um texto é uma sequência de letras. Às vezes é o que você quer; às vezes é um erro (veja em Quando dá errado).

Missão: a barraca de frutas anotou cada venda numa lista. Descubra a fruta mais vendida: crie um Counter com a lista e mostre most_common(1).

from collections import Counter

vendidas = ["banana", "maçã", "banana", "uva", "maçã", "banana"]

print(vendidas)
from collections import Counter

vendidas = ["banana", "maçã", "banana", "uva", "maçã", "banana"]

contagem = Counter(vendidas)
print(contagem.most_common(1))

2. defaultdict: a chave que se cria sozinha

A escola quer separar os alunos por turno. Com um dicionário comum, toda vez que aparece um turno novo é preciso criar a lista vazia antes de acrescentar o nome:

alunos = [("Ana", "manhã"), ("Bruno", "tarde"), ("Carla", "manhã"), ("Davi", "noite"), ("Eva", "tarde")]

turmas = {}
for nome, turno in alunos:
    if turno not in turmas:
        turmas[turno] = []
    turmas[turno].append(nome)

print(turmas)
$ python3 agrupar_mao.py
{'manhã': ['Ana', 'Carla'], 'tarde': ['Bruno', 'Eva'], 'noite': ['Davi']}

O defaultdict ("dicionário com valor padrão") faz o if por você: quando você pede uma chave que não existe, ele cria a chave com um valor inicial, e só depois devolve.

from collections import defaultdict

alunos = [("Ana", "manhã"), ("Bruno", "tarde"), ("Carla", "manhã"), ("Davi", "noite"), ("Eva", "tarde")]

turmas = defaultdict(list)
for nome, turno in alunos:
    turmas[turno].append(nome)

print(turmas)
print(turmas["manhã"])
print(dict(turmas))
$ python3 agrupar_dd.py
defaultdict(<class 'list'>, {'manhã': ['Ana', 'Carla'], 'tarde': ['Bruno', 'Eva'], 'noite': ['Davi']})
['Ana', 'Carla']
{'manhã': ['Ana', 'Carla'], 'tarde': ['Bruno', 'Eva'], 'noite': ['Davi']}
defaultdict(list)
O argumento é a fábrica do valor inicial: a função list, sem parênteses. Toda chave nova recebe list(), uma lista vazia.
turmas[turno].append(nome)
Na primeira vez que aparece "manhã", o defaultdict cria turmas["manhã"] = [] e o append põe a Ana. Na segunda, a lista já existe.
print(turmas)
Mostra defaultdict(<class 'list'>, {…}): o tipo, a fábrica e o conteúdo.
dict(turmas)
Converte para um dicionário comum, que imprime mais limpo e não cria mais chaves sozinho.

Somando por chave: defaultdict(int)

A fábrica pode ser qualquer função sem argumentos. Com int, o valor inicial é int(), que é 0: perfeito para somar.

from collections import defaultdict

vendas = [("pão", 12), ("leite", 3), ("pão", 8), ("café", 1), ("leite", 2)]

total = defaultdict(int)
for produto, quantidade in vendas:
    total[produto] += quantidade

print(dict(total))
print(int())
print(list())
$ python3 dd_int.py
{'pão': 20, 'leite': 5, 'café': 1}
0
[]

As duas últimas linhas mostram o que as fábricas produzem: int() dá 0 e list() dá []. (Para contar ocorrências, o Counter é ainda mais direto; o defaultdict(int) brilha quando você soma quantidades, como aqui.)

Só olhar já cria a chave
from collections import defaultdict

turmas = defaultdict(list)
turmas["manhã"].append("Ana")

if turmas["sábado"]:
    print("Tem aula no sábado")

print(dict(turmas))
print("domingo" in turmas)
$ python3 dd_cria.py
{'manhã': ['Ana'], 'sábado': []}
False

O if turmas["sábado"] só queria perguntar, mas criou a chave 'sábado' com uma lista vazia. Para perguntar sem criar, use in (que não cria nada: "domingo" in turmas deu False) ou turmas.get("sábado").

Missão: o programa deveria juntar as notas de cada aluno, mas dá KeyError. Rode e veja. Depois troque o {} por defaultdict(list) (o import já está lá).

from collections import defaultdict

notas = [("Ana", 9.5), ("Bruno", 7.0), ("Ana", 8.0), ("Bruno", 6.5), ("Ana", 10.0)]

por_aluno = {}
for nome, nota in notas:
    por_aluno[nome].append(nota)

for nome, lista in por_aluno.items():
    print(nome, lista)
from collections import defaultdict

notas = [("Ana", 9.5), ("Bruno", 7.0), ("Ana", 8.0), ("Bruno", 6.5), ("Ana", 10.0)]

por_aluno = defaultdict(list)
for nome, nota in notas:
    por_aluno[nome].append(nota)

for nome, lista in por_aluno.items():
    print(nome, lista)

3. namedtuple: tuplas com nome nos campos

Uma tupla como ("pão francês", 0.75, 120) guarda bem uma ficha de produto, mas obriga a lembrar que o preço é a posição 1 e o estoque, a 2. Daqui a um mês, produto[2] não diz nada a ninguém. A namedtuple ("tupla com nomes") resolve:

from collections import namedtuple

Produto = namedtuple("Produto", ["nome", "preco", "estoque"])

pao = Produto("pão francês", 0.75, 120)
leite = Produto(nome="leite", preco=5.49, estoque=30)

print(pao)
print(pao.nome, pao.preco)
print(leite[0], leite[2])
nome, preco, estoque = leite
print(f"{nome}: R$ {preco:.2f} ({estoque} no estoque)")

mais_caro = pao._replace(preco=0.85)
print(mais_caro)
print(pao._asdict())
$ python3 nt.py
Produto(nome='pão francês', preco=0.75, estoque=120)
pão francês 0.75
leite 30
leite: R$ 5.49 (30 no estoque)
Produto(nome='pão francês', preco=0.85, estoque=120)
{'nome': 'pão francês', 'preco': 0.75, 'estoque': 120}
Produto = namedtuple("Produto", ["nome", "preco", "estoque"])
Cria um tipo novo, como uma classe (aula Classes e Objetos). O primeiro argumento é o nome que aparece no print; a lista são os nomes dos campos, na ordem. Por costume, o nome do tipo começa com maiúscula e é igual ao da variável.
Produto("pão francês", 0.75, 120)
Cria uma ficha, pela ordem dos campos. Também vale pelo nome: Produto(nome="leite", preco=5.49, estoque=30).
pao.nome, pao.preco
O acesso pelo nome, que deixa o código legível.
leite[0], leite[2]
Continua sendo uma tupla: índices e desempacotamento (aula Desempacotamento) funcionam.
pao._replace(preco=0.85)
Tuplas não mudam (aula Tuplas). Para "alterar", cria-se uma ficha nova com um campo trocado. O pao original continua com 0.75, como mostra a última linha.
pao._asdict()
Converte num dicionário comum, útil para gravar em JSON. O _ na frente desses métodos não quer dizer "privado" aqui: é só para não brigar com um campo que você chame de replace.
namedtuple ou dataclass?

A dataclass também cria fichas com campos nomeados, e os campos dela podem mudar (pao.preco = 0.85). Use namedtuple quando a ficha não deve mudar e você quer que ela continue funcionando como tupla (índices, desempacotamento, chave de dicionário); use dataclass quando os dados mudam ou a ficha precisa de métodos.


4. deque: a fila dos dois lados

A fila do caixa: quem chega entra no fim, quem é atendido sai do começo. Numa lista, tirar do começo (lista.pop(0)) obriga o Python a empurrar todos os outros itens uma casa para a frente, e numa fila de milhares de pessoas isso pesa. O deque (lê-se "déc", de double-ended queue, "fila de duas pontas") põe e tira nas duas pontas sem empurrar ninguém.

from collections import deque

fila = deque(["Ana", "Bruno"])
fila.append("Carla")
fila.append("Davi")
print(fila)

atendido = fila.popleft()
print("Atendendo:", atendido)
print(fila)

fila.appendleft("Dona Rosa")
print("Prioridade:", fila)
print("Último da fila:", fila[-1])
print(len(fila), "pessoas")
$ python3 fila.py
deque(['Ana', 'Bruno', 'Carla', 'Davi'])
Atendendo: Ana
deque(['Bruno', 'Carla', 'Davi'])
Prioridade: deque(['Dona Rosa', 'Bruno', 'Carla', 'Davi'])
Último da fila: Davi
4 pessoas
MétodoPontaO que faz
append(x)direita (fim)Entra na fila
popleft()esquerda (começo)Sai da fila: tira e devolve o primeiro
appendleft(x)esquerdaFura a fila (atendimento prioritário)
pop()direitaTira e devolve o último (desistiu de esperar)
fila[0], fila[-1], len(fila)Olhar as pontas e o tamanho, como numa lista

Tamanho máximo: os últimos N

Com maxlen, o deque guarda no máximo aquela quantidade: quando enche, cada item novo empurra para fora o mais antigo, do outro lado. Ótimo para "os 3 últimos pedidos":

from collections import deque

ultimos = deque(maxlen=3)
for pedido in [101, 102, 103, 104, 105]:
    ultimos.append(pedido)
    print(list(ultimos))
$ python3 ultimos.py
[101]
[101, 102]
[101, 102, 103]
[102, 103, 104]
[103, 104, 105]

Girando: rotate

Um rodízio de quem abre a padaria a cada dia: rotate(-1) tira o primeiro e põe no fim.

from collections import deque

turnos = deque(["Ana", "Bruno", "Carla"])
for dia in ["seg", "ter", "qua", "qui"]:
    print(dia, "->", turnos[0])
    turnos.rotate(-1)
$ python3 rotate.py
seg -> Ana
ter -> Bruno
qua -> Carla
qui -> Ana

Formas diferentes: quando a coleção especial vale a pena

TarefaCom o básicoCom collectionsQual usar
Contar ocorrênciasdict + if, ou d[k] = d.get(k, 0) + 1Counter(itens)Counter: uma linha, e ainda tem most_common
Agrupar em listasd.setdefault(k, []).append(x)defaultdict(list)Os dois servem; defaultdict lê melhor num laço
Somar por chaved[k] = d.get(k, 0) + ndefaultdict(int)defaultdict(int)
Ficha com campostupla ou dicionárionamedtuplenamedtuple se não muda; dataclass se muda
Fila (entra no fim, sai do começo)lista.pop(0)deque.popleft()deque; a lista serve para filas pequenas
Pilha (entra e sai no fim)lista.append / lista.pop()dequeA lista já é ótima

Quando dá errado

Somar numa chave que não existe (dicionário comum)
palavras = ["pão", "leite", "pão"]

contagem = {}
for palavra in palavras:
    contagem[palavra] += 1

print(contagem)
$ python3 erro_keyerror.py
Traceback (most recent call last):
  File "/home/ana/erro_keyerror.py", line 5, in <module>
    contagem[palavra] += 1
    ~~~~~~~~^^^^^^^^^
KeyError: 'pão'

KeyError: 'pão': "erro de chave". O += 1 precisa ler o valor atual antes de somar, e no primeiro "pão" não havia valor nenhum. O ~~~~~~~~^^^^^^^^^ aponta a chave que faltou. Como consertar: contagem = Counter(palavras), ou contagem = defaultdict(int) antes do laço.

Counter de um texto em vez de uma lista (sem erro nenhum)
from collections import Counter

compras = "pão leite pão"
print(Counter(compras).most_common(2))
print(Counter(compras.split()).most_common(2))
$ python3 counter_texto.py
[('p', 2), ('ã', 2)]
[('pão', 2), ('leite', 1)]

A primeira linha contou letras: um texto é uma sequência de caracteres, e o Counter conta cada item da sequência. O programa roda, mas a resposta não faz sentido. Como consertar: quebre o texto em palavras com .split() antes de contar.

Mudar um campo da namedtuple
from collections import namedtuple

Produto = namedtuple("Produto", ["nome", "preco", "estoque"])
pao = Produto("pão francês", 0.75, 120)
pao.preco = 0.85
$ python3 erro_nt_set.py
Traceback (most recent call last):
  File "/home/ana/erro_nt_set.py", line 5, in <module>
    pao.preco = 0.85
    ^^^^^^^^^
AttributeError: can't set attribute

"Não é possível definir o atributo". Uma namedtuple é uma tupla, e tuplas são imutáveis (aula Mutável × Imutável). Como consertar: crie uma ficha nova, pao = pao._replace(preco=0.85); ou, se os dados mudam o tempo todo, use uma dataclass.

Faltou um campo
$ python3 erro_nt_falta.py
Traceback (most recent call last):
  File "/home/ana/erro_nt_falta.py", line 4, in <module>
    pao = Produto("pão francês", 0.75)
          ~~~~~~~^^^^^^^^^^^^^^^^^^^^^
TypeError: Produto.__new__() missing 1 required positional argument: 'estoque'

"Faltou 1 argumento posicional obrigatório: estoque". Todos os campos são obrigatórios. O __new__ é o método que cria o objeto, e a mensagem diz exatamente qual campo faltou. Como consertar: passe o estoque, ou dê valores padrão ao criar o tipo: namedtuple("Produto", ["nome", "preco", "estoque"], defaults=[0]) (o padrão vale para o último campo).

Atender numa fila vazia
from collections import deque

fila = deque(["Ana"])
print("Atendendo:", fila.popleft())
print("Atendendo:", fila.popleft())
$ python3 erro_deque.py
Atendendo: Ana
Traceback (most recent call last):
  File "/home/ana/erro_deque.py", line 5, in <module>
    print("Atendendo:", fila.popleft())
                        ~~~~~~~~~~~~^^
IndexError: pop from an empty deque

"Tirar de um deque vazio". A Ana já foi atendida, e não sobrou ninguém. Como consertar: confira antes, while fila: ou if fila: (uma fila vazia vale False, como uma lista vazia).

defaultdict(list()), com parênteses
$ python3 erro_dd.py
Traceback (most recent call last):
  File "/home/ana/erro_dd.py", line 3, in <module>
    turmas = defaultdict(list())
TypeError: first argument must be callable or None

"O primeiro argumento tem de ser algo que se possa chamar, ou None". Com os parênteses, você passou uma lista vazia (o resultado de list()), e não a fábrica list, que o defaultdict vai chamar a cada chave nova. É a mesma diferença entre f e f() da aula Decoradores. Como consertar: defaultdict(list), sem parênteses.


Referência rápida

EscritaO que faz
Counter(itens)Conta cada item; chave ausente vale 0
c.most_common(n)Os n mais comuns, em pares (item, contagem)
c.update(itens), c1 + c2, c.total()Conta mais / soma contadores / soma das contagens (3.10+)
defaultdict(list)Chave nova começa com []
defaultdict(int)Chave nova começa com 0
namedtuple("Nome", ["a", "b"])Cria um tipo de tupla com campos nomeados
x._replace(campo=valor), x._asdict()Cópia com um campo trocado / dicionário
deque(itens), deque(maxlen=n)Fila de duas pontas / com tamanho máximo
append, appendleft, pop, popleftPôr e tirar no fim / no começo
d.rotate(n)Gira n casas (negativo: para a esquerda)
SintomaCausa provável
KeyError ao somar ou dar appendDicionário comum com chave nova: use Counter ou defaultdict
Counter contou letrasRecebeu um texto: faltou o .split()
Apareceram chaves vazias no defaultdictSó ler d[chave] já cria: pergunte com in ou get
AttributeError: can't set attributeTentou mudar um campo de namedtuple: use _replace
__new__() missing 1 required positional argumentFaltou um campo da namedtuple
IndexError: pop from an empty dequepopleft/pop com a fila vazia
TypeError: first argument must be callable or Nonedefaultdict(list()): tire os parênteses

Exercícios

1. O mais vendido

Complete para mostrar os 2 produtos mais vendidos:

vendas = Counter(lista)
print(vendas.)

2. A fábrica

Complete para que toda chave nova comece com uma lista vazia:

grupos = defaultdict()

3. A fila do caixa

Qual método do deque tira e devolve a primeira pessoa da fila?

proximo = fila.

4. c = Counter(["pão", "leite"]). Quanto vale c["café"]?

5. d = deque(maxlen=2), depois d.append(1), d.append(2), d.append(3). O que d guarda?

6. Você precisa de uma ficha de cidade (nome, estado, população) que nunca muda e que vai ser chave de um dicionário. Qual escolher?

Missão final: a fila do caixa

Missão: transforme a lista numa fila de verdade, deque([…]), e atenda as duas primeiras pessoas com popleft(), mostrando Atendendo: … para cada uma. A última linha deve mostrar quem sobrou: Na fila: deque(['Carla', 'Davi']).

from collections import deque

fila = ["Ana", "Bruno", "Carla", "Davi"]
# transforme a fila num deque e atenda as duas primeiras pessoas com popleft()

print("Na fila:", fila)
from collections import deque

fila = deque(["Ana", "Bruno", "Carla", "Davi"])

print("Atendendo:", fila.popleft())
print("Atendendo:", fila.popleft())
print("Na fila:", fila)

Resumo

  • collections traz versões especializadas das coleções básicas; importe só o que usar.
  • Counter conta: Counter(itens), chave ausente vale 0, most_common(n) dá o ranking.
  • defaultdict(fabrica) cria o valor da chave nova: list para agrupar, int para somar. A fábrica vai sem parênteses, e só ler uma chave já a cria.
  • namedtuple dá nome aos campos de uma tupla: p.preco; continua imutável (_replace faz uma cópia).
  • deque é a fila de duas pontas: append e popleft; com maxlen, guarda só os últimos.