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

Diccionarios ​

Las listas te permiten buscar cosas por posición. Pero muchas veces quieres buscar algo por nombre. No es "dame el elemento 3", sino "dame el puntaje de Alicia". Un diccionario guarda datos como pares clave-valor: buscas un valor por su clave, no por su posición.

Cuando el índice posicional de una lista no tiene un significado real, un diccionario es la estructura adecuada. Los dicts asocian claves arbitrarias con valores, dándote búsqueda por nombre en tiempo O(1). Una tabla de puntajes, una respuesta JSON, un archivo de configuración: todos se expresan naturalmente como mapeos clave-valor.

Un dict es un almacén de clave-valor con búsqueda, inserción y eliminación en O(1) promedio (O(1) significa que el costo se mantiene constante sin importar cuánto crezca el dict, porque las claves se encuentran mediante hashing, que convierte la clave en un número que apunta directo a su ranura). Las claves deben ser hasheables, es decir, su contenido nunca cambia para que ese número se mantenga estable: str, int y tuple cumplen esa condición, list no. Los valores pueden ser cualquier objeto. Desde Python 3.7, los dicts conservan las claves en el orden en que las insertaste, algo que importa cada vez que serializas o muestras uno. Esta misma estructura impulsa buena parte de Python por debajo, incluyendo los atributos de tus propios objetos, así que vale la pena conocer los dicts a fondo. El capítulo de Listas cubre a su primo basado en posición.

Crear un diccionario ​

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

Los literales de dict usan llaves con sintaxis clave: valor. Las claves pueden ser de cualquier tipo inmutable (hasheable): strings, enteros, tuplas. Los valores pueden ser cualquier objeto de Python. Los dicts preservan el orden de inserción, así que al iterar obtienes los elementos en el orden en que se agregaron.

Los literales de dict se evalúan de izquierda a derecha. Las claves deben ser hasheables (su contenido no puede cambiar, para que el número bajo el cual Python las archiva se mantenga fijo): str, int y tuple cumplen esa condición, list y dict no. Los valores no tienen esa restricción. El orden de inserción forma parte de la garantía del lenguaje desde Python 3.7, así que la iteración y la serialización son reproducibles. Una trampa silenciosa: una clave duplicada en un mismo literal no lanza error, simplemente conserva el último valor y descarta el anterior.

