Skip to content

Python defaultdict: Simplifique operações de dicionário com valores padrão

Updated on

Todo desenvolvedor Python já enfrentou esse problema: você escreve um loop limpo para agrupar ou contar itens usando um dicionário, executa o código, e um KeyError derruba todo o script porque uma chave ainda não existia. A solução padrão é espalhar verificações if key in dict ou blocos try/except KeyError por todo lugar. Sua lógica para agrupar dez linhas de dados de repente infla para vinte linhas de código defensivo repetitivo.

Isso piora em escala. Quando você está construindo listas de adjacência para grafos, agregando dados de log ou contando frequências de palavras em milhões de registros, essas cláusulas de proteção se acumulam. Elas desaceleram seu trabalho como desenvolvedor, tornam o código mais difícil de revisar e introduzem bugs sutis quando você esquece uma verificação em uma ramificação.

O collections.defaultdict do Python elimina toda essa categoria de problemas. É uma subclasse de dicionário que chama uma função fábrica para fornecer valores ausentes automaticamente. Sem mais KeyError, sem mais cláusulas de proteção, sem mais código repetitivo.

📚

O que é defaultdict?

O defaultdict é uma subclasse do dict embutido do Python. A diferença chave: quando você acessa uma chave que não existe, o defaultdict a cria automaticamente com um valor padrão em vez de lançar um KeyError.

from collections import defaultdict
 
# Dict regular lança KeyError
regular = {}
# regular['missing']  # KeyError: 'missing'
 
# defaultdict cria o valor automaticamente
dd = defaultdict(int)
dd['missing']  # Retorna 0, e agora 'missing' é uma chave
print(dd)  # defaultdict(<class 'int'>, {'missing': 0})

O construtor recebe uma função fábrica como primeiro argumento. Fábricas comuns:

  • int -- retorna 0
  • list -- retorna []
  • set -- retorna set()
  • str -- retorna ""
  • lambda: value -- retorna qualquer valor padrão personalizado

defaultdict(int) -- O padrão de contagem

O uso mais comum. Cada nova chave começa em 0, então você pode incrementar imediatamente.

from collections import defaultdict
 
words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
 
# Sem defaultdict
counts_regular = {}
for word in words:
    if word in counts_regular:
        counts_regular[word] += 1
    else:
        counts_regular[word] = 1
 
# Com defaultdict(int) -- limpo e direto
counts = defaultdict(int)
for word in words:
    counts[word] += 1
 
print(dict(counts))
# {'apple': 3, 'banana': 2, 'cherry': 1}

defaultdict(list) -- O padrão de agrupamento

Agrupe itens relacionados. Cada nova chave começa com uma lista vazia.

from collections import defaultdict
 
students = [
    ('Math', 'Alice'),
    ('Science', 'Bob'),
    ('Math', 'Charlie'),
    ('Science', 'Diana'),
    ('Math', 'Eve'),
    ('History', 'Frank'),
]
 
groups = defaultdict(list)
for subject, student in students:
    groups[subject].append(student)
 
for subject, names in groups.items():
    print(f"{subject}: {', '.join(names)}")
 
# Math: Alice, Charlie, Eve
# Science: Bob, Diana
# History: Frank

Agrupar registros por múltiplos campos

from collections import defaultdict
 
sales = [
    {'region': 'East', 'product': 'Widget', 'amount': 100},
    {'region': 'West', 'product': 'Gadget', 'amount': 200},
    {'region': 'East', 'product': 'Widget', 'amount': 150},
    {'region': 'West', 'product': 'Widget', 'amount': 300},
]
 
by_region_product = defaultdict(list)
for sale in sales:
    key = (sale['region'], sale['product'])
    by_region_product[key].append(sale['amount'])
 
for (region, product), amounts in by_region_product.items():
    total = sum(amounts)
    print(f"{region} - {product}: {amounts} (total: {total})")

defaultdict(set) -- Agrupamento único

Colete valores únicos por chave automaticamente.

from collections import defaultdict
 
edges = [
    ('Alice', 'Bob'), ('Alice', 'Charlie'),
    ('Bob', 'Alice'), ('Bob', 'Diana'),
    ('Alice', 'Bob'),  # duplicate
]
 
