Skip to content
This page has been auto-translated and may contain errors.View in English

Diccionarios

docs.scrimba.com

Las listas te permiten buscar cosas por posición. Pero a menudo quieres buscar algo por nombre. No "dame el elemento 3", sino "dame la puntuación de María". Un diccionario almacena datos como pares clave-valor: buscas un valor por su clave, no por su posición.

Cuando el índice posicional de una lista no es significativo, un diccionario es la estructura correcta. Los diccionarios mapean claves arbitrarias a valores, dándote búsqueda nombrada en tiempo O(1). Un ranking, una respuesta JSON, un archivo de configuración: todos se expresan naturalmente como mapeos clave-valor.

Un dict es un almacén clave-valor con búsqueda, inserción y eliminación promedio O(1) (O(1) significa que el costo permanece plano sin importar cuán grande sea el diccionario, porque las claves se encuentran mediante hash, convirtiendo la clave en un número que apunta directamente a su ranura). Las claves deben ser hashables, lo que significa que su contenido nunca cambia para que ese número permanezca estable: str, int y tuple califican, list no. Los valores pueden ser cualquier objeto. Desde Python 3.7, los diccionarios mantienen las claves en el orden en que las insertaste, lo que importa cada vez que serialices o muestres uno. La misma estructura impulsa mucho de Python bajo la superficie, incluyendo los atributos de tus propios objetos, así que vale la pena conocer los diccionarios a fondo. El capítulo Listas cubre el equivalente basado en posición.

Crear un diccionario

Llaves con un dos puntos entre cada clave y valor, y comas entre pares. Las claves son casi siempre strings. Los valores pueden ser cualquier cosa: números, strings, otras listas, incluso otros diccionarios.

Los literales de diccionario usan llaves con sintaxis clave: valor. Las claves pueden ser cualquier tipo inmutable (hashable): strings, enteros, tuplas. Los valores pueden ser cualquier objeto de Python. Los diccionarios preservan el orden de inserción, así que cuando iteras, obtienes elementos en el orden en que fueron añadidos.

Los literales de diccionario se evalúan de izquierda a derecha. Las claves deben ser hashables (su contenido no puede cambiar, para que el número bajo el cual Python las archiva se mantenga): str, int y tuple califican, list y dict no. Los valores no tienen tales restricciones. El orden de inserción es garantía del lenguaje desde Python 3.7, así que la iteración y serialización son reproducibles. Una trampa silenciosa: una clave duplicada en un literal único no genera un error, mantiene el último valor y descarta el anterior.

python
player = {
    "name": "María",
    "score": 87,
    "level": 5,
    "alive": True,
}
JunoCrear un diccionario Llaves, dos puntos entre cada clave y su valor, comas entre pares. Las claves suelen ser strings, los valores pueden ser cualquier cosa, incluso otro diccionario. El orden en que añades las cosas es el orden en que las recuperas, lo cual uso más de lo que esperaba.
JunoCrear un diccionario Llaves con pares clave: valor. Las claves pueden ser cualquier tipo inmutable (str, int, tuple), los valores pueden ser cualquier objeto. La iteración sigue el orden de inserción, así que un diccionario funciona bien cuando el orden importa.
JunoCrear un diccionario Las claves tienen que ser hashables, así que str, int y tuple están dentro, list y dict están fuera. El orden de inserción es garantizado desde 3.7, así que puedes confiar en él para la salida. Vigila la clave duplicada en el literal: toma el último valor en silencio, sin advertencia.

Acceder a valores

Usa corchetes con la clave para obtener el valor. Si la clave no existe, Python lanza un KeyError. Usa .get() cuando no estés seguro de que una clave esté allí: devuelve None en lugar de fallar, o un valor por defecto que especifiques.

El acceso con corchetes lanza KeyError en una clave faltante. .get(clave) devuelve None en un fallo. .get(clave, default) devuelve el valor por defecto en su lugar. Usa .get() cada vez que la presencia de la clave sea incierta; es más seguro y legible que envolver el acceso en un try/except.