python
jugador = {
    "nombre": "Alicia",
    "puntaje": 87,
    "nivel": 5,
    "vivo": 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 agregas las cosas es el orden en que las recuperas, algo en lo que me apoyo más de lo que esperaba.
JunoCrear un diccionario Llaves con pares clave: valor. Las claves pueden ser de cualquier tipo inmutable (str, int, tuple), los valores pueden ser cualquier objeto. La iteración sigue el orden de inserción, así que un dict funciona bien cuando el orden importa.
JunoCrear un diccionario Las claves tienen que ser hasheables, así que str, int y tuple entran, list y dict quedan fuera. El orden de inserción está garantizado desde la versión 3.7, así que puedes confiar en él para la salida. Cuidado con el literal de clave duplicada: toma el último valor en silencio, sin advertencia.

Acceder a los 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á presente: devuelve None en lugar de fallar, o un valor por defecto que tú especifiques.

El acceso con corchetes lanza KeyError si la clave no existe. .get(key) devuelve None si no la encuentra. .get(key, default) devuelve ese valor por defecto en su lugar. Usa .get() siempre que la presencia de la clave sea incierta; es más seguro y más legible que envolver el acceso en un try/except.

El acceso con corchetes busca la clave calculando su hash y leyendo la ranura correspondiente, lo cual es O(1) en promedio (costo constante sin importar el tamaño). Si no la encuentra, lanza KeyError. .get(key, default=None) hace la misma búsqueda pero devuelve el valor por defecto en lugar de fallar, así que es la opción correcta cuando la presencia de una clave es incierta. Para verificar antes de un acceso con corchetes, key in d también es O(1); elige in cuando solo necesitas un sí o no, y .get() cuando quieres el valor o un valor de respaldo en un solo paso.

python
jugador = {"nombre": "Alicia", "puntaje": 87}

jugador["nombre"]    # "Alicia"
jugador["puntaje"]   # 87
jugador["vidas"]     # KeyError (la clave no existe)
python
jugador.get("puntaje")        # 87
jugador.get("vidas")          # None (sin error, devuelve None por defecto)
jugador.get("vidas", 3)       # 3   (usa este valor por defecto si la clave no está)

.get() es más seguro siempre que una clave pueda faltar:

python
cantidad = inventario.get("flechas", 0)   # 0 si "flechas" no está en el dict
JunoAcceder a los valoresd["clave"] te entrega el valor, pero lanza KeyError si esa clave no existe. Cuando no estés seguro, recurre a .get(): devuelve None en lugar de fallar, o un valor por defecto que tú pases. Ahora le pongo un valor por defecto a todo y mis programas dejaron de fallar por claves faltantes.
JunoAcceder a los valores El acceso con corchetes lanza KeyError si no encuentra la clave, .get(key) devuelve None, y .get(key, default) devuelve lo que tú pases. Usa .get() cuando una clave podría estar ausente: es más limpio que envolver el acceso en un try/except.
JunoAcceder a los valoresd[key] y .get() hacen la misma búsqueda O(1), la diferencia está solo en qué pasa si no encuentra nada: fallar o devolver un valor por defecto. Usa key in d cuando quieras un sí o no, y .get() cuando quieras el valor o un respaldo en un solo paso.

Agregar y actualizar ​

Asigna a una clave usando corchetes. Si la clave ya existe, se reemplaza el valor. Si no existe todavía, se crea una entrada nueva. Usa .update() para combinar todo un diccionario dentro de otro de una sola vez.

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

d[key] = value calcula el hash de la clave e inserta o sobrescribe en un solo paso O(1) promedio, así que no existen un "agregar" y un "actualizar" separados: la asignación hace ambas cosas. .update(other) es la misma operación aplicada a cada entrada de other, sobrescribiendo cualquier clave compartida. Para combinar sin modificar el original, el operador | (Python 3.9+) devuelve un dict nuevo y deja intactas ambas entradas, mientras que |= modifica en el lugar como .update(). Usa | cuando una función no deba modificar un dict que su llamador todavía conserva.

python
jugador = {"nombre": "Alicia", "puntaje": 87}

jugador["puntaje"] = 92      # actualiza uno existente
jugador["nivel"] = 5         # agrega una clave nueva
python
extras = {"nivel": 5, "vivo": True}
jugador.update(extras)   # agrega/sobrescribe con las claves de extras
JunoAgregar y actualizar Asigna a una clave con corchetes: si ya está ahí, el valor se reemplaza; si no, aparece una entrada nueva. Una sola sintaxis cubre ambos casos, sin necesidad de verificar antes. Usa .update() para incorporar todo un diccionario de una sola vez.
JunoAgregar y actualizard[key] = value crea o reemplaza en un solo paso, así que no hay una distinción entre agregar y actualizar. .update(other) combina un dict completo, sobrescribiendo las claves compartidas. Ambas operaciones son O(1) por clave.
JunoAgregar y actualizar La asignación inserta o sobrescribe en un solo paso O(1), y .update() es eso repetido por cada entrada. Cuando no quieras tocar el original, | devuelve un dict nuevo combinado, mientras que |= modifica en el lugar. Usa | dentro de una función para no cambiar sin querer un dict que el llamador todavía conserva.

Eliminar elementos ​

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

.pop(key) lanza KeyError si no encuentra la clave. .pop(key, default) devuelve el valor por defecto en su lugar, lo que lo convierte en el método seguro para eliminar. del d[key] elimina la clave sin devolver nada y lanza KeyError si no la encuentra. .clear() elimina todas las entradas pero conserva el objeto dict.

.pop(key, default) elimina y devuelve en una sola búsqueda O(1) promedio, y el valor por defecto lo hace seguro ante claves faltantes; sin él, una clave ausente lanza KeyError. del d[key] elimina sin devolver nada y falla si no encuentra la clave. .clear() vacía el dict pero conserva el mismo objeto, así que cualquier otro nombre que apunte a él también verá un dict vacío. El fallo que hay que recordar: modificar un dict 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 key in list(d):, y luego eliminar del original.

python
jugador = {"nombre": "Alicia", "puntaje": 87, "nivel": 5}

jugador.pop("nivel")            # elimina "nivel" y devuelve 5
jugador.pop("vidas", None)      # pop seguro, devuelve None si la clave no está
del jugador["puntaje"]          # elimina "puntaje", sin valor de retorno
jugador.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(key) elimina una clave y te entrega su valor de vuelta, y .pop(key, None) se mantiene tranquilo cuando la clave podría no estar. del d[key] elimina sin devolver nada, .clear() vacía todo. El valor por defecto en .pop() me salvó de muchas sorpresas con KeyError.
JunoEliminar elementos.pop(key) falla si no encuentra la clave, .pop(key, default) no lo hace, lo que lo convierte en la opción segura. del d[key] elimina sin devolver nada y falla si no encuentra la clave; .clear() vacía el dict pero conserva el objeto en sí.
JunoEliminar elementos.pop(key, default) es tu eliminar-y-devolver seguro ante claves faltantes; del y el pop con corchetes fallan si la clave no está. Lo que muerde: eliminar mientras iteras lanza RuntimeError. Toma una instantánea con for key in list(d): primero, y luego elimina del original.

Iterar ​

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

.keys(), .values() y .items() devuelven objetos de vista, no listas. Las vistas reflejan el estado actual del dict de forma dinámica: si modificas el dict, la vista se actualiza de inmediato. .items() es la más útil para la mayoría de los bucles porque el desempaquetado de tuplas for k, v in d.items() se lee con claridad.

.keys(), .values() y .items() devuelven objetos de vista (ventanas activas hacia el dict, no listas separadas). Una vista no copia nada y refleja el estado actual del dict, así que si el dict cambia, la vista cambia con él. Como las claves son únicas y hasheables, la vista de claves soporta álgebra de conjuntos: d.keys() & other.keys() encuentra las claves compartidas, - encuentra la diferencia, lo cual es útil para comparar dos configuraciones. La misma trampa de modificar mientras se itera aplica aquí; si necesitas cambiar el dict dentro del bucle, itera sobre una instantánea con list(d.items()).

python
jugador = {"nombre": "Alicia", "puntaje": 87, "nivel": 5}

for clave in jugador:               # itera las claves (lo más común)
    print(clave)

for clave in jugador.keys():        # lo mismo, con la vista explícita de claves
    print(clave)

for valor in jugador.values():      # los valores
    print(valor)

for clave, valor in jugador.items():   # ambos, lo más útil
    print(f"{clave}: {valor}")

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

JunoIterar Recorre un dict directamente y obtienes sus claves. .values() te da los valores, y .items() te da ambos a la vez, que es lo que más usarás. Desempaquetar cada par en dos nombres como for key, value in player.items() hizo que mis bucles fueran mucho más fáciles de leer.
JunoIterar.keys(), .values() y .items() devuelven vistas activas, no listas, así que rastrean el dict a medida que cambia. .items() con for k, v in d.items() se lee más limpio y es el bucle que escribirás con más frecuencia.
JunoIterar Las tres vistas son ventanas activas, no copias, así que reflejan el dict a medida que cambia. La vista de claves hace álgebra de conjuntos, así que d.keys() & other.keys() compara dos dicts en una línea. La misma regla que al eliminar: no modifiques mientras iteras, toma una instantánea con list(d.items()) si es necesario.

Verificar la pertenencia ​

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

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

key in d encuentra la clave calculando su hash y leyendo una sola ranura: O(1) en promedio, constante sin importar el tamaño del dict. value in d.values() tiene que recorrer los valores uno por uno: O(n), el costo crece con el tamaño. Esa asimetría es la razón principal para guardar lo que buscas como claves en lugar de escanear valores: si te encuentras buscando valores con frecuencia, probablemente necesitas invertir las claves del dict, o un segundo dict que mapee el valor de vuelta a la clave.

python
jugador = {"nombre": "Alicia", "puntaje": 87}

"nombre" in jugador      # True
"vidas"  in jugador      # False
"vidas"  not in jugador  # True

in solo verifica claves. Para verificar valores, usa in jugador.values(), aunque rara vez se necesita.

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

Diccionarios anidados ​

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

Los diccionarios anidados son dicts cuyos valores son a su vez dicts. Se accede con subíndices encadenados. Modificar un dict interno afecta al dict externo porque el dict externo guarda una referencia al mismo objeto. Mantén la anidación superficial cuando sea posible: anidar demasiado rápidamente se vuelve difícil de leer y navegar.

Un dict anidado guarda referencias a los dicts internos, no copias de ellos (una referencia es un puntero a un objeto compartido, así que dos nombres pueden apuntar al mismo dict interno). Aquí es donde copiar sorprende: d.copy() es una copia superficial, duplica el dict externo pero los dicts internos siguen siendo compartidos, así que cambiar uno a través de la copia también lo cambia en el original. Cuando necesites que la copia sea completamente independiente, copy.deepcopy() recorre todo el árbol y duplica cada nivel. Úsalo antes de pasarle una configuración a un código que podría modificarla. Cada paso con corchetes es su propia búsqueda O(1), así que la profundidad no cuesta nada en velocidad, solo en legibilidad.

python
usuarios = {
    "alicia": {"puntaje": 87, "nivel": 5},
    "beto": {"puntaje": 74, "nivel": 3},
}

usuarios["alicia"]["puntaje"]   # 87
usuarios["beto"]["nivel"]       # 3

Se accede con corchetes encadenados. Para estructuras muy anidadas, esto puede volverse poco manejable, así que mantén la anidación superficial cuando puedas.

JunoDiccionarios anidados Un valor puede ser todo un diccionario propio, que es como guardas datos estructurados como un jugador con una sección de estadísticas. Dos pares de corchetes te llevan hasta ahí: el primero elige la clave externa, el segundo elige dentro de ella. Es útil, pero trato de no anidar demasiado o se vuelve complicado de leer.
JunoDiccionarios anidados Los diccionarios anidados son dicts cuyos valores son dicts; se accede con corchetes encadenados. El dict externo guarda una referencia al interno, así que cambiar el dict interno se refleja en todos los lugares donde se referencia. Mantén la anidación superficial o se vuelve difícil de navegar.
JunoDiccionarios anidados El dict externo guarda referencias a los dicts internos, así que .copy() es superficial: la copia comparte esos dicts internos y una modificación se filtra en ambos lados. Usa copy.deepcopy() cuando necesites independencia real. La profundidad es gratis en velocidad, costosa en legibilidad, así que mantente superficial.

setdefault ​

.setdefault() lee una clave si existe, o la fija a 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á ahí.

.setdefault(key, default) es un leer-o-crear en una sola 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(key, default) es una sola búsqueda O(1) que lee-o-crea: las claves presentes devuelven su valor existente sin tocarlo, las claves ausentes reciben el default insertado y devuelto. El detalle que conviene saber: default es un argumento común, 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), pagas ese costo en cada llamada. defaultdict, que veremos a continuación, evita esto ejecutando una fábrica solo cuando falta la clave. Para el patrón cotidiano de "agrupar elementos en listas", .setdefault(key, []).append(...) es la línea estándar que reemplaza una verificación con key in d.

