alpinux-wiki/docs/presentations/Python/09-modules-bibliotheque-standard.md
2026-10-15 00:28:56 +02:00

1777 lines
57 KiB
Markdown

# 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 : <https://docs.python.org/fr/3/library/index.html>. 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 <module>
import random
File "/home/toi/cours/random.py", line 3, in <module>
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 <module>
from joueurs import creer_joueur
File "/home/toi/cours/joueurs.py", line 1, in <module>
from scores import meilleur_score
File "/home/toi/cours/scores.py", line 1, in <module>
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
<details><summary>Corrigé de l'exercice 1</summary>
```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`.
</details>
<details><summary>Corrigé de l'exercice 2</summary>
```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.
</details>
<details><summary>Corrigé de l'exercice 3</summary>
```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 `<`.
</details>
<details><summary>Corrigé de l'exercice 4</summary>
```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).
</details>
<details><summary>Corrigé de l'exercice 5</summary>
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.
</details>
<details><summary>Corrigé de l'exercice 6</summary>
```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.
</details>
<details><summary>Corrigé de l'exercice 7</summary>
```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.
</details>
<details><summary>Corrigé de l'exercice 8</summary>
```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é.
</details>
<details><summary>Corrigé du mini-projet</summary>
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 ».
</details>
---
## 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 <https://docs.python.org/fr/3/library/>.
### 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)