El acceso con corchetes busca la clave mediante hash y lee la ranura correspondiente, que es O(1) en promedio (costo plano sin importar el tamaño). En un fallo lanza KeyError. .get(clave, default=None) hace la misma búsqueda pero devuelve el valor por defecto en un fallo en lugar de lanzar, así que es la llamada correcta cuando la presencia de una clave es incierta. Para proteger antes de un acceso con corchetes, clave in d también es O(1); elige in cuando solo necesitas un sí-o-no, .get() cuando quieras el valor o un fallback en un paso.

python
player = {"name": "María", "score": 87}

player["name"]    # "María"
player["score"]   # 87
player["lives"]   # KeyError (clave no existe)
python
player.get("score")          # 87
player.get("lives")          # None (sin error, devuelve None por defecto)
player.get("lives", 3)       # 3   (usa este valor por defecto si la clave no existe)

.get() es más seguro cada vez que una clave podría faltar:

python
count = inventory.get("arrows", 0)   # 0 si "arrows" no está en el diccionario
JunoAcceder a valoresd["clave"] te da el valor, pero lanza KeyError si esa clave no está allí. Cuando no estés seguro, usa .get(): devuelve None en lugar de fallar, o un valor por defecto que pases. Ahora establezco valores por defecto en todo y mis programas dejaron de fallar en claves faltantes.
JunoAcceder a valores El acceso con corchetes lanza KeyError en un fallo, .get(clave) devuelve None, y .get(clave, default) devuelve lo que pases. Usa .get() cuando una clave podría faltar: más limpio que envolver el acceso en un try/except.
JunoAcceder a valoresd[clave] y .get() hacen una búsqueda O(1), la única diferencia es qué sucede en un fallo: lanzar versus devolver un valor por defecto. Usa clave in d cuando quieras un sí-o-no, .get() cuando quieras el valor-o-fallback en un paso.

Añadir y actualizar

Asigna a una clave con corchetes. Si la clave ya existe, el valor se reemplaza. Si aún no existe, se crea una nueva entrada. Usa .update() para fusionar un diccionario completo de una sola vez.

Asignar a una clave es O(1) en promedio y hace ambas cosas: crea la entrada si la clave es nueva, reemplaza el valor si ya existe. .update() acepta otro diccionario o un iterable de pares clave-valor y aplica esa misma asignación para cada entrada, sobrescribiendo claves existentes.

d[clave] = valor realiza hash de la clave e inserta o sobrescribe en un paso O(1) en promedio, así que no hay una operación separada de "añadir" y "actualizar": la asignación hace ambas. .update(otro) es la misma operación ejecutada para cada entrada en otro, sobrescribiendo en cualquier clave compartida. Para fusionar sin mutar el original, el operador | (Python 3.9+) devuelve un nuevo diccionario y deja ambas entradas sin tocar, mientras que |= muta en su lugar como .update(). Usa | cuando una función no debe modificar un diccionario que el llamador aún mantiene.

python
player = {"name": "María", "score": 87}

player["score"] = 92        # actualizar existente
player["level"] = 5         # añadir nueva clave
python
extras = {"level": 5, "alive": True}
player.update(extras)   # añade/sobrescribe con claves de extras
JunoAñadir y actualizar Asigna a una clave con corchetes: si ya está allí el valor se reemplaza, si no aparece una nueva entrada. Una sintaxis maneja ambas, no necesitas verificar primero. Usa .update() para incorporar un diccionario completo de una sola vez.
JunoAñadir y actualizard[clave] = valor crea o reemplaza en un movimiento, así que no hay una distinción entre añadir y actualizar. .update(otro) fusiona un diccionario completo, sobrescribiendo cualquier clave compartida. Ambas son O(1) por clave.
JunoAñadir y actualizar La asignación inserta o sobrescribe en un paso O(1), y .update() es eso repetido por entrada. Cuando no quieras tocar el original, | devuelve un nuevo diccionario fusionado mientras que |= muta en su lugar. Usa | dentro de una función para no cambiar silenciosamente un diccionario que el llamador aún mantiene.

Eliminar elementos

Cuatro formas de eliminar entradas. .pop() elimina una clave y te devuelve el valor. .pop() con un valor por defecto es seguro cuando la clave podría no estar allí. del elimina una clave sin devolver nada. .clear() vacía el diccionario completo.