python
inventario = {}

inventario.setdefault("flechas", 0)    # fija "flechas": 0, devuelve 0
inventario.setdefault("flechas", 10)   # "flechas" ya existe, no cambia, devuelve 0

Es útil para construir estructuras agrupadas sin verificar antes si la clave existe:

python
grupos = {}

for nombre, equipo in jugadores:
    grupos.setdefault(equipo, []).append(nombre)
Junosetdefault.setdefault(key, default) lee una clave si está ahí, o la fija al valor por defecto y lo devuelve si no lo está. Es la forma ordenada de asegurar que una clave exista sin arruinar un valor que ya estaba ahí. Lo uso más que nada para agrupar cosas, sin necesidad de verificar "¿ya está esta clave?".
Junosetdefault.setdefault(key, default) devuelve un valor existente sin cambiarlo, o inserta el valor por defecto y lo devuelve si falta. La ganancia está en agrupar: d.setdefault(k, []).append(x) construye listas por clave sin una verificación de existencia separada.
Junosetdefault Un solo leer-o-crear O(1), pero el valor por defecto siempre se construye incluso cuando la clave ya existe, así que un valor por defecto costoso te cuesta en cada llamada. Eso es exactamente lo que soluciona la fábrica de defaultdict, que solo se activa ante una ausencia. Para agrupar, .setdefault(k, []).append(x) es la línea que jubila la verificación key in d.

