Les enregistrements structurés
- Leçon 9
- 45 min
- Module 3 : Fonctions et structure du programme
Leçon 9 sur 18 · Python pour l'ingénierie de l'IA en pratiqueLes enregistrements structurés
1. Bases de l'ordinateur et de l'environnement
2. Valeurs et flux de contrôle
3. Fonctions et structure du programme
4. Fichiers, données et services externes
5. Débogage et fiabilité
6. Du carnet Jupyter au projet facile à maintenir
Ce que vous saurez faire
Transformez les enregistrements bruts de billets de soutien provenant d’un fichier en objets Ticket vérifiés. Produisez des rapports uniquement avec ces objets. Le programme repère et nomme chaque enregistrement problématique avant qu’un rapport l’utilise. Ainsi, une faute de frappe dans les données ne peut pas produire un nombre erroné dans le rapport.
Carnet JupyterFaites cette leçon dans un carnet Jupyter
Le carnet (notebook) Jupyter contient chaque étape, tâche et vérification de cette leçon, avec les consignes. Ses premières cellules créent le dossier ticket-cleaner tel qu’il est au début de cette leçon. Il fonctionne donc sans votre projet local. Lisez les explications ici et exécutez le code dans le carnet. Aucune installation n’est nécessaire.
Le carnet est en anglais. Colab et Kaggle ouvrent votre propre copie. Il faut un compte Google ou Kaggle gratuit. Vous êtes bloqué ? Consultez le carnet de solution.
Avant de commencerCe qu’il faut savoir et avoir sous la main
Ce que vous devez savoir
- Les modules, les paquets et les importations, dans Modules et importations.
- Le contrat d’une fonction, dans Le contrat d’une fonction : le contrat.
- Deux noms pour une même liste, dans Les collections : deux noms, une seule liste. Cette section explique pourquoi un billet est figé.
Ce qu’il vous faut sur votre ordinateur (seulement pour le parcours local; le carnet et les encadrés de cette page n’en ont pas besoin)
- Le paquet
ticket_cleaner/avec__init__.pyetreport.py, ainsi quecount_tickets.py, qui importe depuis ce paquet. Vous les avez créés dans Modules et importations : exercice guidé. S’ils sont absents ou différents, refaites cette section. Vous pouvez aussi copier les fichiers depuis la cellule de préparation du carnet de cette leçon. Elle contient tous les fichiers tels qu’ils sont au début de cette leçon. - Un terminal dans
~/projects/ticket-cleaner. Vous avez créé le dossier dans Les fichiers et le terminal : exercice guidé. - L’environnement
.venvdu projet, actif dans votre terminal (l’invite commence par(.venv)). Vous l’avez créé dans Isoler les dépendances : exercice guidé. S’il est absent ou ne fonctionne plus, supprimez-le et recréez-le à partir du fichier de verrouillage. Suivez Recréer l’environnement à partir de zéro.
Testé avec Python 3.14.6 sur macOS et Pyodide 314.0 (navigateur). Le carnet a été testé dans Jupyter.
Les enregistrements bruts ne sont pas encore des billets
Section intitulée « Les enregistrements bruts ne sont pas encore des billets »Jusqu’ici, les billets étaient parfaits : les bonnes clés, un statut en minuscules et une priorité numérique. Les données réelles ne sont pas comme ça. Dans le module 4, vous lirez un fichier. Chaque enregistrement arrivera sous forme de dictionnaire de chaînes saisies par des personnes :
{"id": "t-1003", "status": "Open ", "category": "login", "priority": "1"}{"id": "T-1004", "status": "opne", "category": "billing", "priority": "2"}{"id": "T-1005", "status": "open", "category": "billing", "priority": "high"}Le premier enregistrement est valide après nettoyage : lettres minuscules, sans espaces superflus. Le deuxième contient une faute de frappe dans le statut. Le troisième a une priorité qui n’est pas un nombre. Si les fonctions de rapport reçoivent ces enregistrements, filter_by_status ignore "Open " (ouvert) et "opne" (faute de frappe). Et average_priority s’arrête avec une erreur à "high" (élevée).
Le programme comporte donc trois étapes, chacune dans son propre module :
| Étape | Module | Rôle |
|---|---|---|
| Valider | parsing.py |
Trouver les problèmes dans un enregistrement brut. |
| Analyser | parsing.py |
Transformer un enregistrement brut valide en Ticket nettoyé. |
| Produire un rapport | report.py |
Calculer uniquement avec des objets Ticket. |
Les fonctions de rapport peuvent alors se fier à leurs données d’entrée. Toutes les vérifications sont au même endroit.
Annotations de type : le contrat sur la ligne def
Section intitulée « Annotations de type : le contrat sur la ligne def »Une annotation de type indique le type attendu par un paramètre ou le type retourné par une fonction. Elle s’écrit après un : pour un paramètre et après -> pour la valeur de retour :
def average_priority(tickets: list[Ticket]) -> float | None:Lisez ceci ainsi : « average_priority reçoit une liste d’objets Ticket et retourne un float ou None ». Voici les annotations courantes :
| Annotation | Signification |
|---|---|
str, int, float, bool |
Une valeur de ce type. |
list[str] |
Une liste de chaînes. |
dict[str, int] |
Un dictionnaire avec des clés de type chaîne et des valeurs entières. |
float | None |
Un float ou None. Le | signifie « ou ». |
-> None |
La fonction ne retourne rien d’utile (elle affiche du texte ou enregistre un fichier). |
Python ne vérifie pas les annotations de type pendant l’exécution du programme. average_priority("abc") s’exécute quand même et échoue plus tard. Les annotations servent aux personnes et aux outils. VS Code les lit et suggère les bons noms pendant que vous tapez. Il souligne aussi les appels avec un type incorrect avant toute exécution.
Dataclasses : un type pour un billet
Section intitulée « Dataclasses : un type pour un billet »Un dictionnaire peut contenir n’importe quelles clés. Si vous tapez ticket["stauts"], vous découvrez l’erreur seulement quand cette ligne s’exécute. Pour un enregistrement avec un ensemble fixe de champs, créez plutôt votre propre type. Une classe définit un nouveau type. Une dataclass (classe de données) est la façon la plus courte d’écrire une classe qui contient surtout des données :
from dataclasses import dataclass
STATUSES = {"open", "pending", "closed"}PRIORITIES = {1, 2, 3}REQUIRED_FIELDS = ("id", "status", "category", "priority")
@dataclass(frozen=True)class Ticket: """One valid support ticket."""
id: str status: str category: str priority: int
def is_urgent(self) -> bool: """Return True for priority 1, the most urgent level.""" return self.priority == 1| Partie | Signification |
|---|---|
@dataclass |
Demande à Python d’écrire le code habituel pour vous : créer un billet à partir de quatre valeurs, l’afficher lisiblement et comparer deux billets. |
(frozen=True) |
Un billet ne peut pas changer après sa création. |
id: str et les lignes suivantes |
Les champs, chacun avec une annotation de type. Chaque billet possède ces quatre valeurs. |
def is_urgent(self) |
Une méthode : une fonction qui appartient au type. self est le billet sur lequel vous l’appelez. |
Créez un billet en appelant la classe comme une fonction, puis lisez ses champs avec un point :
ticket = Ticket(id="T-1003", status="open", category="login", priority=1)print(ticket) # Ticket(id='T-1003', status='open', category='login', priority=1)print(ticket.status) # openprint(ticket.is_urgent()) # TrueExécutez du Python dans votre navigateur
Créer et utiliser un Ticket
La dernière ligne contient une faute de frappe. Exécutez le code et lisez la dernière ligne de l’erreur. Corrigez ensuite le nom et ajoutez une ligne qui tente de changer le statut : ticket.status = "closed". Que se passe-t-il?
Tab ajoute quatre espaces. Appuyez sur Esc, puis sur Tab, pour quitter la zone. Ctrl+Enter exécute le code.
Pourquoi frozen=True? Dans Les collections, deux noms pour une même liste causaient des changements à des endroits inattendus. Un billet figé ne peut pas changer. Vous pouvez donc le partager sans risque entre des listes et des fonctions. Pour obtenir un billet avec un autre statut, créez-en un nouveau. La fonction replace de dataclasses copie un billet en modifiant un champ : replace(ticket, status="closed").
Exemple détaillé : valider, puis analyser
Section intitulée « Exemple détaillé : valider, puis analyser »La fonction de validation retourne une liste de problèmes. Une liste vide signifie que l’enregistrement est valide. C’est mieux que True ou False, car le rapport peut indiquer pourquoi un enregistrement a été rejeté. Elle trouve aussi tous les problèmes d’un enregistrement en une fois, pas seulement le premier.
from ticket_cleaner.records import PRIORITIES, REQUIRED_FIELDS, STATUSES, Ticket
def clean_text(value: object) -> str: """Return the value as text with no outer spaces, in small letters.""" if value is None: return "" return str(value).strip().lower()
def find_problems(raw: dict) -> list[str]: """Return a list of problems with one raw record. An empty list means valid.""" problems = [] for field in REQUIRED_FIELDS: if clean_text(raw.get(field)) == "": problems.append(f"missing {field}")
status = clean_text(raw.get("status")) if status and status not in STATUSES: problems.append(f"unknown status {status!r}")
priority = clean_text(raw.get("priority")) if priority and not (priority.isdigit() and int(priority) in PRIORITIES): problems.append(f"priority must be 1, 2 or 3, not {priority!r}") return problems
def to_ticket(raw: dict) -> Ticket: """Make a Ticket from a raw record that has no problems.""" return Ticket( id=clean_text(raw["id"]).upper(), status=clean_text(raw["status"]), category=clean_text(raw["category"]), priority=int(clean_text(raw["priority"])), )Quelques détails :
clean_textaccepte toute valeur (object), car un fichier JSON peut contenir des nombres, etstr(2)donne"2".raw.get(field)donneNonesi une clé manque, etclean_text(None)donne"". Une clé manquante et une valeur vide représentent donc le même problème.{status!r}encadre la valeur de guillemets dans le message :unknown status 'opne'(statut inconnu « opne »). Une valeur contenant un espace, comme'open ', devient alors visible.priority.isdigit()donneTrueseulement si chaque caractère est un chiffre. Leands’arrête avantint(priority)quand le résultat estFalse. Ainsi,int("high")ne s’exécute jamais.to_ticketne vérifie rien. Son contrat indique que l’enregistrement n’a aucun problème. Le code appelant doit d’abord appelerfind_problems.
Les fonctions de rapport reçoivent des annotations de type et accèdent aux champs avec un point plutôt qu’avec ["..."] :
from ticket_cleaner.records import Ticket
def filter_by_status(tickets: list[Ticket], status: str = "open") -> list[Ticket]: """Return the tickets that have the given status.""" return [ticket for ticket in tickets if ticket.status == status]
def count_by_category(tickets: list[Ticket]) -> dict[str, int]: """Return the number of tickets in each category, sorted by category.""" counts = {} for ticket in tickets: counts[ticket.category] = counts.get(ticket.category, 0) + 1 return dict(sorted(counts.items()))
def average_priority(tickets: list[Ticket]) -> float | None: """Return the average priority, or None when there are no tickets.""" if not tickets: return None return round(sum(ticket.priority for ticket in tickets) / len(tickets), 1)sum(ticket.priority for ticket in tickets) additionne les priorités en une ligne. Cette expression fonctionne comme une compréhension de liste, sans les crochets.
Ce diagramme montre comment main() utilise les cinq fonctions, ainsi que les types d’entrée et de sortie de chacune :
Description textuelle du diagramme
main() appelle d’abord find_problems pour chaque ligne brute (un dict), qui retourne une list[str]. Pour chaque ligne sans problème, elle appelle to_ticket, qui retourne un Ticket. Elle appelle ensuite trois fonctions de report.py avec des listes de billets. filter_by_status retourne la list[Ticket] sélectionnée. count_by_category retourne un dict[str, int]. average_priority retourne un float ou None. Seule main() affiche du texte.
Exercice guidé : créer les trois modules
Section intitulée « Exercice guidé : créer les trois modules »-
Créez
ticket_cleaner/records.pyavec le code de la section sur les dataclasses. -
Créez
ticket_cleaner/parsing.pyavec le code de l’exemple détaillé. -
Remplacez le contenu de
ticket_cleaner/report.pypar la version avec annotations de type. -
Remplacez
count_tickets.pypar cette version. Les données arrivent maintenant sous forme de chaînes brutes, comme depuis un fichier :count_tickets.py from ticket_cleaner.parsing import find_problems, to_ticketfrom ticket_cleaner.records import Ticketfrom ticket_cleaner.report import average_priority, count_by_category, filter_by_statusSAMPLE_ROWS = [{"id": "T-1001", "status": "open", "category": "billing", "priority": "2"},{"id": "T-1002", "status": "closed", "category": "login", "priority": "3"},{"id": "t-1003", "status": "Open ", "category": "login", "priority": "1"},{"id": "T-1004", "status": "opne", "category": "billing", "priority": "2"},{"id": "T-1005", "status": "open", "category": "billing", "priority": "high"},{"id": "T-1006", "status": "closed", "category": "shipping", "priority": "1"},]def main() -> None:tickets: list[Ticket] = []for raw in SAMPLE_ROWS:problems = find_problems(raw)if problems:print(f"Skipped {raw['id']}: {'; '.join(problems)}")else:tickets.append(to_ticket(raw))status = "open"selected = filter_by_status(tickets, status)print(f"Tickets with status '{status}': {len(selected)}")if not selected:print("No tickets to count.")returnfor category, count in count_by_category(selected).items():print(f" {category}: {count}")print(f"Average priority: {average_priority(selected)}")if __name__ == "__main__":main()"; ".join(problems)réunit les chaînes d’une liste en une seule chaîne, avec"; "entre elles. -
Exécutez le programme :
Fenêtre de terminal python count_tickets.pySortie Skipped T-1004: unknown status 'opne'Skipped T-1005: priority must be 1, 2 or 3, not 'high'Tickets with status 'open': 2billing: 1login: 1Average priority: 1.5t-1003, avec"Open ", est compté comme un billet ouvert, carto_ticketl’a nettoyé. Les deux enregistrements problématiques sont nommés, pas perdus.
À vous de jouer : une règle de plus
Section intitulée « À vous de jouer : une règle de plus »Le service de soutien indique que chaque identifiant de billet doit commencer par T-. Ajoutez cette règle à find_problems dans l’encadré ci-dessous. Pour un identifiant qui ne commence pas par T- après nettoyage, ajoutez le problème "id must start with 'T-'" (l’identifiant doit commencer par « T- »). Un identifiant manquant doit donner seulement "missing id" (identifiant manquant), pas les deux problèmes.
Exécutez du Python dans votre navigateur
À vous de jouer : une règle pour l'identifiant
{**good, "id": "X-9"} crée une copie de good avec un autre identifiant. L’encadré vérifie les trois cas. Cette règle sert à vous exercer. Ne l’ajoutez pas à votre projet, car les prochains modules utilisent les règles précédentes.
Tab ajoute quatre espaces. Appuyez sur Esc, puis sur Tab, pour quitter la zone. Ctrl+Enter exécute le code.
Afficher une réponse
ticket_id = clean_text(raw.get("id")) if ticket_id and not ticket_id.startswith("t-"): problems.append("id must start with 'T-'")clean_text met l’identifiant en minuscules, alors comparez-le à "t-". La vérification if ticket_id and ... ignore un identifiant vide, qui a déjà le problème missing id (identifiant manquant).
Erreur courante
Une faute de frappe dans un nom de champ
Symptôme : après avoir remplacé les dictionnaires par des objets Ticket, une fonction de rapport s’arrête avec une erreur comme celle-ci :
TypeError: 'Ticket' object is not subscriptableou, avec une faute de frappe dans un nom de champ :
AttributeError: 'Ticket' object has no attribute 'stauts'. Did you mean: 'status'?Cause : la première erreur signifie que le code utilise encore l’accès par dictionnaire, ticket["status"], sur un Ticket. Une dataclass possède des champs, pas des clés. « Subscriptable » signifie « accessible avec [ ] ». La deuxième erreur concerne un nom de champ inexistant. Python suggère le nom le plus proche.
Solution :
- Lisez le numéro de ligne dans l’erreur et remplacez
ticket["status"]particket.status. - Pour un nom mal orthographié, utilisez le nom que Python suggère.
- Pour trouver ces erreurs avant d’exécuter le code, cherchez les noms soulignés dans VS Code. L’éditeur connaît les champs de
Ticketgrâce à ses annotations de type.
Vérifiez votre compréhension
Pourquoi find_problems retourne-t-elle une liste de chaînes plutôt que True ou False?
Afficher la réponse
Une liste indique pourquoi un enregistrement n’est pas valide et peut contenir plusieurs problèmes. Le programme peut afficher les raisons dans le rapport pour qu’une personne corrige les données. Une liste vide signifie toujours « valide », car elle est considérée comme fausse : if problems:.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Vérification du module : vérifiez to_ticket et find_problems séparément. Donnez une assertion pour chacune et expliquez le contrat de to_ticket en mots simples.
Afficher la réponse
Par exemple : assert find_problems({"id": "T-1", "status": "opne", "category": "x", "priority": "2"}) == ["unknown status 'opne'"], et assert to_ticket({"id": " t-7 ", "status": "Open", "category": "Login", "priority": "1"}) == Ticket(id="T-7", status="open", category="login", priority=1). Contrat de to_ticket : elle reçoit un enregistrement brut qui n’a aucun problème et retourne un Ticket nettoyé. Le texte est nettoyé et la priorité est un entier. Elle ne vérifie pas l’enregistrement. Le code appelant doit d’abord appeler find_problems.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Récapitulatif
- Les annotations de type montrent le contrat d’une fonction sur sa ligne
def:def f(tickets: list[Ticket]) -> float | None:. Python ne les vérifie pas à l’exécution, mais votre éditeur le fait. - Une dataclass permet de définir rapidement un type pour un enregistrement. Lisez ses champs avec un point.
frozen=Trueempêche les modifications. - Validez d’abord et retournez une liste de problèmes pour que le rapport puisse expliquer pourquoi un enregistrement a été rejeté.
- Analysez seulement les enregistrements valides pour obtenir des objets nettoyés. Les fonctions de rapport travaillent alors uniquement avec des données nettoyées.
- Gardez chaque étape dans son propre module :
records.py,parsing.pyetreport.py.
Vous pouvez maintenant organiser le code en fonctions et en modules avec des contrats clairs. Dans le prochain module, les enregistrements bruts proviennent de vrais fichiers CSV et JSON, ainsi que d’un service Web.
Leçon suivante : Lire et écrire des fichiers