connections = defaultdict(set)
for person, friend in edges:
    connections[person].add(friend)
 
for person, friends in connections.items():
    print(f"{person} is connected to: {friends}")
# Alice is connected to: {'Bob', 'Charlie'}
# Bob is connected to: {'Alice', 'Diana'}

defaultdict(lambda: value) -- Valores padrão personalizados

Quando os tipos embutidos não se encaixam, use um lambda para retornar qualquer valor padrão.

from collections import defaultdict
 
# Valor padrão 'N/A' para entradas ausentes
status = defaultdict(lambda: 'N/A')
status['server1'] = 'running'
status['server2'] = 'stopped'
print(status['server3'])   # N/A
 
# Saldo inicial padrão
accounts = defaultdict(lambda: 100.0)
accounts['alice'] += 50
accounts['bob'] -= 30
print(dict(accounts))  # {'alice': 150.0, 'bob': 70.0}

Dicionário padrão com valores estruturados

from collections import defaultdict
 
def default_profile():
    return {'score': 0, 'level': 1, 'items': []}
 
profiles = defaultdict(default_profile)
profiles['player1']['score'] += 100
profiles['player1']['items'].append('sword')
profiles['player2']['level'] = 5
 
print(profiles['player1'])
# {'score': 100, 'level': 1, 'items': ['sword']}
print(profiles['player3'])
# {'score': 0, 'level': 1, 'items': []}

defaultdict aninhado -- Estruturas de árvore

Um dos padrões mais poderosos é usar defaultdict recursivamente para criar dicionários auto-vivificantes.

from collections import defaultdict
 
def tree():
    return defaultdict(tree)
 
taxonomy = tree()
taxonomy['Animal']['Mammal']['Dog'] = 'Canis lupus familiaris'
taxonomy['Animal']['Mammal']['Cat'] = 'Felis catus'
taxonomy['Animal']['Bird']['Eagle'] = 'Aquila chrysaetos'
taxonomy['Plant']['Tree']['Oak'] = 'Quercus'
 
print(taxonomy['Animal']['Mammal']['Dog'])  # Canis lupus familiaris

Agregação multinível

from collections import defaultdict
 
sales_data = [
    (2025, 'Q1', 'Widget', 500),
    (2025, 'Q1', 'Gadget', 300),
    (2025, 'Q2', 'Widget', 700),
    (2026, 'Q1', 'Widget', 600),
]
 
report = defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
for year, quarter, product, amount in sales_data:
    report[year][quarter][product] += amount
 
print(report[2025]['Q1']['Widget'])  # 500
print(report[2026]['Q1']['Widget'])  # 600

defaultdict vs dict.setdefault() vs get() -- Comparação

Recursodefaultdictdict.setdefault()dict.get()
Import necessárioSim (collections)NãoNão
Cria chave automaticamenteSimSimNão
Modifica dict ao acessarSimSimNão
Padrão personalizado por chamadaNão (fábrica global)SimSim
Desempenho (repetido)Mais rápidoMais lento (overhead de chamada de método)Mais rápido (sem mutação)
Melhor paraAcumulação repetidaPadrões pontuaisFallback somente leitura

Quando usar cada um:

  • defaultdict: construir valores ao longo de muitas iterações (contagem, agrupamento)
  • dict.setdefault(): ocasionalmente precisar de um padrão para uma chave específica
  • dict.get(): ler um valor com fallback sem modificar o dicionário

Converter defaultdict de volta para dict regular

from collections import defaultdict
import json
 
def defaultdict_to_dict(d):
    """Recursively convert defaultdict to regular dict."""
    if isinstance(d, defaultdict):
        d = {k: defaultdict_to_dict(v) for k, v in d.items()}
    return d
 
nested = defaultdict(lambda: defaultdict(int))
nested['x']['y'] = 10
nested['a']['b'] = 20
 
regular = defaultdict_to_dict(nested)
print(json.dumps(regular))  # {"x": {"y": 10}, "a": {"b": 20}}

Você também pode desativar a fábrica padrão definindo-a como None:

dd = defaultdict(int)
dd['a'] += 1
dd.default_factory = None
# dd['missing']  # Agora lança KeyError

Exemplos práticos

Lista de adjacência para grafos

from collections import defaultdict, deque
 
edges = [('A', 'B'), ('A', 'C'), ('B', 'D'), ('C', 'D'), ('D', 'E')]
 
graph = defaultdict(list)
for src, dst in edges:
    graph[src].append(dst)
    graph[dst].append(src)  # undirected graph
 
def bfs(graph, start):
    visited = set()
    queue = deque([start])
    order = []
    while queue:
        node = queue.popleft()
        if node not in visited:
            visited.add(node)
            order.append(node)
            queue.extend(graph[node])
    return order
 
print(bfs(graph, 'A'))  # ['A', 'B', 'C', 'D', 'E']

Índice invertido para busca de texto

from collections import defaultdict
 
documents = {
    'doc1': 'python is a great programming language',
    'doc2': 'data science uses python extensively',
    'doc3': 'machine learning with python and data',
}
 
index = defaultdict(set)
for doc_id, text in documents.items():
    for word in text.split():
        index[word.lower()].add(doc_id)
 
def search(query):
    return index.get(query.lower(), set())
 
print(search('python'))  # {'doc1', 'doc2', 'doc3'}
print(search('data'))    # {'doc2', 'doc3'}

Visualizar dados agrupados com PyGWalker

Após agrupar e agregar dados com defaultdict, você frequentemente deseja visualizar os resultados. PyGWalker (opens in a new tab) transforma seu DataFrame pandas em uma interface de visualização interativa diretamente no Jupyter:

from collections import defaultdict
import pandas as pd
import pygwalker as pyg
 
sales = [
    ('Electronics', 'Laptop', 1200),
    ('Electronics', 'Phone', 800),
    ('Clothing', 'Shirt', 45),
    ('Clothing', 'Jacket', 120),
]
 
totals = defaultdict(lambda: defaultdict(int))
for category, product, amount in sales:
    totals[category][product] += amount
 
rows = []
for category, products in totals.items():
    for product, total in products.items():
        rows.append({'category': category, 'product': product, 'total': total})
 
df = pd.DataFrame(rows)
walker = pyg.walk(df)

FAQ

O que é defaultdict em Python?

defaultdict é uma subclasse de dicionário em collections que fornece um valor padrão para chaves ausentes. Em vez de lançar um KeyError, ele chama uma função fábrica (como int, list ou set) para criar e armazenar um valor padrão automaticamente.

Qual é a diferença entre dict e defaultdict?

A única diferença funcional é como eles lidam com chaves ausentes. Um dict regular lança KeyError. Um defaultdict chama sua função default_factory para criar um valor padrão. Em todos os outros aspectos, eles se comportam de forma idêntica.

Quando devo usar defaultdict(list) vs defaultdict(set)?

Use defaultdict(list) quando quiser agrupar itens e preservar duplicatas e ordem de inserção. Use defaultdict(set) quando quiser coletar apenas itens únicos por chave.

Posso serializar um defaultdict para JSON?

Sim, mas para objetos defaultdict aninhados, converta-os primeiro para dict regular usando uma função de conversão recursiva. Você também pode definir default_factory = None para evitar a criação acidental de chaves antes da serialização.

Como criar um defaultdict aninhado?

Defina uma função fábrica recursiva: def tree(): return defaultdict(tree). Para aninhamento de dois níveis mais simples, use defaultdict(lambda: defaultdict(int)).

Conclusão

O collections.defaultdict do Python é uma das ferramentas mais práticas na biblioteca padrão. Ele transforma padrões de acumulação de dicionários verbosos e propensos a erros em linhas únicas limpas. Use defaultdict(int) para contagem, defaultdict(list) para agrupamento, defaultdict(set) para coleção única e defaultdict aninhado para dados hierárquicos.

A conclusão principal: se você está escrevendo if key not in dict antes de cada operação de dicionário, substitua esse dicionário por um defaultdict. Seu código será mais curto, mais rápido e muito mais fácil de manter.

📚