collections.defaultdict y Counter ​

La biblioteca estándar tiene dos subclases de dict que manejan patrones comunes automáticamente. defaultdict crea un valor por defecto para las 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 dict.

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

defaultdict(factory) ejecuta la fábrica (un invocable sin argumentos, como list o int) la primera vez que lees una clave faltante, guarda el resultado y lo devuelve, así que d[k] nunca lanza KeyError. Esa es la diferencia clave con .setdefault(): la fábrica solo se activa ante una ausencia, así que un valor por defecto costoso no cuesta nada en el camino común. Una trampa que vale la pena señalar: como una simple lectura de una clave faltante la crea, con solo verificar d[k] puede crecer el dict, así que usa k in d cuando solo quieras comprobar, no insertar. Counter es un dict construido para contar: le pasas una secuencia y cuenta las repeticiones, y .most_common(n) devuelve los n primeros por cantidad. Ambas viven en el módulo collections.

defaultdict y Counter viven en la biblioteca estándar, así que hay que importarlas primero. Los imports reciben tratamiento completo en el capítulo de Módulos.

python
from collections import defaultdict

grupos = defaultdict(list)
for nombre, equipo in jugadores:
    grupos[equipo].append(nombre)   # sin KeyError aunque equipo sea nuevo
python
from collections import Counter

