defaultdict : La evolución de un diccionario sin condiciones

Python cuenta con innumerables bibliotecas externas, pero entender bien la biblioteca estándar es suficiente para escribir código potente en el trabajo. En este artículo, profundizaremos en collections.defaultdict.

A través de este artículo, no solo te presentaré el concepto, sino que también entenderás claramente cuándo, por qué y cómo usar defaultdict.

Si tienes curiosidad sobre la primera clase de la serie collections, el Counter, te recomiendo que leas el artículo anterior. Conquista de la biblioteca estándar de Python ① - collections.Counter


1. Concepto Básico: ¿Qué es defaultdict?

Tux con defaultdicts

defaultdict es una subclase de diccionario (dict) especial incluida en el módulo de la biblioteca estándar de Python collections. Mientras que en un diccionario normal, al acceder a una clave inexistente se produce un KeyError, en defaultdict se puede especificar una función de creación de valores por defecto, lo que hace que el código sea mucho más limpio y ayuda a prevenir errores.


2. Uso Básico

from collections import defaultdict

d = defaultdict(int)
d['apple'] += 1
print(d)  # defaultdict(<class 'int'>, {'apple': 1})

Aquí, int() devuelve 0 como valor por defecto. Al acceder a la clave inexistente 'apple', se genera automáticamente 0 sin KeyError, y luego se realiza +1.


3. Ejemplos de Valores por Defecto Variados

from collections import defaultdict

# Valor por defecto: 0 (int)
counter = defaultdict(int)
counter['a'] += 1
print(counter)  # defaultdict(<class 'int'>, {'a': 1})

# Valor por defecto: lista vacía
group = defaultdict(list)
group['fruit'].append('apple')
group['fruit'].append('banana')
print(group)  # defaultdict(<class 'list'>, {'fruit': ['apple', 'banana']})

# Valor por defecto: conjunto vacío
unique_tags = defaultdict(set)
unique_tags['tags'].add('python')
unique_tags['tags'].add('coding')
print(unique_tags)  # defaultdict(<class 'set'>, {'tags': {'python', 'coding'}})

# Valor por defecto: valor inicial personalizado
fixed = defaultdict(lambda: 100)
print(fixed['unknown'])  # 100

4. Ejemplos en la Práctica

1. Contar la Frecuencia de Palabras

words = ['apple', 'banana', 'apple', 'orange', 'banana']
counter = defaultdict(int)

for word in words:
    counter[word] += 1

print(counter)
# defaultdict(<class 'int'>, {'apple': 2, 'banana': 2, 'orange': 1})

👉 Counter vs defaultdict
El conteo de la frecuencia de palabras es más especializado en collections.Counter(), así que si necesitas análisis estadísticos o de ranking, es recomendable usar Counter. Sin embargo, para conteos acumulativos simples, defaultdict(int) también se puede usar con suficiente simplicidad.

2. Organizar Registros por Grupo

logs = [
    ('2024-01-01', 'INFO'),
    ('2024-01-01', 'ERROR'),
    ('2024-01-02', 'DEBUG'),
]

grouped = defaultdict(list)
for date, level in logs:
    grouped[date].append(level)

print(grouped)
# defaultdict(<class 'list'>, {'2024-01-01': ['INFO', 'ERROR'], '2024-01-02': ['DEBUG']})

3. Organizar Etiquetas Sin Duplicados

entries = [
    ('post1', 'python'),
    ('post1', 'coding'),
    ('post1', 'python'),
]

tags = defaultdict(set)
for post, tag in entries:
    tags[post].add(tag)

print(tags)
# defaultdict(<class 'set'>, {'post1': {'python', 'coding'}})

5. Precauciones

  • defaultdict almacena internamente un generador de valores por defecto, por lo que al usar repr() puede verse diferente a un dict normal.
  • Puede haber problemas al serializar a JSON. Es seguro convertirlo a dict(d) antes de procesarlo.
  • Los valores por defecto solo se crean al acceder con []. Si usas get(), no se crearán.
from collections import defaultdict

d = defaultdict(list)
print(d.get('missing'))  # None
print(d['missing'])      # []

6. ¿Cuándo es Bueno Usarlo? – 3 Ventajas Decisivas de defaultdict

defaultdict eleva simultáneamente la legibilidad, mantenibilidad y seguridad en situaciones que suelen emplear el patrón dict + condiciones. Especialmente en las siguientes 3 situaciones es donde se siente '¡Ah, definitivamente es mejor usar defaultdict!'

6-1. Código de Agregación que Cuenta/Suma Sin Condicionales

from collections import defaultdict

# dict normal
counts = {}
for item in items:
    if item not in counts:
        counts[item] = 0
    counts[item] += 1

# defaultdict
counts = defaultdict(int)
for item in items:
    counts[item] += 1

✔ Al eliminar las condiciones, el código se simplifica y se reduce el riesgo de errores.
✔ Especialmente adecuado para análisis de registros, conteo de palabras y procesamiento de grandes volúmenes de datos.

6-2. Acumulando Listas/Conjuntos Como Sustituto de setdefault

from collections import defaultdict

# dict normal
posts = {}
for tag, post in data:
    if tag not in posts:
        posts[tag] = []
    posts[tag].append(post)

# defaultdict
posts = defaultdict(list)
for tag, post in data:
    posts[tag].append(post)

✔ Es mucho más intuitivo que setdefault() y más claro dentro de los bucles.
✔ Estructura optimizada para agrupación de datos.

6-3. Automatizar la Inicialización al Construir un Diccionario Anidado

# Diccionario normal
matrix = {}
if 'x' not in matrix:
    matrix['x'] = {}
matrix['x']['y'] = 10

# Anidado con defaultdict
matrix = defaultdict(lambda: defaultdict(int))  # Se genera automáticamente defaultdict(int) al faltar la clave
matrix['x']['y'] += 10

✔ Facilita la creación de estructuras de datos anidadas, lo que es muy útil para trabajar con diccionarios multidimensionales.
✔ Tiene grandes ventajas en minería de datos, análisis y almacenamiento en estructuras de árbol.

lambda: defaultdict(int) es una estructura que devuelve defaultdict(int) cada vez que falta una clave, creando automáticamente el diccionario anidado.


7. Resumen

collections.defaultdict puede parecer una simple extensión de dict para los principiantes, pero con su uso se siente que es una herramienta estructural que hace que el código sea más claro y seguro.

  • Se puede utilizar un diccionario sin preocuparse por KeyError.
  • Se puede agrupar y acumular datos sin condiciones.
  • Se puede construir diccionarios anidados de manera intuitiva.
# Ejemplo manejado de manera estable y concisa con defaultdict
salaries = defaultdict(int)
for dept, amount in records:
    salaries[dept] += amount

Si con una línea de código se puede prevenir errores, mejorar la legibilidad y aumentar la mantenibilidad,
se puede considerar que defaultdict no es solo una función conveniente, sino que es una herramienta clave en la forma de pensar de Python.

El siguiente tema que abordaremos será pathlib.
Una forma moderna de manejar archivos/directorios de manera orientada a objetos, útil para principiantes y niveles intermedios. Los desarrolladores que están acostumbrados a os.path sentirán '¡Wow! ¡Es mucho más simple que os.path!'. Espera con ansias el próximo artículo.