.pop(clave) lanza KeyError en un fallo. .pop(clave, default) devuelve el valor por defecto en su lugar, lo que lo convierte en el método de eliminación seguro. del d[clave] elimina la clave sin devolver nada y lanza KeyError en un fallo. .clear() elimina todas las entradas pero mantiene el objeto diccionario en sí.

.pop(clave, default) elimina y devuelve en una búsqueda O(1) en promedio, y el valor por defecto lo hace seguro en fallos; sin él, una clave faltante lanza KeyError. del d[clave] elimina sin devolver nada y lanza en un fallo. .clear() vacía el diccionario pero mantiene el mismo objeto, así que cualquier otro nombre que apunte a él ve un diccionario vacío también. El modo de fallo a recordar: mutar un diccionario mientras lo iteras lanza RuntimeError a mitad del bucle. La solución es tomar una instantánea de las claves que planeas eliminar primero, con for clave in list(d):, luego eliminar del original.

python
player = {"name": "María", "score": 87, "level": 5}

player.pop("level")            # elimina "level" y devuelve 5
player.pop("lives", None)      # pop seguro, devuelve None si la clave no existe
del player["score"]            # elimina "score", sin valor devuelto
player.clear()                 # elimina todo

.pop() con un valor por defecto es la forma más segura de eliminar una clave que podría no existir.

JunoEliminar elementos.pop(clave) elimina una clave y te devuelve su valor, y .pop(clave, None) se mantiene tranquilo cuando la clave podría no estar allí. del d[clave] elimina sin devolver nada, .clear() vacía todo. El valor por defecto en .pop() me salvó de muchas sorpresas de KeyError.
JunoEliminar elementos.pop(clave) lanza en un fallo, .pop(clave, default) no, lo que lo convierte en la opción segura. del d[clave] elimina sin devolver nada y lanza en un fallo; .clear() vacía el diccionario pero mantiene el objeto en sí.
JunoEliminar elementos.pop(clave, default) es tu eliminación-y-devolución segura en fallos; del y bracket-pop lanzan en una clave faltante. El que muerde: eliminar mientras iteras lanza RuntimeError. Toma una instantánea con for clave in list(d): primero, luego elimina del original.

Iterar

Tres vistas te permiten iterar a través de diferentes partes de un diccionario. Iterar el diccionario directamente te da las claves. .values() te da los valores. .items() te da ambas a la vez y es lo que usarás más: desempaqueta cada par en dos nombres para bucles limpios y legibles.

.keys(), .values() e .items() devuelven objetos de vista, no listas. Las vistas reflejan el estado actual del diccionario dinámicamente: si modificas el diccionario, la vista se actualiza inmediatamente. .items() es la más útil para la mayoría de bucles porque el desempaquetamiento de tupla for k, v in d.items() se lee claramente.

.keys(), .values() e .items() devuelven objetos de vista (ventanas en vivo al diccionario, no listas separadas). Una vista no copia nada y refleja el estado actual del diccionario, así que si el diccionario cambia, la vista cambia con él. Como las claves son únicas y hashables, la vista de claves soporta álgebra de conjuntos: d.keys() & otro.keys() encuentra claves compartidas, - encuentra la diferencia, que es útil para comparar dos configuraciones. La misma trampa de mutar durante la iteración aplica aquí; si debes cambiar el diccionario mientras iteras, itera sobre una instantánea con list(d.items()).

python
player = {"name": "María", "score": 87, "level": 5}

for key in player:               # iterar claves (más común)
    print(key)

for key in player.keys():        # igual, vista de claves explícita
    print(key)

for value in player.values():    # los valores
    print(value)

for key, value in player.items():   # ambas, más útil
    print(f"{key}: {value}")

.items() es lo que usarás más. Desempaquetar cada par en dos nombres hace el bucle legible.

JunoIterar Itera un diccionario directamente y obtienes sus claves. .values() te da los valores, e .items() te da ambas a la vez, que es la que usarás más. Desempaquetar cada par en dos nombres como for clave, valor in player.items() hizo mis bucles mucho más fáciles de leer.
JunoIterar.keys(), .values() e .items() devuelven vistas en vivo, no listas, así que rastrean el diccionario conforme cambia. .items() con for k, v in d.items() se lee más limpio y es el bucle que escribirás más a menudo.
JunoIterar Las tres vistas son ventanas en vivo, no copias, así que reflejan el diccionario conforme cambia. La vista de claves hace álgebra de conjuntos, así que d.keys() & otro.keys() compara dos diccionarios en una línea. Misma regla que la eliminación: no mutes mientras iteras, toma una instantánea con list(d.items()) si debes.

