Lire les erreurs
- Leçon 13
- 45 min
- Module 5 : Débogage et fiabilité
Leçon 13 sur 18 · Python pour l'ingénierie de l'IA en pratiqueLire les erreurs
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
Trouvez la cause de deux vrais problèmes dans ticket-cleaner. Le premier arrête le programme avec une trace d’appels. Le second donne un résultat incorrect sans aucune erreur. La méthode reste la même pour chaque bogue. Recueillez les faits, trouvez où le résultat attendu diffère du résultat réel, puis réduisez le problème pour montrer sa cause.
Carnet JupyterFaites cette leçon dans un carnet Jupyter
Le carnet (notebook) Jupyter contient les deux analyses, la tâche et sa vérification, avec les instructions. Ses premières cellules créent le dossier ticket-cleaner tel qu’il est au début de cette leçon. Elles installent aussi httpx et téléchargent les fichiers de données. Le carnet fonctionne donc sans votre projet local. Lisez les explications ici et exécutez le code dans le carnet.
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 variables d’environnement et le module de configuration, dans Configuration et secrets.
read_rowsetsplit_records, ainsi que les encodages de texte, dans Lire et écrire des fichiers.- Le mode interactif (
>>>), dans Utiliser un client HTTP simple : exemple détaillé.
Ce qu’il vous faut sur votre ordinateur (uniquement pour le parcours local; le carnet n’en a pas besoin)
- ticket-cleaner lit sa source dans
TICKETS_SOURCE:ticket_cleaner/config.pyet le nouveau fichiercount_tickets.py. Vous les avez créés dans Configuration et secrets : exercices guidés. S’ils manquent ou diffèrent, refaites cette section. Vous pouvez aussi copier les fichiers de 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. TICKETS_SOURCEn’est pas définie dans votre terminal. Supprimez-la avecunset TICKETS_SOURCE(macOS et Linux) ouRemove-Item Env:TICKETS_SOURCE(PowerShell), comme dans Configuration et secrets : variables d’environnement.data/tickets.csvetdata/tickets.json. Vous les avez téléchargés dans Lire et écrire des fichiers : obtenir le fichier de données. Si un fichier manque ou a changé, téléchargez-le de nouveau avec la même commandecurl. Les sommes de contrôle se trouvent dans la fiche du jeu de données. Cette leçon télécharge aussidata/tickets-excel.csv, dans Problème 2.- Un terminal ouvert dans
~/projects/ticket-cleaner. Vous avez créé ce dossier dans Les fichiers et le terminal : exercices guidés. - L’environnement
.venvdu projet, actif dans votre terminal (l’invite commence par(.venv)). Vous l’avez créé dans Isoler les dépendances : exercices guidés. S’il manque 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. Les numéros de ligne de vos traces d’appels peuvent différer si vos fichiers contiennent plus ou moins de lignes. Le carnet a été testé dans Jupyter.
Une méthode, pas de la chance
Section intitulée « Une méthode, pas de la chance »Quand un programme échoue, on peut vouloir modifier quelque chose et le relancer jusqu’à ce qu’il fonctionne. Cela fonctionne parfois, mais vous ne découvrez pas la cause, et le bogue revient souvent. Une meilleure méthode suit un ordre fixe :
Description textuelle du diagramme
Commencez par le symptôme. Si le programme s’arrête avec une trace d’appels, lisez la dernière ligne pour connaître le type d’erreur et le message. Trouvez ensuite le dernier cadre qui appartient à votre code. Si le programme donne un résultat incorrect, notez la sortie attendue et la sortie réelle. Trouvez ensuite la première étape où elles diffèrent, avec print ou un point d’arrêt. Les deux parcours posent ensuite la même question : pouvez-vous reproduire le problème volontairement? Sinon, recueillez d’autres faits (entrée, paramètres, versions) et posez de nouveau la question. Si oui, réduisez l’entrée jusqu’à ce qu’il ne reste que la cause. C’est un exemple minimal qui échoue. Corrigez le bogue et gardez l’exemple comme test de non-régression.
Problème 1 : une trace d’appels
Section intitulée « Problème 1 : une trace d’appels »Un collègue envoie un fichier JSON provenant d’un autre système. Créez-le dans votre projet sous le nom data/broken.json :
[ {"id": "T-3001", "status": "open", "category": "billing", "priority": 2}, null, {"id": "T-3003", "status": "closed", "category": "login", "priority": 3}]Exécutez le programme avec ce fichier. Sur macOS et Linux, vous pouvez définir la variable pour une seule commande :
TICKETS_SOURCE=data/broken.json python count_tickets.py$env:TICKETS_SOURCE = "data/broken.json"python count_tickets.pyTICKETS_SOURCE=data/broken.json python count_tickets.pySource: data/broken.jsonTraceback (most recent call last): File "/Users/amira/projects/ticket-cleaner/count_tickets.py", line 34, in <module> main() ~~~~^^ File "/Users/amira/projects/ticket-cleaner/count_tickets.py", line 23, in main tickets, rejected = split_records(rows) ~~~~~~~~~~~~~^^^^^^ File "/Users/amira/projects/ticket-cleaner/ticket_cleaner/parsing.py", line 50, in split_records problems = find_problems(raw) File "/Users/amira/projects/ticket-cleaner/ticket_cleaner/parsing.py", line 21, in find_problems if clean_text(raw.get(field)) == "": ^^^^^^^AttributeError: 'NoneType' object has no attribute 'get'Une trace d’appels est le chemin des appels de fonction qui ont mené à l’erreur. Chaque appel forme un cadre : un fichier, un numéro de ligne, le nom de la fonction et la ligne de code. Le premier cadre marque le début du programme, et le dernier indique où l’erreur s’est produite. Lisez donc la trace de bas en haut :
| À lire | Ce que cela vous indique |
|---|---|
| La dernière ligne | Le type d’erreur, AttributeError, et le message : une valeur de type NoneType n’a pas de .get. La seule valeur de type NoneType est None. |
| Le dernier cadre | Dans find_problems, la ligne raw.get(field). Les marques ^^^^ montrent la partie exacte : raw.get. Donc, raw vaut None. |
| Le cadre au-dessus | split_records a appelé find_problems(raw) avec une des lignes. |
| Les cadres plus haut | Comment le programme est arrivé là : main() a appelé split_records. Lisez-les seulement au besoin. |
Les faits jusqu’ici : une ligne vaut None. D’où vient-elle? json.load transforme la valeur JSON null en None, et le deuxième élément du fichier est null. La cause se trouve donc dans l’entrée : un enregistrement qui n’est pas un dictionnaire. find_problems supposait que chaque ligne était un dictionnaire, et son contrat ne précisait pas quoi faire autrement.
C’est une entrée invalide, pas un défaut dans la logique du programme. Une entrée externe peut contenir n’importe quoi. Le programme doit donc la rejeter avec une raison, comme tout autre enregistrement invalide. Ajoutez cette vérification au début de find_problems, après la chaîne de documentation :
if not isinstance(raw, dict): return ["not a record"]isinstance(raw, dict) vaut True si raw est un dictionnaire. Le return anticipé arrête l’exécution avant tout raw.get. Relancez le programme :
Source: data/broken.jsonRow 2 rejected: not a record3 rows: 2 valid, 1 rejected.Problème 2 : un résultat incorrect sans erreur
Section intitulée « Problème 2 : un résultat incorrect sans erreur »Le service de soutien exporte ses billets de soutien depuis un tableur. Téléchargez ce fichier :
curl -o data/tickets-excel.csv https://raw.githubusercontent.com/Nextia-Intelligence/nextia-learning-labs/main/C02/data/tickets-excel.csvcurl.exe -o data/tickets-excel.csv https://raw.githubusercontent.com/Nextia-Intelligence/nextia-learning-labs/main/C02/data/tickets-excel.csvcurl -o data/tickets-excel.csv https://raw.githubusercontent.com/Nextia-Intelligence/nextia-learning-labs/main/C02/data/tickets-excel.csvOuvrez-le dans VS Code. Il semble identique à tickets.csv. Exécutez le programme avec ce fichier (définissez TICKETS_SOURCE=data/tickets-excel.csv comme avant) :
Row 14 rejected: missing id; missing categoryRow 15 rejected: missing id15 rows: 0 valid, 15 rejected.Report: reports/summary.jsonIl n’y a aucune trace d’appels. Le programme indique que chaque ligne n’a pas d’identifiant, mais vous voyez les identifiants dans le fichier. Suivez le côté droit de l’arbre de décision.
Résultat attendu et résultat réel. Attendu : 9 valides, 6 rejetés, comme pour tickets.csv. Réel : 0 valide, chaque ligne indique missing id (identifiant manquant).
Trouvez la première étape où ils diffèrent. find_problems indique « missing id », alors examinez ce qu’elle reçoit. Vous pouvez ajouter print(raw), mais un point d’arrêt est souvent plus rapide. Il arrête le programme à une ligne, et vous pouvez examiner chaque variable avant de continuer. Ajoutez cette ligne comme première ligne de find_problems, après la chaîne de documentation :
breakpoint()Relancez le programme. Il s’arrête et affiche l’invite du débogueur, (Pdb). Tapez une commande après l’invite et appuyez sur Entrée :
> /Users/amira/projects/ticket-cleaner/ticket_cleaner/parsing.py(19)find_problems()-> breakpoint()(Pdb) p raw{'\ufeffid': 'T-1001', 'status': 'open', 'category': 'billing', 'priority': '2', 'subject': 'Charged twice for March'}(Pdb) p list(raw)['\ufeffid', 'status', 'category', 'priority', 'subject'](Pdb) p raw.get("id")None(Pdb) q| Commande | Effet |
|---|---|
p expression |
Affiche la valeur de toute expression, avec les variables à cet endroit. |
l |
Affiche le code autour de la ligne actuelle. |
n |
Exécute la ligne suivante, puis s’arrête de nouveau. |
c |
Continue jusqu’au prochain point d’arrêt ou jusqu’à la fin. |
q |
Quitte le programme. Si Python demande Quit anyway? [y/n] (quitter quand même?), tapez y. |
Voilà : la première clé n’est pas 'id', mais '\ufeffid'. Un caractère invisible précède id. Supprimez maintenant la ligne breakpoint(). Un point d’arrêt oublié arrête le programme pour chaque utilisateur.
Réduisez l’exemple. Le problème vient-il du fichier ou du code? Un exemple minimal qui échoue est la plus petite entrée qui montre encore le problème. Créez-en un dans Python interactif (python, puis tapez chaque ligne) :
>>> import csv>>> with open("tiny.csv", "w", encoding="utf-8-sig") as file:... file.write("id,status\nT-1,open\n")...19>>> with open("tiny.csv", encoding="utf-8", newline="") as file:... print(list(csv.DictReader(file)))...[{'\ufeffid': 'T-1', 'status': 'open'}](19 est le nombre de caractères écrits par write. Le mode interactif l’affiche.) Deux lignes de données, et le problème est toujours là. La seule particularité est encoding="utf-8-sig" lors de l’écriture du fichier. C’est ainsi que les tableurs enregistrent les fichiers CSV. Ils placent trois octets au début du fichier pour dire « ce fichier est en UTF-8 ». Ces octets forment une marque d’ordre des octets (BOM). La lecture avec encoding="utf-8" conserve cette marque sous forme du caractère \ufeff. Il devient alors une partie du nom de la première colonne.
Correction. L’encodage utf-8-sig lit l’UTF-8 et supprime la marque d’ordre des octets, s’il y en a une. Un fichier sans marque se lit comme avant. Modifiez la ligne CSV dans read_rows :
with open(path, encoding="utf-8-sig", newline="") as file:Relancez le programme avec data/tickets-excel.csv. Le résultat est 15 rows: 9 valid, 6 rejected. (15 lignes : 9 valides, 6 rejetées), comme pour tickets.csv. Supprimez tiny.csv.
Exercices guidés : les deux problèmes
Section intitulée « Exercices guidés : les deux problèmes »Faites les deux analyses sur votre ordinateur, dans l’ordre.
- Créez
data/broken.jsonet exécutez le programme avec ce fichier. Lisez la trace d’appels de bas en haut. Notez le type d’erreur et le dernier cadre dans votre code. - Ajoutez la vérification
isinstanceàfind_problems. Relancez le programme. La ligne 2 est rejetée avecnot a record(ce n’est pas un enregistrement). - Téléchargez
data/tickets-excel.csvet exécutez le programme avec ce fichier. Notez le résultat attendu et le résultat réel. - Ajoutez
breakpoint()àfind_problems. Exécutez le programme et utilisezp rawetp list(raw). Quittez avecq. - Supprimez la ligne
breakpoint(). - Créez l’exemple minimal qui échoue dans Python interactif.
- Changez l’encodage pour
utf-8-siget relancez le programme. Le résultat est 9 valides et 6 rejetés. - Supprimez
TICKETS_SOURCE(unset TICKETS_SOURCEouRemove-Item Env:TICKETS_SOURCE). Relancez le programme avec le fichier par défaut pour vérifier qu’il fonctionne encore.
À vous : les points-virgules
Section intitulée « À vous : les points-virgules »Dans certains pays, les tableurs séparent les valeurs avec ; plutôt qu’avec ,. Créez data/tickets-semicolon.csv avec ce texte :
id;status;category;priority;subjectT-4001;open;billing;2;Wrong invoice totalT-4002;closed;login;3;Locked outExécutez le programme avec ce fichier. Utilisez l’arbre de décision : notez le résultat attendu et le résultat réel. Trouvez la première étape où ils diffèrent et créez un exemple minimal qui échoue. Vous n’avez pas à le corriger. Écrivez deux phrases : la cause et ce qu’une correction devrait faire.
Montrer la cause
Les deux lignes sont rejetées avec missing id; missing status; missing category; missing priority (identifiant, statut, catégorie et priorité manquants). p list(raw) à un point d’arrêt montre une seule clé : 'id;status;category;priority;subject'. csv.DictReader sépare les valeurs avec , par défaut. Il lit donc chaque ligne comme une seule colonne. Une correction doit indiquer le séparateur au lecteur : csv.DictReader(file, delimiter=";"). Un programme pourrait aussi le détecter, mais il lui faudrait une règle pour les fichiers contenant les deux caractères. Pour ticket-cleaner, rejeter le fichier avec un message clair suffit, et chaque ligne est déjà signalée.
Erreur courante
Modifier le code jusqu'à ce que l'erreur disparaisse
Symptôme : après plusieurs petites modifications, l’erreur disparaît, mais vous ne savez pas pourquoi. Ou une autre erreur apparaît à un endroit qui fonctionnait avant.
Cause : les modifications étaient des suppositions, pas des réponses aux faits. Certaines cachent seulement le symptôme, par exemple raw = raw or {} pour la ligne None. L’erreur disparaît, mais un échec clair devient un résultat trompeur. La ligne est maintenant rejetée avec missing id; missing status; missing category; missing priority. Le vrai problème, une valeur null dans le fichier, reste caché.
Correction :
- Annulez les modifications faites au hasard. Si vous utilisez Git,
git diffles montre etgit restoreles supprime. - Revenez à l’arbre de décision et recueillez un fait à la fois. Par exemple : la dernière ligne de la trace d’appels, la valeur d’une variable ou la plus petite entrée.
- Modifiez le code seulement quand vous pouvez expliquer la cause en une phrase.
Vérifiez votre compréhension
Une trace d'appels contient six cadres. Les deux derniers sont dans httpx. Où commencez-vous à chercher la cause?
Afficher la réponse
Au dernier cadre dans votre code, le troisième à partir du bas. Cette ligne a appelé httpx avec une valeur ou à un moment que httpx ne pouvait pas gérer. Par exemple, une URL avec une faute de frappe. Lisez d’abord la dernière ligne de la trace d’appels pour connaître le type d’erreur et le message. Remontez ensuite jusqu’à votre code.
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
Pourquoi créer un exemple minimal qui échoue si vous voyez déjà le problème dans le fichier complet?
Afficher la réponse
Il prouve la cause. Avec 15 lignes et 5 modules, beaucoup de choses pourraient être en cause. Avec deux lignes de CSV et un seul open, il ne reste que l’encodage. Cet exemple devient aussi le test de non-régression dans les prochaines leçons. C’est une petite entrée qui échoue avant la correction et réussit après.
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
- Lisez une trace d’appels de bas en haut : la dernière ligne donne l’erreur, et le dernier cadre dans votre code indique où.
- Pour un résultat incorrect sans erreur, comparez le résultat attendu et le résultat réel. Trouvez la première étape où ils diffèrent.
breakpoint()arrête le programme.paffiche une valeur,nexécute la ligne suivante,ccontinue etqquitte. Supprimez le point d’arrêt après utilisation.- Un exemple minimal qui échoue élimine tout ce qui n’est pas la cause.
- Une entrée externe peut contenir n’importe quoi. Rejetez un enregistrement invalide avec une raison; ne le cachez pas.
Ensuite, vous ferez gérer les erreurs attendues par le programme avec des messages clairs et des codes de sortie. Vous utiliserez aussi la journalisation pour enregistrer ce qu’il fait.
Leçon suivante : Exceptions et journalisation