Python defaultdict: Simplifica las operaciones de diccionario con valores predeterminados
Updated on
Todo desarrollador de Python ha chocado con este muro: escribes un bucle limpio para agrupar o contar elementos usando un diccionario, ejecutas el código y un KeyError hace que todo el script se detenga porque una clave aún no existía. La solución habitual es añadir verificaciones if key in dict o bloques try/except KeyError por todas partes. Tu lógica para agrupar diez líneas de datos de repente se infla a veinte líneas de código defensivo repetitivo.
Esto empeora a escala. Cuando estás construyendo listas de adyacencia para grafos, agregando datos de registro o contando frecuencias de palabras en millones de registros, esas cláusulas de protección se acumulan. Te ralentizan como desarrollador, hacen el código más difícil de revisar e introducen errores sutiles cuando olvidas una verificación en una rama.
El collections.defaultdict de Python elimina toda esta categoría de problemas. Es una subclase de diccionario que llama a una función factoría para proporcionar valores faltantes automáticamente. Sin más KeyError, sin más cláusulas de protección, sin más código repetitivo.
¿Qué es defaultdict?
El defaultdict es una subclase del dict incorporado de Python. La diferencia clave: cuando accedes a una clave que no existe, defaultdict la crea automáticamente con un valor predeterminado en lugar de lanzar un KeyError.
from collections import defaultdict
# Un dict regular lanza KeyError
regular = {}
# regular['missing'] # KeyError: 'missing'
# defaultdict crea el valor automáticamente
dd = defaultdict(int)
dd['missing'] # Retorna 0, y ahora 'missing' es una clave
print(dd) # defaultdict(<class 'int'>, {'missing': 0})El constructor toma una función factoría como primer argumento. Factorías comunes:
int-- retorna0list-- retorna[]set-- retornaset()str-- retorna""lambda: value-- retorna cualquier valor predeterminado personalizado
defaultdict(int) -- El patrón de conteo
El uso más común. Cada nueva clave comienza en 0, así que puedes incrementar inmediatamente.
from collections import defaultdict
words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
# Sin defaultdict
counts_regular = {}
for word in words:
if word in counts_regular:
counts_regular[word] += 1
else:
counts_regular[word] = 1
# Con defaultdict(int) -- limpio y directo
counts = defaultdict(int)
for word in words:
counts[word] += 1
print(dict(counts))
# {'apple': 3, 'banana': 2, 'cherry': 1}defaultdict(list) -- El patrón de agrupación
Agrupa elementos relacionados. Cada nueva clave comienza con una lista vacía.
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: FrankAgrupar registros por múltiples 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) -- Agrupación única
Recopila valores únicos por clave automáticamente.
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 predeterminados personalizados
Cuando los tipos integrados no se ajustan, usa un lambda para retornar cualquier valor predeterminado.
from collections import defaultdict
# Valor predeterminado 'N/A' para entradas faltantes
status = defaultdict(lambda: 'N/A')
status['server1'] = 'running'
status['server2'] = 'stopped'
print(status['server3']) # N/A
# Saldo inicial predeterminado
accounts = defaultdict(lambda: 100.0)
accounts['alice'] += 50
accounts['bob'] -= 30
print(dict(accounts)) # {'alice': 150.0, 'bob': 70.0}Diccionario predeterminado con valores estructurados
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 anidado -- Estructuras de árbol
Uno de los patrones más potentes es usar defaultdict recursivamente para crear diccionarios 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 familiarisAgregación multinivel
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']) # 600defaultdict vs dict.setdefault() vs get() -- Comparación
| Característica | defaultdict | dict.setdefault() | dict.get() |
|---|---|---|---|
| Requiere import | Sí (collections) | No | No |
| Crea clave automáticamente | Sí | Sí | No |
| Modifica dict al acceder | Sí | Sí | No |
| Valor personalizado por llamada | No (factoría global) | Sí | Sí |
| Rendimiento (repetido) | Más rápido | Más lento (overhead de llamada al método) | Más rápido (sin mutación) |
| Mejor para | Acumulación repetida | Valores predeterminados puntuales | Lectura con respaldo |
Cuándo usar cada uno:
defaultdict: construir valores a lo largo de muchas iteraciones (contar, agrupar)dict.setdefault(): necesitar ocasionalmente un valor predeterminado para una clave específicadict.get(): leer un valor con respaldo sin modificar el diccionario
Convertir defaultdict de vuelta a 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}}También puedes desactivar la factoría predeterminada estableciéndola en None:
dd = defaultdict(int)
dd['a'] += 1
dd.default_factory = None
# dd['missing'] # Ahora lanza KeyErrorEjemplos prácticos
Lista de adyacencia 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 búsqueda 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 datos agrupados con PyGWalker
Después de agrupar y agregar datos con defaultdict, a menudo querrás visualizar los resultados. PyGWalker (opens in a new tab) convierte tu DataFrame de pandas en una interfaz de visualización interactiva directamente en 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
¿Qué es defaultdict en Python?
defaultdict es una subclase de diccionario en collections que proporciona un valor predeterminado para claves faltantes. En lugar de lanzar un KeyError, llama a una función factoría (como int, list o set) para crear y almacenar un valor predeterminado automáticamente.
¿Cuál es la diferencia entre dict y defaultdict?
La única diferencia funcional es cómo manejan las claves faltantes. Un dict regular lanza KeyError. Un defaultdict llama a su función default_factory para crear un valor predeterminado. En todos los demás aspectos se comportan de manera idéntica.
¿Cuándo debo usar defaultdict(list) vs defaultdict(set)?
Usa defaultdict(list) cuando quieras agrupar elementos y preservar duplicados y el orden de inserción. Usa defaultdict(set) cuando quieras recopilar solo elementos únicos por clave.
¿Puedo serializar un defaultdict a JSON?
Sí, pero para objetos defaultdict anidados, conviértelos primero a dict regular usando una función de conversión recursiva. También puedes establecer default_factory = None para evitar la creación accidental de claves antes de la serialización.
¿Cómo creo un defaultdict anidado?
Define una función factoría recursiva: def tree(): return defaultdict(tree). Para anidación de dos niveles más simple, usa defaultdict(lambda: defaultdict(int)).
Conclusión
El collections.defaultdict de Python es una de las herramientas más prácticas en la biblioteca estándar. Convierte patrones de acumulación de diccionarios verbosos y propensos a errores en líneas únicas limpias. Usa defaultdict(int) para contar, defaultdict(list) para agrupar, defaultdict(set) para recopilación única y defaultdict anidado para datos jerárquicos.
La conclusión clave: si te encuentras escribiendo if key not in dict antes de cada operación de diccionario, reemplaza ese diccionario por un defaultdict. Tu código será más corto, más rápido y mucho más fácil de mantener.