Verificar membresía

in verifica si una clave existe en el diccionario. No verifica valores, solo claves. Para verificar si algo no está presente, usa not in.

in y not in son O(1) para diccionarios, y solo verifican claves. Para verificar valores, usarías in d.values(), pero eso es O(n) ya que los valores no están indexados.

clave in d encuentra la clave mediante hash y lee una ranura: O(1) en promedio, costo plano sin importar cuán grande sea el diccionario. valor in d.values() tiene que recorrer los valores uno por uno: O(n), el costo crece con el tamaño. Esa asimetría es toda la razón para almacenar lo que buscas como claves en lugar de escanear valores: si te encuentras buscando valores frecuentemente, probablemente quieres que el diccionario esté indexado de otra manera, u otro diccionario mapeando valor de vuelta a clave.

python
player = {"name": "María", "score": 87}

"name"  in player      # True
"lives" in player      # False
"lives" not in player  # True

in solo verifica claves. Para verificar valores, usa in player.values(), aunque eso raramente es necesario.

JunoVerificar membresíain te dice si una clave está en el diccionario, y solo una clave, nunca un valor. Se mantiene rápido sin importar cuán grande sea el diccionario. Cámbialo a not in para verificar que algo está ausente.
JunoVerificar membresíain y not in verifican solo claves, en O(1). Verificar un valor necesita valor in d.values(), que es O(n) porque los valores no están indexados. Así que diseña alrededor de buscar cosas por clave.
JunoVerificar membresíaclave in d es O(1), valor in d.values() es O(n). Esa brecha es la razón de indexar un diccionario en lo que buscas. Si mantienes escaneando valores, tienes el diccionario al revés; añade un mapa inverso en su lugar.

Diccionarios anidados

Los valores pueden ser diccionarios en sí mismos. Así es cómo representas datos estructurados con múltiples niveles: un jugador que tiene una sección de estadísticas, un archivo de configuración con subsecciones. Dos conjuntos de corchetes acceden a un valor anidado: el primero elige la clave externa, el segundo elige la clave interna.

Los diccionarios anidados son diccionarios donde los valores son en sí mismos diccionarios. Accede con subíndices encadenados. Mutar un diccionario interno afecta al diccionario externo porque el diccionario externo mantiene una referencia al mismo objeto. Mantén la anidación poco profunda donde sea posible: la anidación profunda rápidamente se vuelve difícil de leer y navegar.

Un diccionario anidado mantiene referencias a los diccionarios internos, no copias de ellos (una referencia es un puntero a un objeto compartido único, así que dos nombres pueden apuntar al mismo diccionario interno). Aquí es donde copiar muerde: d.copy() es una copia superficial, duplica el diccionario externo pero los diccionarios internos aún se comparten, así que cambiar uno a través de la copia lo cambia en el original también. Cuando necesitas que la copia sea completamente independiente, copy.deepcopy() recorre el árbol completo y duplica cada nivel. Úsalo antes de pasar una configuración a código que podría mutar. Cada paso de corchete es su propia búsqueda O(1), así que la profundidad no cuesta nada en velocidad, solo en legibilidad.

python
users = {
    "maria": {"score": 87, "level": 5},
    "juan": {"score": 74, "level": 3},
}

users["maria"]["score"]   # 87
users["juan"]["level"]     # 3

Accede con corchetes encadenados. Para estructuras profundamente anidadas, esto puede volverse engorroso, así que mantén la anidación poco profunda donde puedas.

