09 — Carnet de contacts
Niveau requis : 5 pour la v1, 6 pour la v2 / Difficulté : 4/5 / Durée estimée : 3 h (v1) + 3 h (v2)
Objectif
Un carnet d'adresses en ligne de commande : ajouter un contact (nom, téléphone, email), lister, rechercher par nom, modifier, supprimer. Les contacts sont sauvegardés en JSON et survivent aux relances.
Deux versions :
- v1 (niveau 5) — dicts + JSON, style procédural avec fonctions. C'est le grand frère du gestionnaire de tâches (projet 06), en plus complet.
- v2 (niveau 6) — refonte POO : une classe
Contactet une classeContactBook. MÊME comportement extérieur. Le but est de sentir ce que la POO change — et ce qu'elle ne change pas.
C'est le projet le plus exigeant avant les niveaux 9-10 : prends les deux sessions au sérieux.
Notions utilisées
- Fonctions, découpage en responsabilités — niveau 4
- JSON :
load/dump,FileNotFoundError— niveau 5 - Recherche insensible à la casse dans des chaînes (
in,lower()) — niveau 3 - v2 : classes,
__init__, attributs, méthodes — niveau 6 - v2 :
__str__pour l'affichage d'un objet — niveau 6 - v2 : conversion objet ↔ dict pour le JSON (
to_dict()/from_dict()) — niveau 6 (c'est LE pont difficile de la v2)
Cahier des charges
Commun :
- Fichiers :
contacts.py(v1),contacts_v2.py(v2), données danscontacts.json. - Un contact = nom (obligatoire, non vide), téléphone, email.
- Menu : ajouter / lister / rechercher / modifier / supprimer / quitter.
- La recherche matche une sous-chaîne du nom, insensible à la casse (« mar » trouve « Marie » et « Omar »).
- Pas de doublon : refuser un ajout dont le nom (insensible à la casse) existe déjà.
- Modifier : proposer champ par champ, Entrée vide = garder la valeur actuelle.
- Sauvegarde après chaque modification ; premier lancement sans fichier OK.
Spécifique v2 :
- Classe
Contact: attributsname,phone,email; méthodes__str__(),to_dict(), et méthode de classe ou fonctionfrom_dict(d). - Classe
ContactBook: détient la liste deContact; méthodesadd(),find(query),remove(name),save(path),load(path). - Le menu (interface) ne manipule JAMAIS de dicts directement : uniquement des objets et les méthodes du carnet.
- Comportement extérieur identique à la v1, y compris le format de
contacts.json(tes données v1 doivent se charger telles quelles en v2 — c'est un vrai test de migration).
Étapes de construction
v1 — niveau 5
- Structure de données + listing : liste de dicts en dur, fonction
show_contacts(contacts). Lance, vérifie l'affichage. - Menu en boucle +
add_contact(contacts)avec validation du nom non vide et refus des doublons. Teste le doublon avec des casses différentes. find_contacts(contacts, query): retourne une LISTE de résultats (pas le premier trouvé), affichage à part. Teste « mar », « MAR », requête sans résultat.- Persistance :
save_contacts(contacts)/load_contacts()sur le modèle du projet 06 (JSON,with,FileNotFoundError→[]). Sauvegarde après chaque changement. Teste le cycle quitter/relancer. edit_contact(champ par champ, Entrée = conserver) etdelete_contact(par nom exact, avec confirmationo/n). Teste tout le menu. Commit v1.
v2 — niveau 6
- Dans
contacts_v2.py, écris la classeContactseule :__init__,__str__,to_dict,from_dict. Teste-la dans le REPL ou sousif __name__ == "__main__":— créer, afficher, convertir aller-retour (Contact.from_dict(c.to_dict())). - Écris
ContactBookavecadd,find,removeen mémoire (sans JSON). Teste ses méthodes directement, sans menu. - Ajoute
save(path)/load(path)au carnet :saveconvertit chaque contact avecto_dict,loadreconstruit avecfrom_dict. Charge toncontacts.jsonde la v1 : il doit passer tel quel. - Rebranche le menu de la v1 sur le carnet (l'interface appelle les méthodes, ne touche plus aux dicts). Déroule le même scénario complet que la v1 et compare. Commit v2.
Extensions possibles
- Trier le listing par nom (
sortedaveckey=). - Valider grossièrement l'email (contient
@et un point après) — la vraie validation est un puits sans fond, reste simple. - Export CSV (
csv.writer) pour ouvrir le carnet dans un tableur. - v2 : une méthode
merge(other_book)qui fusionne deux fichiers de contacts en gérant les doublons — bon exercice de conception.
Erreurs fréquentes sur ce projet
- v2 :
TypeError: Object of type Contact is not JSON serializable—json.dumpne sait pas sérialiser tes objets. C'est exactement le rôle deto_dict(): JSON ne transporte que des types de base. - v2 : oublier
selfdans une méthode :TypeError: add() takes 1 positional argument but 2 were given. Ce message obscur signifie presque toujours « il manqueself». - Modifier une copie au lieu de l'objet :
contact = contacts[i]puis réaffectercontact = {...}ne change PAS la liste. Modifier les champs (contact["phone"] = ...) si. La différence entre lier un nouveau nom et muter l'objet — révision niveau 3. - Recherche qui retourne le premier résultat au lieu de tous : « mar » doit lister Marie ET Omar. Retourne une liste, toujours.
- Comparaison de doublons sensible à la casse : « marie » et « Marie » passent tous les deux. Normalise (
lower()) au moment du TEST, sans altérer le nom stocké. - v2 : mettre des
print()/input()dansContactouContactBook: les classes portent les données et la logique, l'interface parle. Même règle qu'au niveau 4 pour les fonctions.
Critères de réussite
- v1 : les 6 commandes du menu fonctionnent, données persistées à chaque changement.
- v1 : doublons refusés quelle que soit la casse ; recherche partielle correcte.
- v2 : le
contacts.jsoncréé par la v1 se charge sans modification. - v2 : même scénario de test → même comportement que la v1.
- v2 : aucune manipulation de dict dans le code du menu.
- Je peux expliquer à voix haute ce que la POO a simplifié ici (et ce qu'elle a alourdi — réponse honnête attendue).
Commit conseillé
git add projects/mini-projects/09-carnet-de-contacts/
git commit -m "project: add contact book"
# ... puis, apres la v2 :
git commit -m "project: add contact book OOP version"
Puis entrée de journal + git push. Comme au projet 06 : contacts.json dans .gitignore si tu y mets de vrais contacts.