Datos anidados — una lista de diccionarios
La forma en que llegan los datos reales: diccionarios dentro de listas, listas dentro de diccionarios. Cómo meterse en ellos, agruparlos y por qué copy() no basta aquí.
¿Cómo quieres aprender?
El problema que vamos a resolver
Los últimos capítulos enseñaron cuatro contenedores — listas, tuplas, diccionarios y conjuntos. Cada ejemplo usó uno de ellos, un solo nivel de profundidad.
Los datos reales no llegan así. Una lista de pedidos se ve así:
customer item quantity price
rafi pen 3 15.0
ahmed bag 1 850.0Hay dos niveles aquí. Fuera, una colección — el número de pedidos no se conoce, así que una lista. Dentro, cada pedido es un registro y cada parte de él tiene un nombre — así que un diccionario.
Lo que devuelve una API, lo que obtienes al leer un CSV, lo que devuelve una base de datos: casi siempre esta forma, una lista de diccionarios. El capítulo de hoy no introduce ningún contenedor nuevo. Se trata de meter los que ya tienes dentro de otros, y de reconocer los errores que salen de ahí.
Al terminar este capítulo podrás
- Meterse en una lista de diccionarios y recorrerla
- Leer un
KeyErroroIndexErrorprofundo y decir en qué nivel está el fallo - Agrupar y resumir una lista de diccionarios
- Manejar registros incompletos con el valor por defecto de
.get() - Explicar por qué
copy()no basta para los datos anidados
Requisitos previos: Conjuntos — una colección sin duplicados.
Reconocer la forma
orders = [
{"customer": "rafi", "item": "pen", "quantity": 3, "price": 15.0},
{"customer": "ahmed", "item": "bag", "quantity": 1, "price": 850.0},
]
print(len(orders))
print(orders[0])
print(orders[0]["customer"])
print(orders[1]["price"])2
{'customer': 'rafi', 'item': 'pen', 'quantity': 3, 'price': 15.0}
rafi
850.0La línea orders[0]["customer"] es la habilidad central de este capítulo, y se lee de izquierda a derecha, un paso a la vez:
orders— una listaorders[0]— su primer elemento, que es un diccionarioorders[0]["customer"]— el valorcustomerde ese diccionario
Cuando dudes, detente e imprime el paso intermedio. Escribir print(orders[0]) para ver que es un diccionario es un hábito que elimina la mitad de los problemas con los datos anidados.
Recorrerla con un bucle
orders = [
{"customer": "rafi", "item": "pen", "quantity": 3, "price": 15.0},
{"customer": "ahmed", "item": "bag", "quantity": 1, "price": 850.0},
]
for order in orders:
total = order["quantity"] * order["price"]
print(f"{order['customer']:<8} {order['item']:<6} {total:>8.2f}")rafi pen 45.00
ahmed bag 850.00for order in orders te da un diccionario cada vuelta, y dentro de él accedes a las cosas por nombre. No hay aritmética de índices en ninguna parte.
Comillas dentro de una f-string. f"{order['customer']}" usa comillas simples dentro porque la f-string misma está escrita con comillas dobles. Si usas el mismo tipo dentro y fuera, Python no puede saber dónde termina el texto. Mantén un tipo dentro y el otro fuera.Registros incompletos
En los datos reales, no todos los registros tienen todas las claves:
orders = [{"customer": "rafi"}]
print(orders[0]["discount"])KeyError: 'discount'El KeyError dice que falta la clave, pero no qué registro es el que no la tiene. Cuando eso pasa en uno de doce mil pedidos, cuesta mucho encontrarlo. Recorrer con enumerate te da el número de cada registro, así que puedes imprimir el que no tiene la clave:
orders = [
{"customer": "rafi", "discount": 5},
{"customer": "ahmed", "discount": 0},
{"customer": "dia"},
]
for i, order in enumerate(orders):
if "discount" not in order:
print("record", i, "has no discount:", order)record 2 has no discount: {'customer': 'dia'}Ahora sabes que el problema está en el registro 2 (la cuenta empieza en cero) y cómo se ve ese registro.
Cuando es normal que una clave falte, .get():
orders = [{"customer": "rafi"}]
print(orders[0].get("discount", 0))0La regla del capítulo quince aplica aquí también: corchetes cuadrados cuando debería estar ahí, .get() cuando su ausencia es normal. La tentación es más fuerte con los datos anidados, porque .get() puede silenciar cualquier error — y entonces los datos incompletos se convierten en cero en silencio y entran en tus cálculos.
La otra forma — listas dentro de un diccionario
by_customer = {
"rafi": ["pen", "ink"],
"ahmed": ["bag"],
}
by_customer["rafi"].append("bottle")
by_customer["dia"] = ["eraser"]
print(by_customer)
for customer, items in by_customer.items():
print(f"{customer}: {len(items)} item(s) - {', '.join(items)}"){'rafi': ['pen', 'ink', 'bottle'], 'ahmed': ['bag'], 'dia': ['eraser']}
rafi: 3 item(s) - pen, ink, bottle
ahmed: 1 item(s) - bag
dia: 1 item(s) - eraserLa línea by_customer["rafi"].append("bottle") son dos pasos en uno: by_customer["rafi"] saca una lista, y luego append añade a esa lista.
Esta forma resuelve un problema del capítulo quince, donde una clave solo podía existir una vez y por eso una persona no podía tener varios registros. Haz que el valor sea una lista y ya puede.
Agrupar — el patrón más útil
Convertir una lista de diccionarios en «quién compró qué»:
orders = [
{"customer": "rafi", "item": "pen"},
{"customer": "ahmed", "item": "bag"},
{"customer": "rafi", "item": "ink"},
]
grouped = {}
for order in orders:
customer = order["customer"]
if customer not in grouped:
grouped[customer] = []
grouped[customer].append(order["item"])
print(grouped){'rafi': ['pen', 'ink'], 'ahmed': ['bag']}Las tres líneas de en medio son toda la técnica: si la clave no existe, pon ahí una lista vacía primero, y luego añade sin preocuparte. Sin eso, el primer pedido lanza un KeyError.
Lo mismo cabe en una sola línea:
orders = [
{"customer": "rafi", "item": "pen"},
{"customer": "rafi", "item": "ink"},
]
grouped = {}
for order in orders:
grouped.setdefault(order["customer"], []).append(order["item"])
print(grouped){'rafi': ['pen', 'ink']}setdefault(key, []) dice «dame el valor de esta clave, y si no tiene ninguno, pon ahí esta lista vacía y luego dámela» — y a lo que devuelva se le hace append. La diferencia con .get() importa: .get() no pone nada en el diccionario, setdefault sí.
La forma de tres líneas se lee más fácil, la de una línea se escribe más rápido. Las dos encontrarás en código real.
Aún más profundo
shop = {
"name": "Corner Store",
"staff": [
{"name": "rafi", "shifts": ["mon", "tue"]},
{"name": "dia", "shifts": ["wed"]},
],
}
print(shop["staff"][0]["shifts"][1])
print(len(shop["staff"]))
for person in shop["staff"]:
print(f"{person['name']}: {len(person['shifts'])} shift(s)")tue
2
rafi: 2 shift(s)
dia: 1 shift(s)shop["staff"][0]["shifts"][1] son cuatro pasos: diccionario → lista → diccionario → lista. Leído de izquierda a derecha no es complicado, solo largo.
Pasados tres niveles, detente y piensa. Romper la cadena y dar un nombre al paso intermedio hace que el código se lea mejor:
first_person = shop["staff"][0]
print(first_person["shifts"][1])copy() no basta para los datos anidados
El capítulo doce mostró que b = a deja dos nombres en una sola lista, y que copy() es la solución. Con datos anidados, copy() protege solo un nivel:
import copy
template = {"name": "", "items": []}
a = template.copy()
b = copy.deepcopy(template)
a["items"].append("pen")
b["items"].append("bag")
print(template)
print(a)
print(b){'name': '', 'items': ['pen']}
{'name': '', 'items': ['pen']}
{'name': '', 'items': ['bag']}a se creó con copy() — un diccionario exterior nuevo, pero la lista interior es la misma lista. Así que a["items"].append("pen") cambió también el template original.
b se creó con deepcopy(), que copia también todo lo de dentro. Así que los cambios en b no se propagan a ningún otro sitio.
copy es un módulo, así que necesita import copy antes de usarse — los módulos se ven a fondo en el capítulo veintidós.
¿Cuándo necesitasdeepcopy? Cuando estás haciendo una copia de una estructura anidada para cambiarla, y quieres que el original quede intacto. Ten en cuenta quedeepcopyes lento, y la mayoría de las veces no hace falta — la mayor parte del código construye datos nuevos en lugar de modificar los existentes.
Un ejemplo completo
orders.py:
# The shape data actually arrives in: a list of records
orders = [
{"customer": "rafi", "item": "pen", "quantity": 3, "price": 15.0},
{"customer": "ahmed", "item": "bag", "quantity": 1, "price": 850.0},
{"customer": "rafi", "item": "ink", "quantity": 2, "price": 120.0},
{"customer": "dia", "item": "pen", "quantity": 5, "price": 15.0},
]
print("Customer Item Qty Total")
grand_total = 0.0
for order in orders:
total = order["quantity"] * order["price"]
grand_total = grand_total + total
print(f"{order['customer']:<9} {order['item']:<6} {order['quantity']:>3} {total:>10.2f}")
print(f"{'':<20}{grand_total:>10.2f}")
print()
# Group the spend by customer: the key may not exist yet, so .get supplies a start
spend = {}
for order in orders:
customer = order["customer"]
spend[customer] = spend.get(customer, 0.0) + order["quantity"] * order["price"]
# Sort by amount by putting the amount first in a tuple
pairs = []
for customer, amount in spend.items():
pairs.append((amount, customer))
pairs.sort(reverse=True)
print("Spend per customer, highest first:")
for amount, customer in pairs:
print(f" {customer:<8} {amount:>8.2f}")
distinct = set()
for order in orders:
distinct.add(order["item"])
print()
print("Distinct items:", sorted(distinct))
print("Best customer :", pairs[0][1])Customer Item Qty Total
rafi pen 3 45.00
ahmed bag 1 850.00
rafi ink 2 240.00
dia pen 5 75.00
1210.00
Spend per customer, highest first:
ahmed 850.00
rafi 285.00
dia 75.00
Distinct items: ['bag', 'ink', 'pen']
Best customer : ahmedEste pequeño programa usa los cuatro contenedores de los últimos capítulos, cada uno en el sitio que le corresponde.
Una lista para los pedidos, porque el número se desconoce y el orden importa.
Diccionarios para cada pedido, porque cada parte necesita un nombre; y para spend, porque acumula por nombre.
Tuplas para cada par en pairs. Y aquí hay un truco: la cantidad se pone primero en la tupla, porque Python ordena las tuplas por su primer valor. Así que un .sort(reverse=True) normal ordena por cantidad y no hace falta key=.
Un conjunto para distinct, porque la pregunta era «cuántos distintos», y pen aparece dos veces.
Y spend.get(customer, 0.0) es el patrón de conteo del capítulo quince con otro disfraz — sumar una cantidad en lugar de sumar uno.
Cuando falla
KeyError: 'discount' — pero ¿en qué registro? Recorre con for i, order in enumerate(orders): para tener i a mano cuando falle — como en el ejemplo de arriba. O pon print(order) dentro del bucle; el último que se imprime es el culpable.
TypeError: string indices must be integers Casi siempre esto significa que creías tener un diccionario y tienes una cadena. Si orders es una lista de nombres en lugar de una lista de diccionarios, order["customer"] da exactamente esto. Comprueba con print(type(order)).
TypeError: list indices must be integers or slices, not str El error contrario — tienes una lista y preguntas por nombre. Normalmente falta un [0]: orders[0]["customer"], no orders["customer"].
AttributeError: 'list' object has no attribute 'get' El mismo tipo de error en otra forma. Lo que llamaste .get() es una lista, no un diccionario.
Cambié una copia y el original cambió también copy() es superficial — las listas y diccionarios de dentro siguen compartidos. import copy y usa copy.deepcopy(...).
Un KeyError en la primera vuelta al agrupar grouped[key].append(...) se ejecutó antes de que la clave existiera. Créala con if key not in grouped: y una lista vacía, o usa setdefault.
Paso 4 de 6 — Predice
Comprueba lo que has entendido
Dos niveles de profundidad. ¿Qué se imprime?
orders = [
{"customer": "rafi", "item": "pen"},
{"customer": "ahmed", "item": "bag"},
]
print(orders[1]["item"])- Apen
- Bbag
- Cahmed
- DUn `KeyError`
Un intento de agrupar. ¿Qué pasa en la primera vuelta?
orders = [
{"customer": "rafi", "item": "pen"},
]
grouped = {}
for order in orders:
grouped[order["customer"]].append(order["item"])
print(grouped)- AUn `AttributeError` — un diccionario no tiene `append`
- B`KeyError: 'rafi'` — la clave todavía no existe, así que no hay nada a lo que añadir
- CImprime `{}`
- DFunciona e imprime `{'rafi': ['pen']}`
Se hizo un copy() y se le aplicaron dos cambios. ¿Qué pasó con el original?
import copy
template = {"name": "", "items": []}
a = template.copy()
a["name"] = "rafi"
a["items"].append("pen")
print(template)- A{'name': '', 'items': ['pen']}
- B{'name': '', 'items': []}
- C{'name': 'rafi', 'items': ['pen']}
- D{'name': 'rafi', 'items': []}
Para responder necesitas una cuenta
Inicia sesión para comprobar tus respuestas
Las preguntas están arriba, y resolverlas mentalmente es lo que de verdad importa. Inicia sesión para ver las respuestas, las explicaciones y las pistas de tres niveles.
Tu turno
Escribe un archivo llamado library.py con una lista de al menos seis libros, cada uno un diccionario con un título, un autor, un año y un número de copias.
Luego:
- Imprime cada libro en columnas alineadas
- Calcula el total de copias y la media de los años
- Agrupa por autor en
{"author": ["title", "title"]}e imprime cuántos libros tiene cada uno - Cuenta los autores distintos con un conjunto
- Imprime el título del libro con más copias
Luego rómpelo como lo hacen los datos reales:
- Borra por completo la clave
"year"de un libro y ejecuta el programa. ¿Qué línea lo detuvo, y el mensaje te dijo qué libro era el culpable? - Ahora usa
.get("year", 0)en esa línea y ejecútalo de nuevo. ¿Cuál es la media de los años ahora, y es cierta?
Esa última pregunta es la lección de verdad. .get() detuvo el error, pero tratar un año ausente como cero y promediarlo es una respuesta incorrecta a la que se llega en silencio. Eliminar un error no es lo mismo que resolver un problema.
Paso 6 de 6
Reto — el quiz del capítulo
Diez preguntas de fácil a difícil. Las últimas son difíciles a propósito.
Inicia sesión para hacer el quiz