JunoDiccionarios anidados Un valor puede ser un diccionario completo en sí mismo, que es cómo sostienes datos estructurados como un jugador con una sección de estadísticas. Dos conjuntos de corchetes alcanzan dentro: el primero elige la clave externa, el segundo elige desde adentro. Útil, pero intento no anidar demasiado profundo o se vuelve complicado de leer.
JunoDiccionarios anidados Los diccionarios anidados son diccionarios cuyos valores son diccionarios; alcanza dentro con corchetes encadenados. El diccionario externo mantiene una referencia al interno, así que cambiar el diccionario interno se ve en todas partes donde se hace referencia. Mantén la anidación poco profunda o se vuelve difícil de navegar.
JunoDiccionarios anidados El diccionario externo almacena referencias a los diccionarios internos, así que .copy() es superficial: la copia comparte esos diccionarios internos y una mutación se filtra a través de ambos. Usa copy.deepcopy() cuando necesites verdadera independencia. La profundidad es gratuita en velocidad, costosa en legibilidad, así que mantente poco profundo.

setdefault

.setdefault() lee una clave si existe, o la establece en un valor por defecto si no existe, y luego devuelve el valor. Es útil cuando necesitas que una clave exista pero no quieres sobrescribirla si ya está allí.

.setdefault(clave, default) es una lectura-o-creación en una llamada: si la clave existe, devuelve su valor actual sin cambiar nada; si no existe, inserta el valor por defecto y lo devuelve. El caso de uso común es construir estructuras agrupadas sin una verificación de existencia separada.

.setdefault(clave, default) es una búsqueda O(1) que lee-o-crea: las claves presentes devuelven su valor existente sin tocar, las claves ausentes obtienen default insertado y devuelto. La trampa que vale la pena conocer: default es un argumento ordinario, así que siempre se construye incluso cuando la clave ya existe. Si ese valor por defecto es costoso de construir (un objeto nuevo, una llamada a función), lo pagas en cada llamada. defaultdict, cubierto a continuación, evita esto ejecutando una fábrica solo en un fallo. Para el patrón cotidiano "agrupar elementos en listas", .setdefault(clave, []).append(...) es el one-liner estándar que reemplaza una verificación clave in d.

python
inventory = {}

inventory.setdefault("arrows", 0)    # establece "arrows": 0, devuelve 0
inventory.setdefault("arrows", 10)   # "arrows" ya existe, sin cambios, devuelve 0

Es útil para construir estructuras agrupadas sin verificar la existencia de la clave primero:

python
groups = {}

for name, team in players:
    groups.setdefault(team, []).append(name)
Junosetdefault.setdefault(clave, default) lee una clave si está allí, o la establece en el valor por defecto y la devuelve si no está. Es la forma ordenada de asegurarse de que una clave existe sin clobbear un valor que ya está dentro. La uso más para agrupar cosas, sin necesidad de verificar "¿está esta clave aquí aún?".
Junosetdefault.setdefault(clave, default) devuelve un valor existente sin cambios, o inserta el valor por defecto y lo devuelve en un fallo. El beneficio es agrupar: d.setdefault(k, []).append(x) construye listas por clave sin una verificación de existencia separada.
Junosetdefault Una lectura-o-creación O(1), pero el valor por defecto siempre se construye incluso en un acierto, así que un valor por defecto costoso te cuesta en cada llamada. Eso es exactamente lo que la fábrica on-miss de defaultdict arregla. Para agrupar, .setdefault(k, []).append(x) es el one-liner que retira la verificación clave in d.

collections.defaultdict y Counter

La biblioteca estándar tiene dos subclases de diccionario que manejan patrones comunes automáticamente. defaultdict crea un valor por defecto para claves faltantes para que nunca obtengas un KeyError. Counter cuenta cuántas veces aparece cada elemento en una secuencia y te da los resultados como un diccionario.

defaultdict toma un callable que produce el valor por defecto para nuevas claves, eliminando la necesidad de .setdefault(). Counter es un diccionario especializado para contar frecuencias con un método .most_common(). Ambas son subclases de diccionario, así que todas las operaciones estándar de diccionario funcionan en ellas.

defaultdict(factory) ejecuta la fábrica (un callable sin argumentos, como list o int) la primera vez que lees una clave faltante, almacena el resultado y lo devuelve, así que d[k] nunca lanza KeyError. Esa es la diferencia clave de .setdefault(): la fábrica solo se ejecuta en un fallo, así que un valor por defecto costoso no cuesta nada en la ruta común. Una trampa a señalar: como una lectura ordinaria de una clave faltante la crea, simplemente verificar d[k] puede hacer crecer el diccionario, así que usa k in d cuando solo quieras verificar, no insertar. Counter es un dict construido para tabulación: aliméntalo con una secuencia y cuenta ocurrencias, y .most_common(n) devuelve el top n por recuento. Ambas viven en el módulo collections.