palabras = ["gato", "perro", "gato", "pajaro", "gato", "perro"]
conteos = Counter(palabras)
# Counter({'gato': 3, 'perro': 2, 'pajaro': 1})

conteos.most_common(2)   # [('gato', 3), ('perro', 2)]

Counter ahorra mucho código repetitivo de "contar cosas en un bucle".

Junocollections.defaultdict y Counterdefaultdict rellena 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 dict. 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) crea automáticamente el valor por defecto ante una clave nueva, jubilando a .setdefault() para agrupar y contar. Counter es un dict ajustado para frecuencias, con .most_common(n) incorporado. Ambos son subclases de dict, así que todo lo que ya sabes sigue funcionando.
Junocollections.defaultdict y Counter La fábrica de defaultdict se activa solo ante una ausencia, así que, a diferencia de .setdefault(), un valor por defecto costoso no cuesta nada en el camino común. La trampa: leer una clave faltante la crea, así que verifica con k in d, no con d[k]. Counter cuenta una secuencia y .most_common(n) extrae los n primeros.

En la práctica ​

Construyendo un rastreador de puntajes e imprimiendo un resumen con todas las entradas:

python
puntajes = {"Alicia": 87, "Beto": 74, "Carla": 92, "Diego": 55}

total = sum(puntajes.values())
promedio = total / len(puntajes)

print(f"Jugadores: {len(puntajes)}")
print(f"Promedio:  {promedio:.1f}")
print(f"Mayor:     {max(puntajes.values())}")
print(f"Menor:     {min(puntajes.values())}")
print()

for nombre, puntaje in puntajes.items():
    print(f"  {nombre}: {puntaje}")

Construyendo un dict de resultados por archivo en un bucle, y luego resumiendo todas las entradas:

python
resultados_trabajo = {}
archivos = ["reporte_ene.csv", "reporte_feb.csv", "reporte_mar.csv"]

for nombre_archivo in archivos:
    tamano = len(nombre_archivo) * 100   # marcador de posición para un tamaño real de archivo
    if tamano < 2000:
        estado = "ok"
    else:
        estado = "grande"
    resultados_trabajo[nombre_archivo] = {"tamano": tamano, "estado": estado}

contador_ok = 0
contador_grande = 0

for resultado in resultados_trabajo.values():
    if resultado["estado"] == "ok":
        contador_ok += 1
    else:
        contador_grande += 1

print(f"Se procesaron {len(resultados_trabajo)} archivo(s): {contador_ok} ok, {contador_grande} grandes")

Validando un dict de solicitud anidado al iterar los campos requeridos, y luego normalizando en el lugar un dict de importancia de características:

python
solicitud = {
    "metodo": "POST",
    "ruta": "/usuarios",
    "encabezados": {"Content-Type": "application/json"},
    "cuerpo": {"usuario": "alicia", "email": "[email protected]"},
}

cuerpo = solicitud["cuerpo"]
errores = []

for campo in ["usuario", "email"]:
    if not cuerpo.get(campo):
        errores.append(f"Falta el campo requerido: {campo}")

if "email" in cuerpo and "@" not in cuerpo["email"]:
    errores.append("Formato de email inválido")

print(f"Método: {solicitud['metodo']} {solicitud['ruta']}")
if errores:
    print(f"Errores: {errores}")
else:
    print("Validación exitosa")

# Normalizar los valores de importancia de características para que sumen 1
importancia_caracteristicas = {"edad": 0.34, "ingreso": 0.28, "region": 0.15, "compras": 0.23}
total = sum(importancia_caracteristicas.values())

for clave in importancia_caracteristicas:
    importancia_caracteristicas[clave] = round(importancia_caracteristicas[clave] / total, 3)

print(f"Normalizado: {importancia_caracteristicas}")