Données imbriquées — une liste de dictionnaires
La forme sous laquelle arrivent les vraies données : des dictionnaires dans des listes, des listes dans des dictionnaires. Y entrer, regrouper, et pourquoi copy() ne suffit pas ici.
Comment voulez-vous apprendre ?
Le problème que nous résolvons
Les derniers chapitres ont présenté quatre conteneurs — listes, tuples, dictionnaires, ensembles. Chaque exemple n'en utilisait qu'un, sur un seul niveau.
Les vraies données n'arrivent pas comme ça. Une liste de commandes ressemble à ceci :
customer item quantity price
rafi pen 3 15.0
ahmed bag 1 850.0Il y a deux niveaux ici. À l'extérieur, une collection — le nombre de commandes n'est pas connu, donc une liste. À l'intérieur, chaque commande est un enregistrement dont chaque partie porte un nom — donc un dictionnaire.
Ce que renvoie une API, ce que vous obtenez en lisant un CSV, ce qu'une base de données donne : presque toujours cette forme, une liste de dictionnaires. Le chapitre d'aujourd'hui n'introduit aucun nouveau conteneur. Il s'agit de mettre les vôtres les uns dans les autres, et de repérer les erreurs qui en découlent.
À la fin de ce chapitre, vous pourrez
- Entrer dans une liste de dictionnaires et la parcourir
- Lire un
KeyErrorou unIndexErroren profondeur et dire quel niveau a posé problème - Regrouper et résumer une liste de dictionnaires
- Gérer les enregistrements incomplets avec la valeur par défaut de
.get() - Expliquer pourquoi
copy()ne suffit pas pour les données imbriquées
Prérequis : Ensembles — une collection sans doublons.
Reconnaître la forme
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 ligne orders[0]["customer"] est la compétence centrale de ce chapitre, et elle se lit de gauche à droite, une étape à la fois :
orders— une listeorders[0]— son premier élément, qui est un dictionnaireorders[0]["customer"]— la valeurcustomerde ce dictionnaire
En cas de doute, arrêtez-vous et affichez l'étape du milieu. Écrire print(orders[0]) pour voir que c'est un dictionnaire est une habitude qui enlève la moitié des ennuis avec les données imbriquées.
Le parcourir avec une boucle
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 donne un dictionnaire à chaque tour, et à l'intérieur vous accédez aux choses par leur nom. Aucune arithmétique d'index nulle part.
Guillemets dans une f-string. f"{order['customer']}" utilise des guillemets simples à l'intérieur parce que la f-string elle-même est écrite avec des guillemets droits. Si vous employez le même type dedans et dehors, Python ne peut plus savoir où le texte s'arrête. Gardez un type à l'intérieur et l'autre à l'extérieur.Des enregistrements incomplets
Dans les vraies données, tous les enregistrements n'ont pas toutes les clés :
orders = [{"customer": "rafi"}]
print(orders[0]["discount"])KeyError: 'discount'Le KeyError dit que la clé manque, mais pas quel enregistrement lui manque. Quand cela arrive sur une commande sur douze mille, c'est pénible à trouver. Parcourir avec enumerate vous donne le numéro de chaque enregistrement, pour que vous puissiez afficher celui qui n'a pas la clé :
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'}Vous savez maintenant que le problème est dans l'enregistrement 2 (le compte commence à zéro), et à quoi cet enregistrement ressemble.
Quand l'absence d'une clé est normale, .get() :
orders = [{"customer": "rafi"}]
print(orders[0].get("discount", 0))0La règle du chapitre quinze s'applique aussi ici : les crochets quand elle devrait être là, .get() quand son absence est normale. La tentation est plus forte avec les données imbriquées, parce que .get() peut faire taire toutes les erreurs — et alors les données incomplètes deviennent silencieusement zéro et entrent dans vos calculs.
L'autre forme — des listes dans un dictionnaire
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 ligne by_customer["rafi"].append("bottle") fait deux étapes en une : by_customer["rafi"] extrait une liste, puis append ajoute à cette liste.
Cette forme répond à un problème du chapitre quinze, où une clé ne pouvait exister qu'une seule fois et où une personne ne pouvait donc pas avoir plusieurs enregistrements. Faites de la valeur une liste et une personne peut en avoir plusieurs.
Regrouper — le motif le plus utile
Transformer une liste de dictionnaires en « qui a acheté quoi » :
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']}Les trois lignes du milieu sont toute la technique : si la clé manque, mettez d'abord une liste vide à sa place, puis ajoutez sans vous inquiéter. Sans cela, la toute première commande lève un KeyError.
La même chose tient en une ligne :
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, []) dit « donnez-moi la valeur de cette clé, et si elle n'en a pas, mettez-y cette liste vide puis rendez-la-moi » — et c'est sur ce qui revient que se fait l'ajout. La différence avec .get() compte : .get() ne met rien dans le dictionnaire, setdefault si.
La forme en trois lignes se lit plus facilement, la forme en une ligne s'écrit plus vite. Vous rencontrerez les deux dans du vrai code.
Plus profond encore
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] fait quatre étapes : dictionnaire → liste → dictionnaire → liste. Lu de gauche à droite, ce n'est pas compliqué, seulement long.
Au-delà de trois niveaux, arrêtez-vous et réfléchissez. Casser la chaîne et donner un nom à l'étape du milieu rend le code lisible :
first_person = shop["staff"][0]
print(first_person["shifts"][1])copy() ne suffit pas pour les données imbriquées
Le chapitre douze a montré que b = a laisse deux noms sur une seule liste, et que copy() est le correctif. Avec les données imbriquées, copy() ne protège qu'un niveau :
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 a été fait avec copy() — un nouveau dictionnaire extérieur, mais la liste intérieure est la même liste. Donc a["items"].append("pen") a aussi modifié le template d'origine.
b a été fait avec deepcopy(), qui copie aussi tout ce qu'il y a à l'intérieur. Les modifications de b ne se propagent donc nulle part.
copy est un module, il faut donc import copy avant de l'utiliser — les modules sont traités en entier au chapitre vingt-deux.
Quand avez-vous besoin dedeepcopy? Quand vous prenez copie d'une structure imbriquée pour la modifier, et que vous voulez laisser l'origine tranquille. Sachez quedeepcopyest lent, et que la plupart du temps il est inutile — la plupart du code construit de nouvelles données plutôt que d'en modifier d'existantes.
Un exemple complet
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 : ahmedCe petit programme utilise les quatre conteneurs des derniers chapitres, chacun à la place qui lui convient.
Une liste pour les commandes, parce que le nombre est inconnu et que l'ordre compte.
Des dictionnaires pour chaque commande, parce que chaque partie a besoin d'un nom ; et pour spend, parce qu'il accumule par nom.
Des tuples pour chaque paire dans pairs. Et il y a une astuce ici : le montant est placé en premier dans le tuple, parce que Python trie les tuples par leur première valeur. Un simple .sort(reverse=True) trie donc par montant, et aucun key= n'est nécessaire.
Un ensemble pour distinct, parce que la question était « combien de différents », et que pen apparaît deux fois.
Et spend.get(customer, 0.0) est le motif de comptage du chapitre quinze sous une autre forme — ajouter un montant au lieu d'incrémenter de un.
En cas de problème
KeyError: 'discount' — mais dans quel enregistrement ? Parcourez avec for i, order in enumerate(orders): pour que i soit connu quand cela casse — comme dans l'exemple ci-dessus. Ou mettez print(order) dans la boucle ; le dernier affiché est le coupable.
TypeError: string indices must be integers Presque toujours, cela signifie que vous pensiez avoir un dictionnaire et que vous avez une chaîne. Si orders est une liste de noms plutôt qu'une liste de dictionnaires, order["customer"] donne exactement cela. Vérifiez avec print(type(order)).
TypeError: list indices must be integers or slices, not str L'erreur inverse — vous avez une liste et vous l'interrogez par nom. Il manque en général un [0] : orders[0]["customer"], pas orders["customer"].
AttributeError: 'list' object has no attribute 'get' Le même type d'erreur sous une autre forme. La chose sur laquelle vous appelez .get() est une liste, pas un dictionnaire.
J'ai modifié une copie et l'original a changé aussi copy() est superficiel — les listes et les dictionnaires intérieurs restent partagés. Faites import copy et utilisez copy.deepcopy(...).
Un KeyError dès le premier tour pendant le regroupement grouped[key].append(...) s'est exécuté avant que la clé n'existe. Créez-la avec if key not in grouped: et une liste vide, ou utilisez setdefault.
Étape 4 sur 6 — Prédire
Vérifiez votre compréhension
Deux niveaux de profondeur. Que s'affiche ?
orders = [
{"customer": "rafi", "item": "pen"},
{"customer": "ahmed", "item": "bag"},
]
print(orders[1]["item"])- Apen
- Bbag
- Cahmed
- DUn `KeyError`
Une tentative de regroupement. Que se passe-t-il dès le premier tour ?
orders = [
{"customer": "rafi", "item": "pen"},
]
grouped = {}
for order in orders:
grouped[order["customer"]].append(order["item"])
print(grouped)- AUn `AttributeError` — un dictionnaire n'a pas d'`append`
- B`KeyError: 'rafi'` — la clé n'existe pas encore, donc il n'y a rien sur quoi ajouter
- CCela affiche `{}`
- DCela fonctionne et affiche `{'rafi': ['pen']}`
On a pris un copy() et on y a fait deux modifications. Qu'est-il arrivé à l'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': []}
Répondre nécessite un compte
Connectez-vous pour vérifier vos réponses
Les questions sont ci-dessus, et les résoudre de tête est ce qui compte vraiment. Connectez-vous pour voir les réponses, les explications et les indices à trois niveaux.
À vous de jouer
Écrivez un fichier appelé library.py contenant une liste d'au moins six livres, chacun étant un dictionnaire avec un titre, un auteur, une année et un nombre d'exemplaires.
Ensuite :
- Afficher chaque livre en colonnes alignées
- Calculer le total des exemplaires et l'année moyenne
- Regrouper par auteur dans
{"author": ["title", "title"]}et afficher combien de livres chacun a - Compter les auteurs distincts avec un ensemble
- Afficher le titre du livre ayant le plus d'exemplaires
Puis cassez-le comme le font les vraies données :
- Supprimez entièrement la clé
"year"d'un livre et lancez le programme. Quelle ligne l'a arrêté, et le message vous a-t-il dit quel livre était en cause ? - Maintenant utilisez
.get("year", 0)sur cette ligne et relancez. Quelle est l'année moyenne maintenant, et est-elle juste ?
Cette dernière question est la vraie leçon. .get() a arrêté l'erreur, mais traiter une année manquante comme zéro et en faire la moyenne est une mauvaise réponse obtenue en silence. Supprimer une erreur ne revient pas à résoudre un problème.
Étape 6 sur 6
Défi — le quiz du chapitre
Dix questions, de la plus facile à la plus difficile. Les dernières sont difficiles exprès.
Connectez-vous pour faire le quiz