defaultdict y Counter viven en la biblioteca estándar, así que necesitan ser importadas primero. Las importaciones reciben tratamiento completo en el capítulo Módulos.

python
from collections import defaultdict

groups = defaultdict(list)
for name, team in players:
    groups[team].append(name)   # sin KeyError si team es nuevo
python
from collections import Counter

words = ["cat", "dog", "cat", "bird", "cat", "dog"]
counts = Counter(words)
# Counter({'cat': 3, 'dog': 2, 'bird': 1})

counts.most_common(2)   # [('cat', 3), ('dog', 2)]

Counter ahorra mucho boilerplate de "contar cosas en un bucle".

Junocollections.defaultdict y Counterdefaultdict proporciona un valor por defecto para cualquier clave faltante, así que agrupar y contar dejan de lanzarte KeyError. Counter cuenta cuántas veces aparece cada elemento en una secuencia y te lo devuelve como un diccionario. La primera vez que cambié un bucle de conteo escrito a mano por Counter, la mitad de mi código desapareció.
Junocollections.defaultdict y Counterdefaultdict(list) o defaultdict(int) auto-crea el valor por defecto en una clave nueva, retirando .setdefault() para agrupar y contar. Counter es un diccionario sintonizado para frecuencias, con .most_common(n) incorporado. Ambas son subclases de diccionario, así que todo lo que sabes sigue funcionando.
Junocollections.defaultdict y Counter La fábrica de defaultdict solo se ejecuta en un fallo, así que a diferencia de .setdefault() un valor por defecto costoso no cuesta nada en la ruta común. La trampa: leer una clave faltante la crea, así que verifica con k in d, no d[k]. Counter tabula una secuencia y .most_common(n) extrae el top n.

En la práctica

Construir un rastreador de puntuación e imprimir un resumen con todas las entradas:

python
scores = {"María": 87, "Juan": 74, "Carol": 92, "David": 55}

total = sum(scores.values())
average = total / len(scores)

print(f"Jugadores:  {len(scores)}")
print(f"Promedio:  {average:.1f}")
print(f"Más alto:  {max(scores.values())}")
print(f"Más bajo:   {min(scores.values())}")
print()

for name, score in scores.items():
    print(f"  {name}: {score}")

Construir un diccionario de resultados por archivo en un bucle, luego resumir a través de todas las entradas:

python
job_results = {}
files = ["report_jan.csv", "report_feb.csv", "report_mar.csv"]

for filename in files:
    size = len(filename) * 100   # placeholder para el tamaño real del archivo
    if size < 2000:
        status = "ok"
    else:
        status = "large"
    job_results[filename] = {"size": size, "status": status}

ok_count = 0
large_count = 0

for result in job_results.values():
    if result["status"] == "ok":
        ok_count += 1
    else:
        large_count += 1

print(f"Procesados {len(job_results)} archivo(s): {ok_count} ok, {large_count} grande")

Validar un diccionario de solicitud anidado iterando campos obligatorios, luego normalizar un diccionario de importancia de características en su lugar:

python
request = {
    "method": "POST",
    "path": "/users",
    "headers": {"Content-Type": "application/json"},
    "body": {"username": "maria", "email": "maria@example.com"},
}

body = request["body"]
errors = []

for field in ["username", "email"]:
    if not body.get(field):
        errors.append(f"Campo obligatorio faltante: {field}")

if "email" in body and "@" not in body["email"]:
    errors.append("Formato de correo electrónico inválido")

print(f"Método: {request['method']} {request['path']}")
if errors:
    print(f"Errores: {errors}")
else:
    print("Validación pasada")

# Normalizar valores de importancia de características para sumar a 1
feature_importance = {"age": 0.34, "income": 0.28, "region": 0.15, "purchases": 0.23}
total = sum(feature_importance.values())

for key in feature_importance:
    feature_importance[key] = round(feature_importance[key] / total, 3)

print(f"Normalizado: {feature_importance}")