# Chapitre 09 — Modules et bibliothèque standard > **Durée estimée** : 4 à 5 heures (cours + exercices + mini-projet) > > **Prérequis** : [Chapitre 08 — Erreurs et débogage](08-erreurs-et-debogage.md) (et en particulier les fonctions du [chapitre 05](05-fonctions.md)). > > **Objectifs** — À la fin de ce chapitre, tu sauras : > - importer un module de plusieurs façons (`import`, `from … import`, `as`) et choisir la bonne ; > - découper ton propre programme en plusieurs fichiers (modules) et en dossiers (paquets) ; > - comprendre et utiliser `if __name__ == "__main__":` ; > - utiliser les modules incontournables de la bibliothèque standard : `random`, `math`, `datetime`, `os`, `sys`, `collections` ; > - explorer un module inconnu avec `dir()`, `help()` et la documentation officielle ; > - éviter les pièges classiques (fichier nommé `random.py`, import circulaire…). --- ## Cours ### 1. Pourquoi des modules ? Jusqu'ici, tous tes programmes tenaient dans **un seul fichier**. C'est parfait pour 50 lignes. Mais un vrai programme en fait vite 500, 5 000 ou 50 000. Tout mettre dans un seul fichier, ce serait comme ranger toutes tes affaires dans un unique tiroir géant : on ne retrouve plus rien. Un **module**, c'est simplement **un fichier `.py`** qui contient des fonctions, des variables (et plus tard des classes) qu'on peut **réutiliser ailleurs**. Les modules servent à : - **organiser** : un fichier par thème (`calculs.py`, `affichage.py`, `sauvegarde.py`) ; - **réutiliser** : écrire une fonction une fois, l'utiliser dans dix programmes ; - **profiter du travail des autres** : Python est livré avec des centaines de modules prêts à l'emploi. Cette collection de modules livrés avec Python s'appelle la **bibliothèque standard**. On dit souvent que Python est livré « piles incluses » (*batteries included*) : pas besoin d'installer quoi que ce soit pour tirer des nombres au hasard, manipuler des dates, lire du JSON ou calculer une racine carrée. > 💡 **Analogie** : la bibliothèque standard, c'est une boîte à outils fournie avec ta maison. Un module, c'est un tiroir de cette boîte (le tiroir « dates », le tiroir « hasard »…). `import`, c'est ouvrir le tiroir pour y prendre ce dont tu as besoin. Tu en as déjà utilisé sans le savoir : `json` et `csv` au [chapitre 07](07-fichiers-csv-json.md), `pathlib` aussi. Ce chapitre t'explique enfin ce qui se passe vraiment derrière `import`. ### 2. Importer un module #### `import module` La forme la plus simple et la plus courante : ```python import math print(math.sqrt(16)) print(math.pi) ``` ``` 4.0 3.141592653589793 ``` `import math` charge le module et crée une variable `math` qui le représente. Pour utiliser son contenu, on écrit **`module.nom`** (notation pointée). L'avantage est énorme : en lisant `math.sqrt`, tu sais immédiatement d'où vient `sqrt`. #### `from module import nom` Si tu utilises souvent les mêmes fonctions, tu peux les importer directement : ```python from math import sqrt, pi print(sqrt(16)) print(pi) ``` ``` 4.0 3.141592653589793 ``` Plus court à écrire, mais on perd l'information « ça vient de `math` ». Et attention : seuls les noms listés sont importés. `math` lui-même n'existe pas dans ce programme. #### Renommer avec `as` Tu peux donner un **alias** à un module ou à une fonction : ```python import statistics as stats from datetime import datetime as dt print(stats.mean([12, 15, 9, 18])) print(type(dt.now()).__name__) ``` ``` 13.5 datetime ``` Certains alias sont des conventions universelles que tu rencontreras au [chapitre 14](14-specialisations.md) : `import pandas as pd`, `import numpy as np`. #### `from module import *` : à éviter ```python from math import * from statistics import * ``` L'étoile importe **tout** le contenu du module. Problème : tu ne sais plus d'où vient chaque nom, et si deux modules ont une fonction du même nom, la seconde écrase la première **en silence**. Réserve cette forme à l'interpréteur interactif pour tester rapidement. #### Quelle forme choisir ? | Forme | Quand l'utiliser | |---|---| | `import math` | Par défaut. Clair et sans conflit. | | `from math import sqrt` | Pour 1 à 3 noms utilisés très souvent. | | `import statistics as stats` | Pour un nom de module long, ou une convention connue. | | `from math import *` | Jamais dans un vrai programme. | **Convention** (PEP 8, voir [chapitre 13](13-tests-pytest-bonnes-pratiques.md)) : les `import` se placent **tout en haut du fichier**, un module par ligne, dans cet ordre : bibliothèque standard, puis bibliothèques installées, puis tes propres modules, séparés par une ligne vide. ```python import json import random from pathlib import Path from mes_outils import nettoyer_texte ``` > ⚠️ Cet extrait suppose qu'un fichier `mes_outils.py` existe : il illustre seulement l'ordre des imports. ### 3. Explorer un module : `dir()`, `help()` et la documentation Comment savoir ce que contient un module ? Trois outils : **`dir(module)`** liste tout ce qu'il contient : ```python import math print(dir(math)[:12]) ``` ``` ['__doc__', '__loader__', '__name__', '__package__', '__spec__', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh'] ``` Les noms entourés de doubles tirets bas (`__doc__`…) sont des éléments techniques ; ignore-les pour l'instant. **`help(fonction)`** affiche la documentation (la *docstring*, vue au [chapitre 05](05-fonctions.md)). Essaie dans l'interpréteur interactif (`python` dans le terminal) : ``` >>> import math >>> help(math.sqrt) Help on built-in function sqrt in module math: sqrt(x, /) Return the square root of x. ``` Appuie sur `q` pour quitter l'aide. **La documentation officielle**, en français : . Chaque module y a sa page, avec des exemples. Prends l'habitude d'y aller : c'est **la** référence. Dans VS Code, survoler une fonction ou faire **Ctrl+clic** dessus affiche aussi sa documentation. ### 4. Créer ton propre module Créer un module, c'est… créer un fichier `.py`. Rien de plus. Prenons un dossier de projet avec deux fichiers **côte à côte** : ``` mon_projet/ ├── geometrie.py └── main.py ``` `geometrie.py` : ```python """Petites fonctions de géométrie.""" import math PI_ARRONDI = 3.14 def aire_cercle(rayon): """Renvoie l'aire d'un cercle.""" return math.pi * rayon**2 def perimetre_rectangle(longueur, largeur): """Renvoie le périmètre d'un rectangle.""" return 2 * (longueur + largeur) ``` `main.py` : ```python import geometrie print(f"{geometrie.aire_cercle(2):.2f}") print(geometrie.perimetre_rectangle(3, 5)) print(geometrie.PI_ARRONDI) ``` En lançant `python main.py` depuis le dossier `mon_projet` : ``` 12.57 16 3.14 ``` Points importants : - On écrit `import geometrie` **sans** l'extension `.py`. - Le nom du fichier doit être un **nom de variable valide** : lettres minuscules, chiffres, tirets bas. `mon-module.py` ou `2outils.py` ne pourront pas être importés (`import mon-module` provoque une `SyntaxError`). - Le module a sa propre « boîte » de noms : la variable `PI_ARRONDI` de `geometrie` ne se mélange pas avec celles de `main.py`. On parle d'**espace de noms**. - Un module peut lui-même importer d'autres modules (`geometrie` importe `math`). #### Où Python cherche-t-il les modules ? Quand tu écris `import geometrie`, Python cherche dans l'ordre : 1. **le dossier du script lancé** (ici `mon_projet/`) ; 2. les dossiers de la bibliothèque standard ; 3. les dossiers des bibliothèques installées avec `pip` (voir [chapitre 12](12-outils-venv-pip-git.md)). Il prend **le premier fichier trouvé**. Tu peux voir cette liste de dossiers avec `sys.path` : ```python import sys for dossier in sys.path: print(dossier) ``` Le premier dossier de la liste est celui de ton script. Retiens bien ce point, il explique le piège le plus célèbre de ce chapitre (fichier nommé `random.py`, voir les pièges). #### Un module n'est exécuté qu'une fois Lors du premier `import`, Python **exécute tout le fichier** du module, de haut en bas, puis le garde en mémoire. Les `import` suivants du même module (dans le même programme) réutilisent la version déjà chargée, sans le réexécuter. ### 5. `if __name__ == "__main__":` Puisque `import` exécute tout le fichier, que se passe-t-il si ton module contient du code de test ? `temperatures.py` : ```python def celsius_vers_fahrenheit(celsius): return celsius * 9 / 5 + 32 print("Test :", celsius_vers_fahrenheit(100)) ``` `meteo.py` : ```python import temperatures print(f"Il fait {temperatures.celsius_vers_fahrenheit(20)} °F") ``` ``` Test : 212.0 Il fait 68.0 °F ``` La ligne `Test : 212.0` s'affiche alors qu'on ne l'a pas demandée : elle vient de l'exécution de `temperatures.py` au moment de l'import. Gênant. La solution est une variable spéciale, **`__name__`**, que Python crée automatiquement dans chaque fichier : - si le fichier est **lancé directement** (`python temperatures.py`), `__name__` vaut `"__main__"` ; - si le fichier est **importé**, `__name__` vaut le nom du module (`"temperatures"`). D'où ce schéma, que tu verras dans presque tous les fichiers Python : ```python def celsius_vers_fahrenheit(celsius): return celsius * 9 / 5 + 32 if __name__ == "__main__": # Ce bloc ne s'exécute QUE si on lance ce fichier directement print("Test :", celsius_vers_fahrenheit(100)) ``` Maintenant : - `python temperatures.py` affiche `Test : 212.0` ; - `python meteo.py` affiche seulement `Il fait 68.0 °F`. > 💡 **Analogie** : un module, c'est comme un chef cuisinier. Quand on l'invite dans une autre cuisine (import), il apporte ses recettes mais ne se met pas à cuisiner tout seul. Quand il est dans sa propre cuisine (lancé directement), il peut faire une démonstration. Une organisation très répandue consiste à regrouper le programme principal dans une fonction `main()` : ```python def saluer(nom): return f"Bonjour {nom} !" def main(): nom = input("Ton prénom : ") print(saluer(nom)) if __name__ == "__main__": main() ``` Avec l'entrée `Léa` : ``` Ton prénom : Léa Bonjour Léa ! ``` Ainsi, un autre fichier peut faire `from salutations import saluer` sans déclencher le `input()`. ### 6. Les paquets : des modules rangés dans des dossiers Quand tu as beaucoup de modules, tu peux les regrouper dans un **dossier** : c'est un **paquet** (*package*). On y ajoute traditionnellement un fichier (souvent vide) nommé `__init__.py`, qui indique « ce dossier est un paquet Python ». ``` jeu/ ├── main.py └── outils/ ├── __init__.py ├── des.py └── affichage.py ``` `outils/des.py` : ```python import random def lancer_de(faces=6): return random.randint(1, faces) ``` `outils/affichage.py` : ```python def cadre(texte): bordure = "+" + "-" * (len(texte) + 2) + "+" return f"{bordure}\n| {texte} |\n{bordure}" ``` `main.py` : ```python from outils.des import lancer_de from outils import affichage resultat = lancer_de() print(affichage.cadre(f"Tu as fait {resultat}")) ``` Exemple de sortie (le nombre change à chaque exécution) : ``` +--------------+ | Tu as fait 4 | +--------------+ ``` On accède à un module d'un paquet avec un **point** : `outils.des`, comme un chemin de dossier. Tu peux imbriquer des paquets dans des paquets. La structure complète d'un projet est détaillée au [chapitre 13](13-tests-pytest-bonnes-pratiques.md). ### 7. `random` : le hasard Le module `random` génère des nombres **pseudo-aléatoires** (calculés par un algorithme, mais imprévisibles en pratique). ```python import random print(random.randint(1, 6)) # entier entre 1 et 6, bornes INCLUSES print(random.random()) # décimal dans [0, 1[ print(random.uniform(1.5, 3.5)) # décimal entre 1.5 et 3.5 couleurs = ["rouge", "vert", "bleu", "jaune"] print(random.choice(couleurs)) # un élément au hasard print(random.choices(couleurs, k=5)) # 5 tirages AVEC remise (doublons possibles) print(random.sample(range(1, 50), 6)) # 6 tirages SANS remise (loto) cartes = ["As", "Roi", "Dame", "Valet", "10"] random.shuffle(cartes) # mélange la liste SUR PLACE (renvoie None) print(cartes) ``` Exemple de sortie (différente à chaque exécution) : ``` 6 0.16931005030681068 2.6574891618278063 bleu ['jaune', 'bleu', 'jaune', 'jaune', 'jaune'] [7, 34, 25, 47, 17, 40] ['Dame', 'As', '10', 'Valet', 'Roi'] ``` > ⚠️ Contrairement à `range(1, 6)` qui s'arrête à 5, **`randint(1, 6)` inclut le 6**. C'est une exception à la règle habituelle de Python. #### Tirage pondéré `choices` accepte des poids : ici « pile » a 9 fois plus de chances de sortir que « face ». ```python import random print(random.choices(["pile", "face"], weights=[9, 1], k=10)) ``` #### Rendre le hasard reproductible : `seed` Pour déboguer ou tester, on veut parfois **le même hasard** à chaque exécution. `random.seed(n)` fixe le point de départ de l'algorithme : ```python import random random.seed(2024) print([random.randint(1, 100) for _ in range(5)]) random.seed(2024) print([random.randint(1, 100) for _ in range(5)]) ``` ``` [61, 24, 94, 75, 39] [61, 24, 94, 75, 39] ``` Même graine, mêmes nombres. Très pratique pour écrire des tests. > ⚠️ **Sécurité** : `random` est prévisible si on connaît la graine. Pour des **mots de passe** ou des jetons secrets, utilise le module **`secrets`**, conçu pour la cryptographie : `secrets.choice(...)`, `secrets.randbelow(n)`, `secrets.token_hex(16)`. On s'en servira dans le mini-projet. ### 8. `math` : les mathématiques ```python import math print(math.pi, math.e) print(math.sqrt(2)) # racine carrée print(math.floor(3.7), math.ceil(3.2)) # arrondi inférieur / supérieur print(math.floor(-3.7), int(-3.7)) # attention aux négatifs ! print(math.factorial(5)) # 5! = 120 print(math.gcd(12, 18), math.lcm(4, 6)) # PGCD et PPCM print(math.hypot(3, 4)) # hypoténuse : racine de 3² + 4² print(math.dist((0, 0), (3, 4))) # distance entre deux points print(math.log10(1000), math.log2(8)) # logarithmes print(math.cos(math.pi)) # trigonométrie (en radians) print(math.degrees(math.pi / 2), math.radians(180)) ``` ``` 3.141592653589793 2.718281828459045 1.4142135623730951 3 4 -4 -3 120 6 12 5.0 5.0 3.0 3.0 -1.0 90.0 3.141592653589793 ``` `math.floor` arrondit **vers le bas** (vers moins l'infini) alors que `int()` **tronque** (supprime la partie décimale) : pour `-3.7`, on obtient `-4` et `-3`. #### Comparer des décimaux : `math.isclose` Tu te souviens de l'imprécision des `float` ([chapitre 01](01-variables-types-operateurs.md)) ? `math.isclose` permet de comparer deux décimaux « à peu près » : ```python import math print(0.1 + 0.2 == 0.3) print(math.isclose(0.1 + 0.2, 0.3)) ``` ``` False True ``` #### En bonus : `statistics` Pour les calculs statistiques simples, inutile de tout recoder : ```python import statistics notes = [12, 15, 9, 18] print(statistics.mean(notes)) # moyenne print(statistics.median(notes)) # médiane print(round(statistics.stdev(notes), 2)) # écart-type print(statistics.mode([1, 2, 2, 3])) # valeur la plus fréquente ``` ``` 13.5 13.5 3.87 2 ``` ### 9. `datetime` : dates et heures Le module `datetime` contient plusieurs **types** (attention, l'un d'eux porte le même nom que le module !) : | Type | Représente | Exemple | |---|---|---| | `date` | une date (jour, mois, année) | 14 juillet 2025 | | `datetime` | une date **et** une heure | 31/12/2025 à 23:59:30 | | `timedelta` | une **durée** | 30 jours, 2 heures | On les importe généralement ainsi : ```python from datetime import date, datetime, timedelta ``` #### Créer et lire une date ```python from datetime import date, datetime fete = date(2025, 7, 14) # année, mois, jour print(fete) print(fete.year, fete.month, fete.day) print(fete.weekday()) # 0 = lundi, 6 = dimanche reveillon = datetime(2025, 12, 31, 23, 59, 30) print(reveillon) print(reveillon.hour, reveillon.minute) print(type(date.today()).__name__) # la date du jour print(type(datetime.now()).__name__) # la date et l'heure actuelles ``` ``` 2025-07-14 2025 7 14 0 2025-12-31 23:59:30 23 59 date datetime ``` #### Calculer avec les dates : `timedelta` Additionner une durée à une date, ou soustraire deux dates, c'est là que `datetime` brille. Il gère pour toi les mois de 28, 30 ou 31 jours et les années bissextiles : ```python from datetime import date, datetime, timedelta fete = date(2025, 7, 14) print(fete + timedelta(days=30)) # dans 30 jours print(fete - timedelta(weeks=2)) # il y a 2 semaines ecart = date(2025, 12, 25) - fete # date - date = timedelta print(ecart) print(ecart.days) print(date(2024, 2, 28) + timedelta(days=1)) # 2024 est bissextile print(date(2025, 2, 28) + timedelta(days=1)) duree = datetime(2026, 1, 1, 10, 0) - datetime(2025, 12, 31, 22, 30) print(duree, "soit", duree.total_seconds(), "secondes") ``` ``` 2025-08-13 2025-06-30 164 days, 0:00:00 164 2024-02-29 2025-03-01 11:30:00 soit 41400.0 secondes ``` #### Formater et lire du texte : `strftime` / `strptime` - **`strftime`** (*string format time*) : date → texte, selon un **format**. - **`strptime`** (*string parse time*) : texte → date, selon un format. | Code | Signification | Exemple | |---|---|---| | `%d` | jour sur 2 chiffres | `03` | | `%m` | mois sur 2 chiffres | `02` | | `%Y` | année sur 4 chiffres | `2026` | | `%H` | heure (00-23) | `14` | | `%M` | minutes | `30` | | `%S` | secondes | `05` | | `%A` / `%B` | nom du jour / du mois (en anglais par défaut) | `Monday` / `July` | ```python from datetime import date, datetime fete = date(2025, 7, 14) print(fete.strftime("%d/%m/%Y")) rdv = datetime.strptime("03/02/2026 14:30", "%d/%m/%Y %H:%M") print(rdv) print(rdv.strftime("Rendez-vous le %d/%m à %Hh%M")) print(date.fromisoformat("2026-02-03")) # format international AAAA-MM-JJ ``` ``` 14/07/2025 2026-02-03 14:30:00 Rendez-vous le 03/02 à 14h30 2026-02-03 ``` Les noms de jours et de mois (`%A`, `%B`) sortent en anglais par défaut. La façon la plus simple et la plus fiable d'avoir le français est d'utiliser ta propre liste : ```python from datetime import date JOURS = ["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"] MOIS = ["janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août", "septembre", "octobre", "novembre", "décembre"] fete = date(2025, 7, 14) print(f"{JOURS[fete.weekday()]} {fete.day} {MOIS[fete.month - 1]} {fete.year}") ``` ``` lundi 14 juillet 2025 ``` > 💡 Pour stocker une date dans un fichier JSON ou CSV, utilise le format ISO (`date.isoformat()` → `"2025-07-14"`) et relis-le avec `date.fromisoformat()`. Ce format se trie correctement par ordre alphabétique ! ### 10. `os` et `sys` : parler au système #### `os` : le système d'exploitation ```python import os print(os.getcwd()) # dossier de travail actuel print(os.listdir(".")) # contenu d'un dossier print(os.environ.get("HOME")) # variable d'environnement (None si absente) print(os.sep) # séparateur de chemin : / ou \ ``` La sortie dépend de ton ordinateur. Par exemple sous Linux : ``` /home/toi/cours ['mon_script.py', 'notes.txt'] /home/toi / ``` Le module `os` contient aussi des fonctions pour créer ou supprimer des dossiers (`os.makedirs`, `os.remove`, `os.rename`…). Mais pour manipuler des **chemins et fichiers**, préfère **`pathlib`** (vu au [chapitre 07](07-fichiers-csv-json.md)), plus moderne et plus lisible : | Avec `os` | Avec `pathlib` | |---|---| | `os.getcwd()` | `Path.cwd()` | | `os.listdir(d)` | `Path(d).iterdir()` | | `os.path.exists(c)` | `Path(c).exists()` | | `os.path.join(d, "a.txt")` | `Path(d) / "a.txt"` | | `os.makedirs(d, exist_ok=True)` | `Path(d).mkdir(parents=True, exist_ok=True)` | `os` reste utile pour les **variables d'environnement** (`os.environ`), très utilisées pour stocker des réglages ou des mots de passe en dehors du code. #### `sys` : l'interpréteur Python ```python import sys print(sys.version_info >= (3, 12)) # vérifier la version de Python print(sys.argv) # arguments de la ligne de commande ``` **`sys.argv`** est une liste contenant le nom du script suivi des **arguments** tapés dans le terminal. Fichier `bonjour.py` : ```python import sys if len(sys.argv) < 2: print("Usage : python bonjour.py PRENOM") sys.exit(1) # arrête le programme avec un code d'erreur prenom = sys.argv[1] print(f"Bonjour {prenom} !") ``` ``` $ python bonjour.py Usage : python bonjour.py PRENOM $ python bonjour.py Léa Bonjour Léa ! ``` `sys.exit()` arrête immédiatement le programme. Par convention, le code `0` signifie « tout va bien » et tout autre nombre signale une erreur. C'est la base des scripts d'automatisation du [chapitre 14](14-specialisations.md). ### 11. `collections` : des structures de données en plus Le module `collections` offre des versions « améliorées » des listes et dictionnaires du [chapitre 04](04-structures-de-donnees.md). #### `Counter` : compter Compter des éléments est si fréquent qu'il existe un outil dédié : ```python from collections import Counter lettres = Counter("abracadabra") print(lettres) print(lettres["a"], lettres["z"]) # pas de KeyError : 0 si absent print(lettres.most_common(2)) # les 2 plus fréquents mots = "le chat et le chien et le lapin".split() print(Counter(mots).most_common(3)) ``` ``` Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1}) 5 0 [('a', 5), ('b', 2)] [('le', 3), ('et', 2), ('chat', 1)] ``` Compare avec la version « à la main » du chapitre 04 : une boucle, un `if mot in compteur`, une incrémentation… `Counter` fait tout en une ligne. #### `defaultdict` : un dictionnaire avec valeur par défaut Pour regrouper des éléments, on écrit souvent : « si la clé n'existe pas, crée une liste vide, puis ajoute ». `defaultdict` le fait automatiquement : ```python from collections import defaultdict eleves = [("Alice", "A"), ("Bob", "B"), ("Chloé", "A"), ("David", "C")] par_classe = defaultdict(list) # valeur par défaut : une liste vide for nom, classe in eleves: par_classe[classe].append(nom) # pas besoin de vérifier si la clé existe print(dict(par_classe)) ``` ``` {'A': ['Alice', 'Chloé'], 'B': ['Bob'], 'C': ['David']} ``` On passe à `defaultdict` **le type** de la valeur par défaut, sans parenthèses : `list` (liste vide), `int` (0), `set` (ensemble vide), `str` (chaîne vide). #### `namedtuple` : des tuples avec des noms ```python from collections import namedtuple Point = namedtuple("Point", ["x", "y"]) p = Point(3, 4) print(p) print(p.x, p.y) # accès par nom : plus lisible que p[0] print(p[0]) # l'accès par indice marche toujours ``` ``` Point(x=3, y=4) 3 4 3 ``` C'est un premier pas vers les **classes** du [chapitre 10](10-poo-classes-objets.md) et les *dataclasses* du [chapitre 11](11-poo-avancee-heritage-typage.md), qui feront la même chose en plus puissant. #### `deque` : une file à double entrée `deque` (prononcé « dèque ») est une liste optimisée pour ajouter et retirer **aux deux bouts**. Avec `maxlen`, elle ne garde que les N derniers éléments, idéal pour un historique : ```python from collections import deque derniers = deque(maxlen=3) for numero in range(1, 6): derniers.append(numero) print(list(derniers)) ``` ``` [1] [1, 2] [1, 2, 3] [2, 3, 4] [3, 4, 5] ``` ### 12. D'autres modules utiles à connaître Tu n'as pas besoin de tout apprendre par cœur. Retiens surtout qu'**ils existent**, pour penser à les chercher le jour venu : | Module | Pour quoi faire | Exemple | |---|---|---| | `time` | pauses, mesurer une durée | `time.sleep(1)`, `time.perf_counter()` | | `string` | constantes de caractères | `string.ascii_letters`, `string.digits` | | `secrets` | hasard sécurisé | `secrets.choice(...)`, `secrets.token_hex(8)` | | `statistics` | moyenne, médiane… | `statistics.median(...)` | | `json`, `csv` | formats de fichiers | vus au [chapitre 07](07-fichiers-csv-json.md) | | `pathlib`, `shutil` | fichiers, copies de dossiers | `shutil.copy(...)` | | `itertools` | combinaisons, permutations | `itertools.permutations("abc")` | | `textwrap` | mise en forme de texte | `textwrap.wrap(texte, 40)` | | `urllib.request` | télécharger une page web | (on préférera `requests`, chapitre 14) | ```python import string import time print(string.ascii_lowercase) print(string.digits) debut = time.perf_counter() total = sum(range(1_000_000)) duree = time.perf_counter() - debut print(f"Calcul effectué en {duree:.4f} s") ``` Exemple de sortie (la durée varie selon l'ordinateur) : ``` abcdefghijklmnopqrstuvwxyz 0123456789 Calcul effectué en 0.0123 s ``` Et quand la bibliothèque standard ne suffit pas, il existe plus de 500 000 bibliothèques supplémentaires sur PyPI, qu'on installe avec `pip` : ce sera le sujet du [chapitre 12](12-outils-venv-pip-git.md). --- ## Pièges fréquents ### Piège 1 — Nommer son fichier comme un module de la bibliothèque standard C'est **le** piège classique. Tu veux tester le module `random`, alors tu crées un fichier… `random.py` : ```python # fichier : random.py import random print(random.randint(1, 6)) ``` ``` Traceback (most recent call last): File "/home/toi/cours/random.py", line 1, in import random File "/home/toi/cours/random.py", line 3, in print(random.randint(1, 6)) ^^^^^^^^^^^^^^ AttributeError: module 'random' has no attribute 'randint' (consider renaming '/home/toi/cours/random.py' since it has the same name as the standard library module named 'random' and prevents importing that standard library module) ``` Pourquoi ? Python cherche d'abord dans **le dossier du script** (section 4) : `import random` importe donc… ton propre fichier, qui n'a pas de fonction `randint`. Python 3.13 te donne même la solution dans le message (*consider renaming*). En Python 3.12, le message est moins clair : `AttributeError: partially initialized module 'random' has no attribute 'randint' (most likely due to a circular import)`. ✅ **Correction** : renomme ton fichier (`test_hasard.py`, `mon_random.py`…) **et supprime le dossier `__pycache__`** s'il a été créé à côté. Noms à ne jamais donner à tes fichiers : `random.py`, `math.py`, `json.py`, `csv.py`, `string.py`, `time.py`, `datetime.py`, `email.py`, `test.py`, `turtle.py`… ### Piège 2 — L'import circulaire Deux modules qui s'importent **mutuellement** : ```python # fichier : joueurs.py from scores import meilleur_score def creer_joueur(nom): return {"nom": nom, "score": 0} ``` ```python # fichier : scores.py from joueurs import creer_joueur def meilleur_score(joueurs): return max(joueur["score"] for joueur in joueurs) ``` En lançant un `main.py` qui contient `from joueurs import creer_joueur`, en Python 3.12 : ``` Traceback (most recent call last): File "/home/toi/cours/main.py", line 1, in from joueurs import creer_joueur File "/home/toi/cours/joueurs.py", line 1, in from scores import meilleur_score File "/home/toi/cours/scores.py", line 1, in from joueurs import creer_joueur ImportError: cannot import name 'creer_joueur' from partially initialized module 'joueurs' (most likely due to a circular import) (/home/toi/cours/joueurs.py) ``` (En Python 3.13, la dernière ligne est formulée autrement : `ImportError: cannot import name 'creer_joueur' from 'joueurs' (consider renaming ...)`. Ne te laisse pas tromper : il ne s'agit pas d'un conflit de nom ici, mais bien d'un import circulaire. Le signe qui ne trompe pas : dans le traceback, le même fichier `joueurs.py` apparaît au début **et** est réimporté à la fin.) Python commence à exécuter `joueurs.py`, qui demande `scores.py`, qui redemande `joueurs.py`… lequel n'est pas encore fini de charger (*partially initialized*) : `creer_joueur` n'existe pas encore. ✅ **Corrections**, de la meilleure à la moins bonne : 1. **Réorganiser** : souvent, l'un des deux modules n'a pas vraiment besoin de l'autre. Ici `joueurs.py` n'utilise même pas `meilleur_score` : on supprime l'import. 2. Déplacer le code commun dans un **troisième module** que les deux importent. 3. En dernier recours, déplacer l'`import` **à l'intérieur** de la fonction qui en a besoin. ### Piège 3 — Confondre le module `datetime` et le type `datetime` ```python import datetime maintenant = datetime.now() ``` ``` AttributeError: module 'datetime' has no attribute 'now' ``` Ici, `datetime` désigne le **module** ; le type est à l'intérieur : `datetime.datetime.now()`. ✅ Plus lisible : ```python from datetime import datetime maintenant = datetime.now() ``` ### Piège 4 — Oublier le préfixe du module (ou l'import) ```python import math print(sqrt(16)) ``` ``` NameError: name 'sqrt' is not defined ``` Avec `import math`, il faut écrire `math.sqrt(16)`. ✅ Ou bien `from math import sqrt`. ### Piège 5 — Mettre l'extension ou un tiret dans l'import ```python import outils.py ``` ``` ModuleNotFoundError: No module named 'outils.py'; 'outils' is not a package ``` Python croit que `outils` est un paquet contenant un module `py` ! ✅ `import outils`. De même, un fichier `mon-module.py` ne peut pas être importé (le tiret est lu comme une soustraction) : nomme-le `mon_module.py`. ### Piège 6 — `ModuleNotFoundError` parce qu'on n'est pas dans le bon dossier ``` ModuleNotFoundError: No module named 'geometrie' ``` Le fichier existe pourtant bien ! Causes fréquentes : - `geometrie.py` n'est **pas dans le même dossier** que le script lancé ; - faute de frappe dans le nom (`geometrie` / `geometry`) ; - pour une bibliothèque externe : elle n'est pas installée dans l'environnement utilisé (voir [chapitre 12](12-outils-venv-pip-git.md)). ✅ Vérifie avec `print(sys.path)` où Python cherche, et place tes modules à côté de ton script principal. ### Piège 7 — `random.shuffle` renvoie `None` ```python import random cartes = ["As", "Roi", "Dame"] cartes = random.shuffle(cartes) print(cartes) ``` ``` None ``` `shuffle` mélange la liste **sur place** et ne renvoie rien (comme `liste.sort()`). En réaffectant, on a écrasé la liste par `None`. ✅ Écris juste `random.shuffle(cartes)`, ou utilise `random.sample(cartes, len(cartes))` pour obtenir une **nouvelle** liste mélangée. ### Piège 8 — Utiliser `random` pour des mots de passe `random` est conçu pour les simulations et les jeux, pas pour la sécurité : ses tirages sont reproductibles par quelqu'un qui connaît son état interne. ✅ Pour tout ce qui est secret (mots de passe, jetons, codes de validation), utilise `secrets`. --- ## Exercices ### Exercice 1 ⭐ — Lancer de dés Écris un programme qui simule le lancer de **deux dés à six faces**, affiche chaque dé et leur somme, et affiche `Double !` si les deux dés sont identiques. ``` Dé 1 : 4 Dé 2 : 4 Somme : 8 Double ! ``` ### Exercice 2 ⭐ — Calculs géométriques En utilisant le module `math`, écris trois fonctions : - `aire_disque(rayon)` ; - `hypotenuse(a, b)` : la longueur de l'hypoténuse d'un triangle rectangle ; - `nombre_de_dalles(surface_m2, surface_dalle_m2)` : le nombre de dalles à acheter (on ne peut pas acheter une fraction de dalle, donc on arrondit **au-dessus**). ```python print(f"{aire_disque(3):.2f}") # 28.27 print(hypotenuse(6, 8)) # 10.0 print(nombre_de_dalles(10.5, 0.36)) # 30 ``` ### Exercice 3 ⭐ — Combien de jours ? Écris une fonction `jours_avant(jour_cible, aujourd_hui)` qui reçoit deux objets `date` et renvoie le nombre de jours qui les séparent. Puis écris une fonction `prochain_anniversaire(naissance, aujourd_hui)` qui renvoie le nombre de jours avant le prochain anniversaire (0 si c'est aujourd'hui). ```python aujourd_hui = date(2026, 3, 10) print(jours_avant(date(2026, 12, 25), aujourd_hui)) # 290 print(prochain_anniversaire(date(1998, 5, 1), aujourd_hui)) # 52 print(prochain_anniversaire(date(2001, 1, 15), aujourd_hui)) # 311 print(prochain_anniversaire(date(1990, 3, 10), aujourd_hui)) # 0 ``` (On ignore le cas des personnes nées un 29 février.) ### Exercice 4 ⭐⭐ — Analyse d'un texte avec `Counter` Écris une fonction `analyser(texte)` qui affiche : - le nombre total de mots ; - le nombre de mots différents ; - les 3 mots les plus fréquents (en minuscules, ponctuation `.,;:!?` retirée) ; - la lettre la plus fréquente (on ne compte que les lettres). ```python analyser("Le petit chat boit du lait. Le chat dort. Le chien aboie !") ``` ``` Mots : 12 Mots différents : 9 Top 3 : le (3), chat (2), petit (1) Lettre la plus fréquente : t ``` ### Exercice 5 ⭐⭐ — Ton premier module Crée un module `conversions.py` contenant : - `km_vers_miles(km)` (1 mile = 1,609344 km) ; - `celsius_vers_fahrenheit(c)` ; - `secondes_vers_hms(secondes)` qui renvoie une chaîne `"HH:MM:SS"`. Le module doit contenir un bloc `if __name__ == "__main__":` qui vérifie les trois fonctions avec des `assert` et affiche `Tous les tests sont passés.` Crée ensuite un fichier `principal.py` qui importe le module et affiche : ``` 42.195 km = 26.22 miles 37 °C = 98.6 °F 5000 s = 01:23:20 ``` Vérifie que lancer `principal.py` n'affiche **pas** le message des tests. ### Exercice 6 ⭐⭐ — Répertoire par initiale Avec `defaultdict`, écris une fonction `regrouper_par_initiale(noms)` qui renvoie un dictionnaire associant chaque initiale (en majuscule) à la liste **triée** des noms qui commencent par cette lettre. Les clés doivent elles aussi être triées. ```python noms = ["léa", "Hugo", "lucas", "Emma", "hugo", "Inès", "Ethan", "Louise"] print(regrouper_par_initiale(noms)) ``` ``` {'E': ['Emma', 'Ethan'], 'H': ['Hugo', 'Hugo'], 'I': ['Inès'], 'L': ['Louise', 'Lucas', 'Léa']} ``` (Les noms sont remis en forme avec `.capitalize()`.) ### Exercice 7 ⭐⭐⭐ — Simulation statistique Simule **10 000 lancers** de deux dés et compte, avec `Counter`, combien de fois chaque somme (2 à 12) apparaît. Affiche un histogramme horizontal, où chaque `#` représente 50 lancers, avec le pourcentage. Utilise `random.seed(42)` pour obtenir des résultats reproductibles. ``` 2 : 2.7 % ##### 3 : 5.4 % ########## ... 7 : 17.0 % ################################## ... ``` Question bonus : pourquoi le 7 est-il la somme la plus fréquente ? ### Exercice 8 ⭐⭐⭐ — Un outil en ligne de commande Écris un script `compter.py` qui s'utilise ainsi dans le terminal : ``` $ python compter.py Usage : python compter.py FICHIER [NOMBRE] $ python compter.py absent.txt Erreur : le fichier 'absent.txt' n'existe pas. $ python compter.py poeme.txt 3 poeme.txt : 6 lignes, 26 mots Les 3 mots les plus fréquents : la : 4 mer : 3 le : 2 ``` - Le premier argument est le chemin d'un fichier texte, le second (optionnel, 5 par défaut) le nombre de mots à afficher. - Utilise `sys.argv`, `sys.exit`, `pathlib` et `Counter`. - Si le second argument n'est pas un entier, affiche une erreur claire. - Organise ton code avec une fonction `main()` et `if __name__ == "__main__":`. Fichier `poeme.txt` de test : ``` La mer, la mer, toujours recommencée. Le vent se lève, il faut tenter de vivre. La mer brille et le ciel s'endort et la vague chante. ``` --- ## Mini-projet — Générateur de mots de passe et module utilitaire maison ### Cahier des charges Tu vas construire un petit projet sur **deux fichiers** : ``` generateur_mdp/ ├── mdp_outils.py ← ton module utilitaire réutilisable └── generateur.py ← le programme principal (menu) ``` **Le module `mdp_outils.py`** doit fournir : 1. `generer_mot_de_passe(longueur=12, majuscules=True, chiffres=True, symboles=True)` : renvoie un mot de passe aléatoire **sécurisé** (module `secrets`), contenant **au moins un caractère de chaque catégorie demandée** (les minuscules sont toujours incluses). Lève une `ValueError` si la longueur est inférieure à 4 ou au nombre de catégories. 2. `generer_phrase_de_passe(nombre_mots=4, separateur="-")` : assemble des mots tirés d'une liste (au moins 20 mots) et ajoute un nombre à 2 chiffres à la fin. Exemple : `tigre-nuage-velo-orange-42`. 3. `evaluer_force(mot_de_passe)` : renvoie un tuple `(score, verdict)` où le score (0 à 5) compte un point pour : longueur ≥ 8, longueur ≥ 12, présence de majuscules, de chiffres, de symboles. Verdict : `"faible"` (0-2), `"moyen"` (3), `"fort"` (4-5). 4. Un bloc `if __name__ == "__main__":` avec des tests (`assert`). **Le programme `generateur.py`** affiche un menu en boucle : ``` === Générateur de mots de passe === 1. Mot de passe aléatoire 2. Phrase de passe 3. Tester la force d'un mot de passe 4. Voir l'historique de la session 5. Quitter Ton choix : ``` - Le choix 1 demande la longueur et les options (o/n) ; les saisies invalides sont gérées (chapitre 08 !). - Les choix 1 et 2 affichent le résultat avec sa force, et l'ajoutent à l'historique avec l'heure (`datetime`). - L'historique ne garde que les **5 dernières** générations (`deque`). ### Étapes suggérées 1. Commence par `mdp_outils.py` et sa fonction `evaluer_force` (la plus simple), avec des `assert` dans le bloc `__main__`. Lance le module seul pour tester. 2. Écris `generer_mot_de_passe` : construis l'alphabet autorisé avec `string.ascii_lowercase`, `string.ascii_uppercase`, `string.digits`, `string.punctuation`. Tire d'abord **un caractère obligatoire** par catégorie, complète avec des caractères tirés dans l'alphabet complet, puis **mélange** le tout (`secrets.SystemRandom().shuffle`). 3. Écris `generer_phrase_de_passe`. 4. Écris `generateur.py` : une fonction par choix du menu, une fonction `demander_entier(message, minimum, maximum)` et une fonction `demander_oui_non(message)`. 5. Ajoute l'historique avec `deque(maxlen=5)` et `datetime.now().strftime("%H:%M:%S")`. ### Pistes d'amélioration - Option « éviter les caractères ambigus » (`l`, `1`, `I`, `O`, `0`). - Copier automatiquement le mot de passe dans le presse-papiers (bibliothèque externe `pyperclip`, voir [chapitre 12](12-outils-venv-pip-git.md)). - Lire la liste de mots de la phrase de passe dans un fichier texte. - Accepter des arguments en ligne de commande (`python generateur.py --longueur 20`) avec le module `argparse`. --- ## Corrigés
Corrigé de l'exercice 1 ```python import random de1 = random.randint(1, 6) de2 = random.randint(1, 6) print(f"Dé 1 : {de1}") print(f"Dé 2 : {de2}") print(f"Somme : {de1 + de2}") if de1 == de2: print("Double !") ``` Exemple de sortie (différente à chaque exécution) : ``` Dé 1 : 3 Dé 2 : 5 Somme : 8 ``` `randint(1, 6)` inclut bien le 1 **et** le 6. Pour tester le cas « Double ! », tu peux temporairement ajouter `random.seed(...)` en essayant plusieurs graines, ou forcer `de2 = de1`.
Corrigé de l'exercice 2 ```python import math def aire_disque(rayon): return math.pi * rayon**2 def hypotenuse(a, b): return math.hypot(a, b) # ou math.sqrt(a**2 + b**2) def nombre_de_dalles(surface_m2, surface_dalle_m2): return math.ceil(surface_m2 / surface_dalle_m2) print(f"{aire_disque(3):.2f}") print(hypotenuse(6, 8)) print(nombre_de_dalles(10.5, 0.36)) ``` ``` 28.27 10.0 30 ``` 10,5 / 0,36 = 29,17 : avec 29 dalles, il manquerait un morceau, d'où `math.ceil` (arrondi supérieur). `round` aurait donné 29, et `int` aussi.
Corrigé de l'exercice 3 ```python from datetime import date def jours_avant(jour_cible, aujourd_hui): return (jour_cible - aujourd_hui).days def prochain_anniversaire(naissance, aujourd_hui): anniversaire = date(aujourd_hui.year, naissance.month, naissance.day) if anniversaire < aujourd_hui: # déjà passé cette année anniversaire = date(aujourd_hui.year + 1, naissance.month, naissance.day) return jours_avant(anniversaire, aujourd_hui) aujourd_hui = date(2026, 3, 10) print(jours_avant(date(2026, 12, 25), aujourd_hui)) print(prochain_anniversaire(date(1998, 5, 1), aujourd_hui)) print(prochain_anniversaire(date(2001, 1, 15), aujourd_hui)) print(prochain_anniversaire(date(1990, 3, 10), aujourd_hui)) ``` ``` 290 52 311 0 ``` Pourquoi passer `aujourd_hui` en paramètre plutôt que d'appeler `date.today()` dans la fonction ? Pour pouvoir **tester** la fonction avec une date fixe : le résultat est alors toujours le même. Dans le programme réel, on appellera `prochain_anniversaire(naissance, date.today())`. Les dates se comparent directement avec `<`.
Corrigé de l'exercice 4 ```python from collections import Counter def analyser(texte): mots = [mot.strip(".,;:!?").lower() for mot in texte.split()] mots = [mot for mot in mots if mot] # retire les « mots » vides (ex : « ! » seul) compteur_mots = Counter(mots) lettres = Counter(c for c in texte.lower() if c.isalpha()) lettre, _ = lettres.most_common(1)[0] top = ", ".join(f"{mot} ({n})" for mot, n in compteur_mots.most_common(3)) print(f"Mots : {len(mots)}") print(f"Mots différents : {len(compteur_mots)}") print(f"Top 3 : {top}") print(f"Lettre la plus fréquente : {lettre}") analyser("Le petit chat boit du lait. Le chat dort. Le chien aboie !") ``` ``` Mots : 12 Mots différents : 9 Top 3 : le (3), chat (2), petit (1) Lettre la plus fréquente : t ``` Le `!` isolé devient une chaîne vide après `strip` : sans le filtre, on compterait 13 « mots ». `len(compteur_mots)` donne le nombre de clés, donc de mots **différents**. `most_common(1)` renvoie une liste contenant un tuple : `[('t', 6)]`, d'où le `[0]` puis le dépaquetage. À égalité, `most_common` garde l'ordre de première apparition (c'est pourquoi `petit` sort avant les autres mots vus une seule fois).
Corrigé de l'exercice 5 Fichier `conversions.py` : ```python """Fonctions de conversion d'unités.""" KM_PAR_MILE = 1.609344 def km_vers_miles(km): return km / KM_PAR_MILE def celsius_vers_fahrenheit(c): return c * 9 / 5 + 32 def secondes_vers_hms(secondes): heures, reste = divmod(secondes, 3600) minutes, secondes = divmod(reste, 60) return f"{heures:02d}:{minutes:02d}:{secondes:02d}" if __name__ == "__main__": assert round(km_vers_miles(1.609344), 6) == 1 assert celsius_vers_fahrenheit(100) == 212 assert celsius_vers_fahrenheit(-40) == -40 assert secondes_vers_hms(3661) == "01:01:01" assert secondes_vers_hms(0) == "00:00:00" print("Tous les tests sont passés.") ``` Fichier `principal.py`, dans le **même dossier** : ```python import conversions print(f"42.195 km = {conversions.km_vers_miles(42.195):.2f} miles") print(f"37 °C = {conversions.celsius_vers_fahrenheit(37):.1f} °F") print(f"5000 s = {conversions.secondes_vers_hms(5000)}") ``` `python principal.py` : ``` 42.195 km = 26.22 miles 37 °C = 98.6 °F 5000 s = 01:23:20 ``` `python conversions.py` : ``` Tous les tests sont passés. ``` `divmod(a, b)` renvoie à la fois le quotient et le reste (`a // b`, `a % b`). Le format `:02d` affiche un entier sur 2 chiffres en complétant par des zéros. La constante `KM_PAR_MILE` est en majuscules : c'est la convention pour une valeur qui ne change jamais.
Corrigé de l'exercice 6 ```python from collections import defaultdict def regrouper_par_initiale(noms): groupes = defaultdict(list) for nom in noms: nom = nom.capitalize() groupes[nom[0]].append(nom) return {initiale: sorted(groupes[initiale]) for initiale in sorted(groupes)} noms = ["léa", "Hugo", "lucas", "Emma", "hugo", "Inès", "Ethan", "Louise"] print(regrouper_par_initiale(noms)) ``` ``` {'E': ['Emma', 'Ethan'], 'H': ['Hugo', 'Hugo'], 'I': ['Inès'], 'L': ['Louise', 'Lucas', 'Léa']} ``` La compréhension de dictionnaire finale ([chapitre 06](06-chaines-et-comprehensions.md)) trie à la fois les clés et chaque liste, et transforme le `defaultdict` en `dict` ordinaire. Remarque que `Léa` arrive **après** `Lucas` : le tri compare les codes des caractères, et `é` a un code plus grand que toutes les lettres non accentuées. Trier « à la française » demanderait le module `locale` ou une clé de tri qui retire les accents.
Corrigé de l'exercice 7 ```python import random from collections import Counter NOMBRE_LANCERS = 10_000 LANCERS_PAR_DIESE = 50 random.seed(42) sommes = Counter( random.randint(1, 6) + random.randint(1, 6) for _ in range(NOMBRE_LANCERS) ) for somme in range(2, 13): nombre = sommes[somme] pourcentage = nombre / NOMBRE_LANCERS * 100 barre = "#" * (nombre // LANCERS_PAR_DIESE) print(f"{somme:2} : {pourcentage:5.1f} % {barre}") ``` ``` 2 : 2.7 % ##### 3 : 5.4 % ########## 4 : 8.2 % ################ 5 : 10.9 % ##################### 6 : 13.6 % ########################### 7 : 17.0 % ################################## 8 : 13.9 % ########################### 9 : 11.2 % ###################### 10 : 8.4 % ################ 11 : 5.6 % ########### 12 : 3.1 % ###### ``` On parcourt `range(2, 13)` plutôt que le `Counter` directement, pour avoir les sommes **dans l'ordre**. `{somme:2}` aligne à droite sur 2 caractères, `{pourcentage:5.1f}` sur 5 caractères avec 1 décimale. **Bonus** : il y a 36 combinaisons possibles de deux dés. Le 7 s'obtient de 6 façons (1+6, 2+5, 3+4, 4+3, 5+2, 6+1), soit 6/36 ≈ 16,7 %, alors que le 2 ne s'obtient que d'une façon (1+1), soit 1/36 ≈ 2,8 %. La simulation retrouve bien ces valeurs théoriques.
Corrigé de l'exercice 8 ```python """Compte les lignes et les mots d'un fichier texte.""" import sys from collections import Counter from pathlib import Path PONCTUATION = ".,;:!?\"'()" def compter_mots(texte): mots = (mot.strip(PONCTUATION).lower() for mot in texte.split()) return Counter(mot for mot in mots if mot) def main(): if len(sys.argv) < 2: print("Usage : python compter.py FICHIER [NOMBRE]") sys.exit(1) chemin = Path(sys.argv[1]) nombre = 5 if len(sys.argv) >= 3: try: nombre = int(sys.argv[2]) except ValueError: print(f"Erreur : {sys.argv[2]!r} n'est pas un nombre entier.") sys.exit(1) try: texte = chemin.read_text(encoding="utf-8") except FileNotFoundError: print(f"Erreur : le fichier '{chemin}' n'existe pas.") sys.exit(1) compteur = compter_mots(texte) nb_lignes = len(texte.splitlines()) nb_mots = compteur.total() print(f"{chemin.name} : {nb_lignes} lignes, {nb_mots} mots") print(f"Les {nombre} mots les plus fréquents :") for mot, occurrences in compteur.most_common(nombre): print(f" {mot} : {occurrences}") if __name__ == "__main__": main() ``` ``` $ python compter.py Usage : python compter.py FICHIER [NOMBRE] $ python compter.py absent.txt Erreur : le fichier 'absent.txt' n'existe pas. $ python compter.py poeme.txt trois Erreur : 'trois' n'est pas un nombre entier. $ python compter.py poeme.txt 3 poeme.txt : 6 lignes, 26 mots Les 3 mots les plus fréquents : la : 4 mer : 3 le : 2 ``` `Counter.total()` (Python 3.10+) renvoie la somme de tous les compteurs, c'est-à-dire le nombre total de mots. `le` et `et` apparaissent tous deux 2 fois : à égalité, `most_common` garde l'ordre de première apparition, donc `le` (ligne 2) passe avant `et` (ligne 3). Attention : `s'endort` reste un seul mot, car l'apostrophe n'est retirée qu'aux extrémités. Les messages d'erreur et `sys.exit(1)` permettent à d'autres scripts de savoir que quelque chose s'est mal passé.
Corrigé du mini-projet Fichier `mdp_outils.py` : ```python """Outils de génération et d'évaluation de mots de passe.""" import secrets import string MOTS = [ "tigre", "nuage", "velo", "orange", "piano", "cactus", "fusee", "galaxie", "pirate", "volcan", "biscuit", "dragon", "lune", "marmotte", "tornade", "citron", "baleine", "chateau", "domino", "glacier", "hibou", "jungle", "kayak", "licorne", "mangue", "neige", "olive", "pingouin", "requin", "tulipe", ] _aleatoire = secrets.SystemRandom() # générateur sécurisé, avec les méthodes de random def generer_mot_de_passe(longueur=12, majuscules=True, chiffres=True, symboles=True): """Renvoie un mot de passe aléatoire sécurisé. Il contient toujours des minuscules, et au moins un caractère de chaque catégorie demandée. """ categories = [string.ascii_lowercase] if majuscules: categories.append(string.ascii_uppercase) if chiffres: categories.append(string.digits) if symboles: categories.append(string.punctuation) if longueur < max(4, len(categories)): raise ValueError(f"longueur trop courte : {longueur} (minimum 4)") alphabet = "".join(categories) caracteres = [secrets.choice(categorie) for categorie in categories] caracteres += [secrets.choice(alphabet) for _ in range(longueur - len(caracteres))] _aleatoire.shuffle(caracteres) # sinon les caractères obligatoires seraient au début return "".join(caracteres) def generer_phrase_de_passe(nombre_mots=4, separateur="-"): """Renvoie une phrase de passe du type tigre-nuage-velo-orange-42.""" mots = [secrets.choice(MOTS) for _ in range(nombre_mots)] nombre = secrets.randbelow(90) + 10 # entre 10 et 99 return separateur.join(mots) + separateur + str(nombre) def evaluer_force(mot_de_passe): """Renvoie (score sur 5, verdict).""" criteres = [ len(mot_de_passe) >= 8, len(mot_de_passe) >= 12, any(c.isupper() for c in mot_de_passe), any(c.isdigit() for c in mot_de_passe), any(c in string.punctuation for c in mot_de_passe), ] score = sum(criteres) # True compte pour 1, False pour 0 if score <= 2: verdict = "faible" elif score == 3: verdict = "moyen" else: verdict = "fort" return score, verdict if __name__ == "__main__": assert evaluer_force("abc") == (0, "faible") assert evaluer_force("Abcdefgh1") == (3, "moyen") assert evaluer_force("Abcdefgh1!xyz") == (5, "fort") for _ in range(100): mdp = generer_mot_de_passe(10) assert len(mdp) == 10 assert any(c.isupper() for c in mdp) assert any(c.isdigit() for c in mdp) assert any(c in string.punctuation for c in mdp) mdp = generer_mot_de_passe(6, majuscules=False, chiffres=False, symboles=False) assert mdp.islower() and len(mdp) == 6 try: generer_mot_de_passe(3) except ValueError: pass else: raise AssertionError("une ValueError était attendue") phrase = generer_phrase_de_passe(3, "_") assert phrase.count("_") == 3 print("mdp_outils : tous les tests sont passés.") print("Exemple :", generer_mot_de_passe(), "/", generer_phrase_de_passe()) ``` Fichier `generateur.py` : ```python """Menu interactif de génération de mots de passe.""" from collections import deque from datetime import datetime import mdp_outils historique = deque(maxlen=5) def demander_entier(message, minimum, maximum): while True: try: valeur = int(input(message)) except ValueError: print("Merci d'entrer un nombre entier.") continue if minimum <= valeur <= maximum: return valeur print(f"Le nombre doit être entre {minimum} et {maximum}.") def demander_oui_non(message): while True: reponse = input(f"{message} (o/n) : ").strip().lower() if reponse in ("o", "oui"): return True if reponse in ("n", "non"): return False print("Réponds par o ou n.") def afficher_et_memoriser(secret): score, verdict = mdp_outils.evaluer_force(secret) print(f"\n → {secret} (force : {verdict}, {score}/5)\n") heure = datetime.now().strftime("%H:%M:%S") historique.append(f"[{heure}] {secret}") def choix_mot_de_passe(): longueur = demander_entier("Longueur (4 à 64) : ", 4, 64) majuscules = demander_oui_non("Majuscules ?") chiffres = demander_oui_non("Chiffres ?") symboles = demander_oui_non("Symboles ?") secret = mdp_outils.generer_mot_de_passe(longueur, majuscules, chiffres, symboles) afficher_et_memoriser(secret) def choix_phrase(): nombre_mots = demander_entier("Nombre de mots (3 à 8) : ", 3, 8) afficher_et_memoriser(mdp_outils.generer_phrase_de_passe(nombre_mots)) def choix_tester(): mot_de_passe = input("Mot de passe à tester : ") score, verdict = mdp_outils.evaluer_force(mot_de_passe) print(f"Force : {verdict} ({score}/5)") def choix_historique(): if not historique: print("Aucune génération pour l'instant.") for ligne in historique: print(ligne) def main(): actions = { "1": choix_mot_de_passe, "2": choix_phrase, "3": choix_tester, "4": choix_historique, } while True: print("=== Générateur de mots de passe ===") print("1. Mot de passe aléatoire") print("2. Phrase de passe") print("3. Tester la force d'un mot de passe") print("4. Voir l'historique de la session") print("5. Quitter") choix = input("Ton choix : ").strip() if choix == "5": print("À bientôt !") break action = actions.get(choix) if action is None: print("Choix invalide.") else: action() if __name__ == "__main__": main() ``` Exemple de session (les mots de passe changent à chaque exécution) : ``` === Générateur de mots de passe === 1. Mot de passe aléatoire 2. Phrase de passe 3. Tester la force d'un mot de passe 4. Voir l'historique de la session 5. Quitter Ton choix : 1 Longueur (4 à 64) : 2 Le nombre doit être entre 4 et 64. Longueur (4 à 64) : 16 Majuscules ? (o/n) : o Chiffres ? (o/n) : peut-être Réponds par o ou n. Chiffres ? (o/n) : o Symboles ? (o/n) : n → uJbMEcdEOm7yrS9X (force : fort, 4/5) === Générateur de mots de passe === ... Ton choix : 3 Mot de passe à tester : soleil Force : faible (0/5) ... Ton choix : 4 [14:02:31] uJbMEcdEOm7yrS9X ... Ton choix : 5 À bientôt ! ``` Points importants : - **Séparation des rôles** : `mdp_outils.py` ne fait **aucun** `print` ni `input` (hors tests). Il est donc réutilisable dans un autre programme, une application web, des tests… `generateur.py` ne s'occupe que du dialogue avec l'utilisateur. - Le **dictionnaire de fonctions** `actions` évite une longue chaîne de `if/elif` : les fonctions sont des valeurs comme les autres, qu'on peut ranger dans un dictionnaire puis appeler avec `action()`. - `secrets.SystemRandom()` donne accès aux méthodes habituelles de `random` (comme `shuffle`), mais avec la source de hasard sécurisée du système. - `sum(criteres)` fonctionne car `True` vaut 1 et `False` vaut 0. - `deque(maxlen=5)` oublie automatiquement les générations les plus anciennes. - La variable `_aleatoire` commence par un tiret bas : c'est la convention pour dire « usage interne au module, ne pas l'utiliser de l'extérieur ».
--- ## Récapitulatif - Un **module** est un fichier `.py` ; un **paquet** est un dossier de modules (avec un `__init__.py`). La **bibliothèque standard** est l'ensemble des modules livrés avec Python. - `import module` puis `module.nom` (forme par défaut) ; `from module import nom` pour quelques noms fréquents ; `as` pour un alias ; jamais `import *`. - Python cherche les modules **d'abord dans le dossier du script** : ne nomme jamais un fichier comme un module existant (`random.py`, `math.py`…). - Un module est exécuté **une fois**, au premier import. Le code de test se place sous `if __name__ == "__main__":`, souvent dans une fonction `main()`. - `random` : `randint` (bornes incluses), `choice`, `choices`, `sample`, `shuffle` (sur place), `seed`. Pour la sécurité : `secrets`. - `math` : `sqrt`, `pi`, `floor`, `ceil`, `hypot`, `isclose`… et `statistics` pour moyenne, médiane, écart-type. - `datetime` : `date`, `datetime`, `timedelta` ; `strftime` (date → texte), `strptime` (texte → date), `isoformat` / `fromisoformat`. - `os` (variables d'environnement, système), `sys` (`argv`, `exit`, `path`) ; préfère `pathlib` pour les fichiers. - `collections` : `Counter` (compter), `defaultdict` (regrouper), `namedtuple` (tuples nommés), `deque` (file, historique limité). - Pour explorer : `dir()`, `help()`, et la documentation officielle . ### Checklist de validation - [ ] Je connais les différentes façons d'importer et je sais laquelle utiliser par défaut. - [ ] J'ai créé un module à moi et je l'ai importé depuis un autre fichier. - [ ] Je sais expliquer à quoi sert `if __name__ == "__main__":`. - [ ] Je sais tirer un nombre au hasard, piocher dans une liste et rendre le hasard reproductible. - [ ] Je sais calculer le nombre de jours entre deux dates et formater une date en `JJ/MM/AAAA`. - [ ] Je sais compter des éléments avec `Counter` et regrouper avec `defaultdict`. - [ ] Je sais lire les arguments de la ligne de commande avec `sys.argv`. - [ ] Je sais pourquoi mon fichier ne doit jamais s'appeler `random.py`. --- [← Chapitre précédent](08-erreurs-et-debogage.md) | [Sommaire](README.md) | [Chapitre suivant →](10-poo-classes-objets.md)