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.
Counter: contar palavras, vendas e letras, e achar os mais comuns commost_common.defaultdict: agrupar e somar sem oKeyErrorda chave que ainda não existe.namedtuple: uma tupla em que cada campo tem nome (produto.precoem vez deproduto[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 com | A especialidade | Exemplo da Ana |
|---|---|---|---|
Counter | dicionário | Contar quantas vezes cada coisa aparece | Qual pão vendeu mais hoje? |
defaultdict | dicionário | Criar sozinho o valor de uma chave nova | Separar os alunos por turno |
namedtuple | tupla | Dar nome a cada posição | Ficha de produto: nome, preço, estoque |
deque | lista | Pôr e tirar rápido nas duas pontas | A 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.
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 + tercasoma 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 recebelist(), uma lista vazia. - turmas[turno].append(nome)
- Na primeira vez que aparece
"manhã", odefaultdictcriaturmas["manhã"] = []e oappendpõ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.)
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
paooriginal 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 dereplace.
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étodo | Ponta | O que faz |
|---|---|---|
append(x) | direita (fim) | Entra na fila |
popleft() | esquerda (começo) | Sai da fila: tira e devolve o primeiro |
appendleft(x) | esquerda | Fura a fila (atendimento prioritário) |
pop() | direita | Tira 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
| Tarefa | Com o básico | Com collections | Qual usar |
|---|---|---|---|
| Contar ocorrências | dict + if, ou d[k] = d.get(k, 0) + 1 | Counter(itens) | Counter: uma linha, e ainda tem most_common |
| Agrupar em listas | d.setdefault(k, []).append(x) | defaultdict(list) | Os dois servem; defaultdict lê melhor num laço |
| Somar por chave | d[k] = d.get(k, 0) + n | defaultdict(int) | defaultdict(int) |
| Ficha com campos | tupla ou dicionário | namedtuple | namedtuple 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() | deque | A lista já é ótima |
Quando dá errado
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.
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.
$ 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).
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
| Escrita | O 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, popleft | Pôr e tirar no fim / no começo |
d.rotate(n) | Gira n casas (negativo: para a esquerda) |
| Sintoma | Causa provável |
|---|---|
KeyError ao somar ou dar append | Dicionário comum com chave nova: use Counter ou defaultdict |
Counter contou letras | Recebeu um texto: faltou o .split() |
Apareceram chaves vazias no defaultdict | Só ler d[chave] já cria: pergunte com in ou get |
AttributeError: can't set attribute | Tentou mudar um campo de namedtuple: use _replace |
__new__() missing 1 required positional argument | Faltou um campo da namedtuple |
IndexError: pop from an empty deque | popleft/pop com a fila vazia |
TypeError: first argument must be callable or None | defaultdict(list()): tire os parênteses |
Exercícios
1. O mais vendido
Complete para mostrar os 2 produtos mais vendidos:
print(vendas.)
2. A fábrica
Complete para que toda chave nova comece com uma lista vazia:
3. A fila do caixa
Qual método do deque tira e devolve a primeira pessoa da 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
collectionstraz versões especializadas das coleções básicas; importe só o que usar.Counterconta:Counter(itens), chave ausente vale 0,most_common(n)dá o ranking.defaultdict(fabrica)cria o valor da chave nova:listpara agrupar,intpara somar. A fábrica vai sem parênteses, e só ler uma chave já a cria.namedtupledá nome aos campos de uma tupla:p.preco; continua imutável (_replacefaz uma cópia).dequeé a fila de duas pontas:appendepopleft; commaxlen, guarda só os últimos.