Solutions — série 08-4 (refactoring, docstrings et README)
Rappel de la règle d'or : on n'ouvre ce fichier qu'après avoir terminé (ou sérieusement séché sur) la série. Sur les refactorings, compare la séquence de gestes (suite relancée entre chaque ?) autant que le code final ; sur les docstrings et le README, compare ce que la solution a jugé digne d'être dit.
Exercice facile a — les docstrings des fonctions du niveau
Raisonnement — Pour chaque fonction, se mettre à la place de l'APPELANT : que reçoit-il, que doit-il fournir, qu'est-ce qui peut lui exploser à la figure ? Le résumé tient en une ligne à l'infinitif anglais (Return ...) ; Args/Returns seulement s'ils apportent quelque chose ; Raises OBLIGATOIRE dès qu'il y a un contrat d'erreur (c'est la doc de la leçon 08-3).
Solution
def median(values):
"""Return the median of values.
For an even count, return the mean of the two middle values.
Args:
values: a non-empty list of numbers (order does not matter).
Returns:
The median, as an int or a float.
Raises:
ValueError: if values is empty.
"""
def average(values):
"""Return the arithmetic mean of values.
Args:
values: a non-empty list of numbers.
Returns:
The mean, always as a float.
Raises:
ValueError: if values is empty.
TypeError: if any element is not a number.
"""
def grade_to_mention(grade):
"""Return the French mention for a grade out of 20.
Thresholds: 16 "tres bien", 14 "bien", 12 "assez bien", 10 "passable",
below 10 "insuffisant".
Raises:
ValueError: if grade is outside [0, 20].
"""
Vérification dans le REPL : help(median) affiche exactement ce texte, proprement indenté.
Pourquoi ça marche — Chaque docstring dit quelque chose que la SIGNATURE ne dit pas : « order does not matter » (l'appelant n'a pas à trier), « always as a float » (pas de surprise de type), les seuils exacts du barème (sinon il faut lire cinq if). C'est le critère : si la docstring n'apprend rien de plus que le nom, elle est du bruit.
Erreur classique sur cet exercice — La docstring-écho : """Compute the median of values.""" sous def median(values) — zéro information ajoutée. Et son opposée, la docstring-roman qui documente le COMMENT (« sorts the list, then takes the middle index... ») : l'implémentation peut changer demain, la docstring décrit le contrat, pas la mécanique.
Variante plus difficile — Ajoute une docstring de MODULE (première ligne du fichier stats.py, avant les imports) : une phrase sur ce que le module regroupe. Puis tape python -c "import stats; help(stats)" : module ET fonctions, toute la doc est là — sans ouvrir le code.
Exercice facile b — triage de commentaires
Raisonnement — Le test du masquage : cache le commentaire, relis le code. Si rien ne manque → BRUIT. Si une information disparaît (une raison, une contrainte, un choix non évident) → UTILE. Les noms clairs de la leçon 08-1 rendent la plupart des commentaires-paraphrases redondants.
Solution
MAX_TITLE_LENGTH = 50 # longueur maximale d'un titre
# BRUIT — le nom de la constante dit deja tout, mot pour mot.
title = title.strip() # enleve les espaces
# BRUIT — paraphrase de strip(). Le lecteur de niveau 08 connait strip().
if not title: # si le titre est vide
# BRUIT — paraphrase du code. (Detail : apres strip(), "vide" couvre
# aussi les titres blancs — si on veut le dire, c'est un commentaire
# POURQUOI : « strip first: blank titles must be rejected too ».)
title = title[:MAX_TITLE_LENGTH - 3] + "..." # -3 : place pour "..."
# UTILE — explique le nombre magique -3. Sans lui, le lecteur doit
# deviner pourquoi 3. (Encore mieux : le supprimer en nommant,
# voir variante.)
tasks.append({"title": title, "done": False}) # ajoute la tache
# BRUIT — append qui dit « ajoute ».
return tasks # renvoie la liste
# BRUIT — le champion du monde de la paraphrase.
Bilan : cinq BRUIT supprimés, un UTILE conservé. Le fichier devient plus court ET plus lisible — le seul commentaire restant attire enfin l'œil.
Pourquoi ça marche — Un commentaire est une dette : il doit être maintenu à chaque modification du code qu'il annote, sinon il ment. On ne garde que ceux qui paient un loyer : expliquer un pourquoi invisible. Les cinq supprimés ne paient rien — ils répètent le code en français.
Erreur classique sur cet exercice — Garder « au cas où » les paraphrases « qui ne font pas de mal ». Elles en font : elles diluent. Un lecteur qui a lu trois commentaires inutiles ne lit plus le quatrième — qui était le bon.
Variante plus difficile — Élimine même le commentaire UTILE en nommant :
ELLIPSIS = "..."
title = title[:MAX_TITLE_LENGTH - len(ELLIPSIS)] + ELLIPSIS
Le -3 s'explique tout seul. La hiérarchie des solutions : un bon nom > un bon commentaire > un mauvais commentaire. (C'est le geste appliqué dans l'exercice moyen a.)
Exercice facile c — renommage propre, puis sabotage d'import
Raisonnement — Un renommage public touche TROIS endroits : la définition, les appelants du projet, les tests. La procédure : suite verte → recherche globale de grade_to_mention (pour connaître tous les sites AVANT de toucher) → renommer partout → suite verte. Puis, pour apprivoiser l'erreur : défaire exprès la mise à jour du test et regarder l'ImportError en face.
Solution — Après renommage complet, suite verte (11 tests inchangés sur le fond : seuls l'import et les appels ont changé de nom). Puis sabotage — le test réimporte l'ancien nom :
=================================== ERRORS ====================================
________________________ ERROR collecting test_grades.py ______________________
Traceback:
test_grades.py:3: in <module>
from grades import grade_to_mention
E ImportError: cannot import name 'grade_to_mention' from 'grades' (C:\...\grades.py)
=========================== short test summary info ===========================
ERROR test_grades.py
!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
Lecture : ERROR collecting (leçon 08-2) — rien n'a tourné ; la ligne E nomme le disparu et le module. Correction : réaligner l'import. Re-vert.
Pourquoi ça marche — L'ImportError de renommage est l'erreur la plus FRANCHE du refactoring : immédiate, localisée, impossible à rater. C'est pour ça qu'un renommage public est un geste sûr QUAND la suite existe — le danger, c'est le renommage sans tests, où l'appelant oublié ne crashe qu'au moment où un utilisateur passe par ce chemin.
Erreur classique sur cet exercice — Renommer via des remplacements texte trop larges : remplacer grade partout transforme aussi grade_to_mention en note_to_mention, les variables locales, les messages d'erreur… et le match="between 0 and 20" peut se retrouver à tester un message modifié. Renomme le SYMBOLE exact (mot entier, casse exacte), et relis le diff avant de relancer.
Variante plus difficile — VS Code sait faire ce geste tout seul : clic droit sur le nom → « Rename Symbol » (F2) — il met à jour définition, appelants et imports d'un coup. Refais le renommage inverse avec F2 et compare avec ta version manuelle. (L'outil est fiable sur un projet bien structuré — encore un dividende de la leçon 08-1.)
Exercice moyen a — trois gestes sous filet
Raisonnement — D'abord le filet : les deux fonctions sont pures, donc testables telles quelles — nominal, frontière de troncature (30 pile / 31), liste vide, et le tri pour f2. ENSUITE les gestes, un par un, suite relancée entre chaque. La frontière 30/31 est le test le plus important : la troncature est pleine d'erreurs « off-by-one » possibles.
Solution
Le filet d'abord (il teste le COMPORTEMENT, valable avant comme après) :
# ex-08-4-d/test_format.py
from format_titles import format_titles, format_titles_sorted
def test_short_title_unchanged():
assert format_titles(["short"]) == ["short"]
def test_title_of_exactly_30_chars_unchanged():
assert format_titles(["a" * 30]) == ["a" * 30]
def test_title_of_31_chars_truncated_to_30():
assert format_titles(["a" * 31]) == ["a" * 27 + "..."]
def test_empty_list():
assert format_titles([]) == []
def test_sorted_variant_sorts():
assert format_titles_sorted(["b", "a"]) == ["a", "b"]
Geste 1 — renommer (f1 → format_titles, f2 → format_titles_sorted, t → titles, x → title, r → formatted). Suite : verte. Geste 2 — extraire le doublon :
def truncate_title(title):
if len(title) > 30:
return title[:27] + "..."
return title
def format_titles(titles):
formatted = []
for title in titles:
formatted.append(truncate_title(title))
return formatted
def format_titles_sorted(titles):
# sorted() of the formatted list: we sort what the user will SEE
# (truncated titles), not the raw input
return sorted(format_titles(titles))
Suite : verte. Geste 3 — les constantes :
MAX_DISPLAY_LENGTH = 30
ELLIPSIS = "..."
def truncate_title(title):
if len(title) > MAX_DISPLAY_LENGTH:
return title[:MAX_DISPLAY_LENGTH - len(ELLIPSIS)] + ELLIPSIS
return title
Suite : verte. Trois commits : refactor: rename to explicit names, refactor: extract truncate_title, refactor: name magic numbers.
Le piège de l'énoncé : format_titles_sorted appelle-t-elle format_titles ou refait-elle sa boucle sur truncate_title ? Les deux sont vertes. Choix retenu : appeler format_titles, car la relation « la version triée EST la version normale, triée » est une vérité du domaine qu'on veut inscrire dans le code — si le formatage évolue, le tri suit automatiquement. Le commentaire pourquoi sur le sorted documente l'autre subtilité (on trie APRÈS troncature — comportement hérité de f2, préservé par le refactoring).
Pourquoi ça marche — À aucun moment le comportement n'a changé : la preuve, la suite écrite AVANT le premier geste est restée verte jusqu'au bout sans qu'on la touche. Remarque le remplacement de 27 par MAX_DISPLAY_LENGTH - len(ELLIPSIS) : le 27 était un nombre magique DÉRIVÉ (30 - 3) — le nommer indépendamment (TRUNCATED_LENGTH = 27) aurait figé la dérivation ; l'exprimer comme calcul la rend inviolable (change la longueur max, tout suit).
Erreur classique sur cet exercice — Faire les trois gestes d'un coup puis lancer la suite « à la fin pour vérifier ». Si elle est verte, tu as eu de la chance ; si elle est rouge, te voilà avec trois suspects — exactement la situation que la règle « un geste = une relance » rend impossible. La DISCIPLINE est l'objet de l'exercice, pas le code final.
Variante plus difficile — Nouveau besoin : tronquer à 40 pour un écran large. Transforme truncate_title pour accepter la limite en paramètre avec une valeur par défaut (def truncate_title(title, max_length=MAX_DISPLAY_LENGTH)) — est-ce encore un refactoring ? (Non : la signature publique change, c'est une ÉVOLUTION — nouveau test, commit feat:, pas refactor:. Savoir nommer la nature de son geste fait partie de la compétence.)
Exercice moyen b — le README et son test d'acceptation
Raisonnement — Écrire les cinq blocs est la partie facile ; l'exercice est le REJEU : dérouler soi-même, dans un dossier vierge et un terminal frais, chaque commande du README, dans l'ordre, sans rien faire qui n'y soit pas écrit. Chaque accroc (commande qui échoue, étape implicite, fichier manquant) se corrige dans le README, pas de tête.
Solution — Un README type (à adapter à TON projet) :
# Task Manager
Gestionnaire de taches en ligne de commande : ajout, completion,
suppression, liste — avec sauvegarde automatique dans un fichier JSON.
## Installation
git clone https://github.com/<user>/task-manager.git
cd task-manager
Aucune dependance externe (Python 3.10+ requis). Pour lancer les tests :
python -m pip install pytest
## Usage
python main.py
Le fichier de sauvegarde `tasks.json` est cree automatiquement au premier
lancement, dans le dossier courant.
## Exemple
=== Task Manager ===
1. Ajouter 2. Terminer 3. Lister 4. Quitter
> 3
[x] write tests
[ ] refactor storage module
Fait : 1/2
## Tests
python -m pytest
Et le compte rendu type d'un rejeu honnête (journal) :
Rejeu README, dossier vide, terminal frais :
- git clone : OK
- python main.py : ECHEC — "python" ouvre le Store sur cette machine
-> corrige : note ajoutee sur py/python selon l'installation Windows
- premier lancement : le programme a crashe, tasks.json absent
-> BUG (pas un trou de README !) : load_tasks ne gere plus le fichier
absent depuis le refactoring. Test ajoute (lecon 08-3), corrige.
- python -m pytest : OK, 14 passed
Pourquoi ça marche — Le rejeu détecte les DEUX familles de problèmes : les trous de documentation (étape implicite dans ta tête, absente du fichier) et — bonus fréquent — de vrais bugs de « premier lancement » que tu ne vois jamais parce que TON dossier a toujours ses fichiers de données. La phrase sur tasks.json dans le bloc Usage vient typiquement d'un rejeu.
Erreur classique sur cet exercice — Rejouer dans le dossier du projet existant (avec ses données, son cache, son historique) : tout marche, forcément. Le test d'acceptation n'a de valeur QUE depuis le clone frais — c'est la situation de l'inconnu qu'on simule.
Variante plus difficile — Fais rejouer le README par quelqu'un d'autre (ou par une IA en lui donnant UNIQUEMENT le README et l'accès au dépôt) avec interdiction de te poser des questions. Chaque question qui arrive quand même est une ligne à ajouter.
Exercice difficile a — le grand ménage
Raisonnement — Pas de solution unique (c'est TON code), mais une procédure vérifiable. L'ordre est la moitié de l'exercice : filet d'abord (quitte à faire le refactoring 08-1 « sans filet » pour rendre testable — avec vérification manuelle rigoureuse avant/après), puis gestes atomiques protégés, en commençant par les renommages (le geste le moins risqué, qui révèle la structure et fait souvent apparaître le découpage tout seul).
Solution — Un déroulé type sur une fonction réelle de quiz (avant : 48 lignes, trois commentaires # etape, calcul et affichage mêlés) :
Commit 0 test: add characterization tests for run_quiz scoring
(7 tests sur compute_score, extraite au prealable — le print
etait dans la boucle de score : refactoring 08-1 d'abord,
verification manuelle sur trois parties completes)
Commit 1 refactor: rename q/s/n to question/score/attempts [suite verte]
Commit 2 refactor: extract pick_questions from run_quiz [suite verte]
Commit 3 refactor: extract format_result from run_quiz [suite verte]
(le commentaire "# etape 3 : affichage du resultat" est mort,
la fonction porte son nom)
Commit 4 refactor: replace magic 3 with MAX_ATTEMPTS [suite verte]
Commit 5 docs: add docstrings to quiz module functions
Après : run_quiz fait 12 lignes et se lit comme un sommaire ; trois fonctions pures nouvelles, chacune avec docstring et tests.
Pourquoi ça marche — Les tests écrits au commit 0 sont des « tests de caractérisation » : ils photographient le comportement ACTUEL (même s'il est imparfait) pour garantir que le ménage ne change rien. C'est différent des tests de la leçon 08-3 (qui verrouillent un comportement VOULU) — et c'est l'outil standard pour aborder du code existant sans spécification.
Erreur classique sur cet exercice — Découvrir pendant le ménage que le comportement actuel est FAUX (un arrondi bizarre, un doublon compté deux fois) et le corriger « tant qu'on y est ». Non : note-le, finis le refactoring à comportement constant, PUIS corrige dans un commit séparé (fix:) avec son test rouge-puis-vert (leçon 08-3). Deux casquettes, jamais en même temps.
Variante plus difficile — Mesure le résultat : compare git diff --stat entre le premier et le dernier commit, compte les fonctions avant/après, et fais relire la version finale à toi-du-lendemain. Si une fonction dépasse encore 25 lignes, demande-toi quel # etape invisible s'y cache encore.
Mini-projet — le projet présentable
Le résultat dépend de ton projet ; voici la grille d'auto-correction, point par point, avec le niveau d'exigence attendu :
1. Filet — python -m pytest : vert, et la suite couvre les fonctions de calcul (si une fonction publique de logique n'a AUCUN test, le refactoring de l'étape 2 se fera sans filet sur elle — corrige d'abord).
2. Refactoring final — La revue se fait fonction par fonction, avec la grille : nom PEP 8 explicite ? une responsabilité ? testable ? Chaque geste suit le cycle vert → geste → vert → commit. Attendu réaliste : 3 à 8 gestes sur un projet de niveau 07, pas 40 — on refactore ce qui gêne, pas tout.
3. Docstrings — Module : une phrase en tête de chaque .py. Fonctions publiques : une ligne pour les triviales, format Args/Returns/Raises pour celles qui ont un contrat. Vérification : help(module) dans le REPL se lit sans honte.
4. Commentaires — Après triage, chaque survivant répond à « pourquoi » et tu peux le défendre à voix haute. Densité attendue : peu. Un fichier de niveau 07 bien nommé et bien découpé a besoin de TRÈS peu de commentaires — s'il en reste beaucoup, c'est souvent un défaut de nommage déguisé.
5. README — Les cinq blocs, ET la preuve du rejeu (note de journal). L'exemple du bloc « Exemple » est une session réelle copiée-collée, pas rédigée de mémoire.
6. La relecture finale du diff — git log --oneline du niveau raconte une histoire lisible : test:, refactor:, docs:, fix: — quelqu'un peut suivre ce qui s'est passé sans toi.
Erreur classique — Confondre « présentable » et « parfait » : repousser la fin du niveau parce qu'« il reste des choses à améliorer ». Il en restera toujours. Le critère est celui de l'inconnu : comprendre, installer, lancer, tester — sans une question. Quand c'est atteint, le niveau est FINI ; les améliorations suivantes attendront un besoin réel.
Variante plus difficile — Publie le dépôt sur GitHub (niveau 00) et mets le lien dans ton journal. Un projet testé, documenté, refactoré et public : c'est exactement l'artefact qu'on montre — et le standard par défaut de tous tes projets à partir du niveau 09.