diff --git a/.gitignore b/.gitignore index 4c70c57..aa08389 100644 --- a/.gitignore +++ b/.gitignore @@ -119,6 +119,9 @@ venv.bak/ .dmypy.json dmypy.json +# Ruff +.ruff_cache/ + # Pyre type checker .pyre/ diff --git a/01-fondamentaux-et-syntaxe/01-installation-et-configuration.md b/01-fondamentaux-et-syntaxe/01-installation-et-configuration.md index 9b94ad5..ff82d6a 100644 --- a/01-fondamentaux-et-syntaxe/01-installation-et-configuration.md +++ b/01-fondamentaux-et-syntaxe/01-installation-et-configuration.md @@ -16,7 +16,7 @@ Actuellement, il existe deux grandes versions de Python : - **Python 2.x** : ancienne version, qui n'est plus maintenue depuis 2020 - **Python 3.x** : version moderne et activement maintenue -**Recommandation** : Installez toujours Python 3.x (la version la plus récente, Python 3.13 au moment de la rédaction). Python 2 est obsolète et ne devrait plus être utilisé pour de nouveaux projets. +**Recommandation** : Installez toujours Python 3.x (la version la plus récente, Python 3.14 au moment de la rédaction). Python 2 est obsolète et ne devrait plus être utilisé pour de nouveaux projets. --- @@ -47,7 +47,7 @@ Actuellement, il existe deux grandes versions de Python : #### Méthode 2 : Installation via le Microsoft Store 1. Ouvrez le Microsoft Store -2. Recherchez "Python 3.13" (ou la version la plus récente disponible) +2. Recherchez "Python 3.14" (ou la version la plus récente disponible) 3. Cliquez sur "Obtenir" ou "Installer" 4. Cette méthode configure automatiquement le PATH @@ -141,14 +141,14 @@ Une fois l'installation terminée, il est important de vérifier que Python fonc ```bash python --version ``` - ou + ou, avec le lanceur Python (Windows) : ```bash - python3 --version + py --version ``` 3. Vous devriez voir s'afficher quelque chose comme : ``` - Python 3.12.1 + Python 3.14.0 ``` ### Sur macOS et Linux @@ -197,8 +197,7 @@ python3 Vous verrez apparaître quelque chose comme : ``` -Python 3.12.1 (main, Dec 18 2024, 12:00:00) -[GCC 11.3.0] on linux +Python 3.14.0 (main, Oct 7 2025, 10:30:00) [GCC 13.3.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> ``` @@ -290,8 +289,8 @@ Cette extension vous fournira : 5. **Exécuter le programme** - Sauvegardez le fichier (`Ctrl+S` ou `Cmd+S`) - - Cliquez avec le bouton droit dans l'éditeur et sélectionnez "Run Python File in Terminal" - - Ou ouvrez un terminal dans VS Code (`Ctrl+ù` ou `Cmd+ù`) et tapez : + - Cliquez sur le bouton ▶ "Run Python File" en haut à droite de l'éditeur, ou faites un clic droit dans l'éditeur et sélectionnez "Run Python File in Terminal" + - Ou ouvrez un terminal intégré (menu "Terminal > Nouveau terminal", ou le raccourci `` Ctrl+` `` — la touche backtick, à gauche de la touche `1`) et tapez : ```bash python hello.py ``` @@ -362,7 +361,7 @@ pip uninstall nom_du_paquet **Note** : Sur certains systèmes (surtout Linux et macOS), vous devrez peut-être utiliser `pip3` au lieu de `pip`. -> ⚠️ **Important (Python 3.12+)** : Sur les distributions Linux récentes, pip refuse d'installer des paquets en dehors d'un environnement virtuel pour protéger le système (PEP 668). Si vous voyez l'erreur `externally-managed-environment`, créez d'abord un environnement virtuel (voir section 6.4) : +> ⚠️ **Important — erreur `externally-managed-environment` (PEP 668)** : Sur les distributions Linux récentes (Ubuntu 23.04+, Debian 12+...), pip refuse d'installer des paquets en dehors d'un environnement virtuel pour protéger le système. Si vous voyez cette erreur, créez d'abord un environnement virtuel (voir section 6.4) : > ```bash > python3 -m venv mon_env > source mon_env/bin/activate @@ -396,10 +395,25 @@ Nous reviendrons sur ce concept important plus tard dans la formation. ### Plusieurs versions de Python installées -Si vous avez plusieurs versions de Python, utilisez des commandes plus spécifiques : -- `python3.12` pour Python 3.12 -- `python3.11` pour Python 3.11 -- etc. +Si vous avez plusieurs versions de Python installées, la commande pour en cibler une précise dépend de votre système. + +**Sur macOS / Linux** — ajoutez le numéro de version à la commande : + +```bash +python3.14 --version # cible Python 3.14 +python3.13 --version # cible Python 3.13 +``` + +**Sur Windows** — utilisez le *lanceur Python* `py`, installé automatiquement avec Python : + +```bash +py --version # la version par défaut (la plus récente installée) +py -3.14 # cible précisément Python 3.14 +py -3.13 # cible précisément Python 3.13 +py -0p # liste toutes les versions installées, avec leur chemin +``` + +> 🔍 **Approfondissement — gérer Python avec `uv`** : des outils modernes comme [`uv`](https://docs.astral.sh/uv/) (signé par l'éditeur de Ruff) savent installer et faire cohabiter plusieurs versions de Python automatiquement, sur tous les systèmes (`uv python install 3.14`). Nous y reviendrons au module 6 ; pour débuter, l'installateur officiel et le lanceur `py` suffisent amplement. ### Problèmes de permissions sur Linux/macOS diff --git a/01-fondamentaux-et-syntaxe/02-variables-types-et-operateurs.md b/01-fondamentaux-et-syntaxe/02-variables-types-et-operateurs.md index f714429..dd4495d 100644 --- a/01-fondamentaux-et-syntaxe/02-variables-types-et-operateurs.md +++ b/01-fondamentaux-et-syntaxe/02-variables-types-et-operateurs.md @@ -61,6 +61,37 @@ compteur = compteur + 1 print(compteur) # Affiche : 6 ``` +### Affectation multiple + +Python permet d'affecter des valeurs à plusieurs variables en une seule instruction, ce qui rend le code plus concis. + +**Affecter plusieurs variables à la fois** : + +```python +x, y, z = 1, 2, 3 +print(x) # Affiche : 1 +print(y) # Affiche : 2 +print(z) # Affiche : 3 +``` + +**Donner la même valeur à plusieurs variables** : + +```python +a = b = c = 0 +print(a, b, c) # Affiche : 0 0 0 +``` + +**Échanger le contenu de deux variables** (sans variable temporaire) : + +```python +x = 10 +y = 20 +x, y = y, x +print(x, y) # Affiche : 20 10 +``` + +> 💡 L'échange `x, y = y, x` fonctionne parce que Python évalue **d'abord** tout le membre de droite (`y, x`), puis affecte le résultat au membre de gauche. Inutile de passer par une variable temporaire comme dans beaucoup d'autres langages. + ### Règles de nommage des variables Python a quelques règles strictes et des conventions pour nommer les variables : @@ -84,9 +115,11 @@ _variable_privee = 42 2age = 30 # Commence par un chiffre ❌ mon-age = 25 # Contient un tiret ❌ for = 10 # Mot réservé Python ❌ -mon âge = 25 # Contient un espace et un accent ❌ +mon âge = 25 # Contient un espace ❌ ``` +> 💡 **À noter** : depuis Python 3, les caractères accentués sont en réalité **autorisés** dans les noms de variables — `âge = 25` ou `prénom = "Bob"` fonctionnent parfaitement. C'est donc l'**espace** qui rend `mon âge` invalide, pas l'accent. Cela dit, la convention (PEP 8) recommande de se limiter à l'alphabet anglais (ASCII) pour les noms : préférez `age` à `âge`. + **Conventions (bonnes pratiques)** : - Utilisez des noms descriptifs : `age_utilisateur` plutôt que `a` - Utilisez le snake_case (mots séparés par des underscores) : `prenom_utilisateur` @@ -154,15 +187,29 @@ resultat = 0.1 + 0.2 print(resultat) # Affiche : 0.30000000000000004 (!) ``` -Cette particularité est commune à tous les langages de programmation et est liée à la façon dont les ordinateurs représentent les nombres décimaux. +Cette particularité est commune à la plupart des langages de programmation et est liée à la façon dont les ordinateurs représentent les nombres décimaux (la norme IEEE 754 du calcul en virgule flottante). + +**Comparer des nombres flottants** : + +À cause de ces imprécisions, **ne comparez pas deux flottants avec `==`**. Utilisez `math.isclose()` : + +```python +import math +print(0.1 + 0.2 == 0.3) # Affiche : False (le piège !) +print(math.isclose(0.1 + 0.2, 0.3)) # Affiche : True (la bonne façon de comparer) +``` + +> 💡 Pour des calculs décimaux **exacts** (sommes d'argent, par exemple), utilisez le module `decimal` : `Decimal("0.1") + Decimal("0.2")` vaut exactement `Decimal("0.3")`. **Notation scientifique** : ```python -grand_nombre = 3e8 # 3 × 10^8 = 300000000 +grand_nombre = 3e8 # 3 × 10^8 = 300000000.0 petit_nombre = 1.5e-4 # 1.5 × 10^-4 = 0.00015 ``` +> 💡 La notation scientifique produit **toujours un `float`**, même lorsque le résultat est rond : `3e8` vaut `300000000.0` (un float), et non `300000000` (un int). + ### 3. Les Chaînes de Caractères (str) Les **strings** (chaînes de caractères) représentent du texte. On les écrit entre guillemets simples `'` ou doubles `"`. @@ -182,6 +229,33 @@ message1 = "J'aime Python" # Plus lisible message2 = 'J\'aime Python' # Nécessite d'échapper l'apostrophe avec \ ``` +**Séquences d'échappement (caractères spéciaux)** : + +Le caractère `\` (antislash) introduit des **séquences d'échappement**, qui représentent des caractères spéciaux : + +| Séquence | Signification | +|----------|---------------| +| `\n` | Saut de ligne (nouvelle ligne) | +| `\t` | Tabulation | +| `\\` | Un antislash littéral `\` | +| `\"` | Un guillemet double dans une chaîne entre `"` | +| `\'` | Une apostrophe dans une chaîne entre `'` | + +```python +print("Ligne 1\nLigne 2") # \n = saut de ligne (affiche sur deux lignes) +print("Nom :\tAlice") # \t = tabulation +print("Un antislash : \\") # \\ = un seul antislash +print("Il a dit \"Bonjour\"") # \" = guillemet double dans la chaîne +``` + +> 💡 **Chaînes brutes (`r"..."`)** : préfixez une chaîne par `r` pour que les `\` ne soient **pas interprétés**. Pratique pour les chemins Windows et les expressions régulières (chapitre 2.4). + +```python +chemin = "dossier\nouveau" # \n est interprété comme un saut de ligne ! +chemin_brut = r"dossier\nouveau" # chaîne brute : le \ reste littéral +print(chemin_brut) # Affiche : dossier\nouveau +``` + **Chaînes multi-lignes** : Pour écrire du texte sur plusieurs lignes, utilisez trois guillemets : @@ -260,6 +334,27 @@ print(texte.endswith("thon ")) # Affiche : True print("Python" in texte) # Affiche : True ``` +**Immuabilité des chaînes** : + +Les chaînes sont **immuables** : une fois créée, une chaîne ne peut pas être modifiée. Les méthodes comme `.upper()` ou `.replace()` ne changent pas la chaîne d'origine — elles en renvoient une **nouvelle** : + +```python +texte = "bonjour" +texte.upper() # renvoie "BONJOUR"... mais ne modifie PAS texte +print(texte) # Affiche : bonjour (inchangé !) + +texte = texte.upper() # pour « changer » la chaîne, il faut la réaffecter +print(texte) # Affiche : BONJOUR +``` + +On peut **lire** un caractère par son index, mais pas le **remplacer** : + +```python +mot = "Python" +print(mot[0]) # Affiche : P (lecture : OK) +# mot[0] = "J" # ❌ TypeError : 'str' object does not support item assignment +``` + ### 4. Les Booléens (bool) Les **booléens** ne peuvent prendre que deux valeurs : `True` (vrai) ou `False` (faux). Ils sont essentiels pour les conditions et la logique de votre programme. @@ -338,6 +433,25 @@ vide = None print(type(vide)) # Affiche : ``` +### `isinstance()` : la façon recommandée de tester un type + +`type()` affiche le type exact, mais pour **tester** si une variable est d'un type donné, on préfère `isinstance(valeur, type)`, qui renvoie un booléen : + +```python +age = 25 +print(isinstance(age, int)) # Affiche : True +print(isinstance(age, str)) # Affiche : False + +nom = "Alice" +print(isinstance(nom, str)) # Affiche : True + +# On peut tester plusieurs types à la fois (avec un tuple) +valeur = 3.14 +print(isinstance(valeur, (int, float))) # Affiche : True +``` + +> 💡 Préférez `isinstance(x, int)` à `type(x) == int` : `isinstance` tient compte de l'héritage (un objet d'une sous-classe est reconnu comme une instance de la classe parente), ce qui est presque toujours le comportement souhaité. + --- ## Conversion Entre Types (Casting) @@ -545,6 +659,26 @@ x = 5 # Affectation : x reçoit la valeur 5 x == 5 # Comparaison : est-ce que x est égal à 5 ? (retourne True) ``` +### Comparaisons enchaînées + +Python permet d'**enchaîner** plusieurs comparaisons sur une même ligne, comme en mathématiques. C'est plus lisible que de les combiner avec `and`. + +```python +age = 25 + +# « age est-il entre 18 (inclus) et 65 (exclu) ? » +print(18 <= age < 65) # Affiche : True + +# Équivaut à (mais plus court et plus lisible) : +print(18 <= age and age < 65) # Affiche : True + +note = 14 +if 10 <= note < 16: + print("Mention assez bien ou bien") # s'affiche +``` + +Dans `18 <= age < 65`, Python n'évalue `age` **qu'une seule fois**. L'enchaînement fonctionne avec tous les opérateurs de comparaison (`<`, `<=`, `>`, `>=`, `==`, `!=`). + ### Comparaison de chaînes On peut aussi comparer des chaînes de caractères : @@ -635,7 +769,25 @@ print(peut_conduire) # Affiche : False # Avec parenthèses pour clarifier est_weekend = True a_argent = False -peut_sortir = est_weekend and (a_argent or not a_argent) # Toujours vrai ! +peut_sortir = est_weekend and (a_argent or not a_argent) # (a_argent or not a_argent) vaut toujours True, donc ceci équivaut à est_weekend (ici True) +``` + +### Court-circuit : ce que renvoient `and` et `or` + +Avec des booléens, `and` et `or` renvoient `True` ou `False`. Mais en réalité, ils renvoient **l'une des deux valeurs évaluées** — pas forcément un booléen : + +```python +print(5 and 3) # Affiche : 3 (1er vrai → `and` renvoie le 2e) +print(0 and 3) # Affiche : 0 (1er faux → `and` s'arrête dessus) +print(0 or "défaut") # Affiche : défaut (`or` renvoie la 1re valeur « vraie ») +``` + +Python s'arrête dès qu'il connaît le résultat : c'est l'**évaluation en court-circuit**. D'où un idiome très courant, la **valeur par défaut** : + +```python +nom = "" # une chaîne vide est « fausse » +affichage = nom or "Anonyme" # si nom est vide/faux, on prend "Anonyme" +print(affichage) # Affiche : Anonyme ``` ### Priorité des opérateurs logiques @@ -665,6 +817,39 @@ resultat = ((not False) and True) or False --- +## Les Opérateurs Binaires (bit à bit) 🔍 + +> 🔍 **Section optionnelle (avancée)** : ces opérateurs manipulent les entiers **bit par bit** (sur leur représentation en base 2). Ils sont moins courants que les précédents — vous pouvez les survoler et y revenir lorsque vous en aurez besoin. + +Ne confondez pas les opérateurs **logiques** (`and`, `or`, `not`), qui raisonnent sur des booléens, avec les opérateurs **binaires** (`&`, `|`, `^`...), qui agissent directement sur les bits des entiers. + +| Opérateur | Nom | Exemple | Résultat | +|-----------|-----|---------|----------| +| `&` | ET binaire | `12 & 10` | `8` | +| `\|` | OU binaire | `12 \| 10` | `14` | +| `^` | OU exclusif (XOR) | `12 ^ 10` | `6` | +| `~` | NON binaire (complément à deux) | `~5` | `-6` | +| `<<` | Décalage à gauche | `1 << 4` | `16` | +| `>>` | Décalage à droite | `16 >> 2` | `4` | + +```python +# 12 s'écrit 0b1100 en binaire, 10 s'écrit 0b1010 +print(12 & 10) # Affiche : 8 (bits présents dans les deux) +print(12 | 10) # Affiche : 14 (bits présents dans l'un ou l'autre) +print(12 ^ 10) # Affiche : 6 (bits présents dans un seul des deux) + +# Les décalages reviennent à multiplier / diviser par des puissances de 2 +print(1 << 4) # Affiche : 16 (1 * 2**4) +print(16 >> 2) # Affiche : 4 (16 // 2**2) + +# La fonction bin() montre la représentation binaire d'un entier +print(bin(12)) # Affiche : 0b1100 +``` + +Ces opérateurs servent surtout aux **drapeaux binaires** (combiner plusieurs options dans un seul entier), à la programmation bas niveau (réseau, formats de fichiers binaires) et à certaines optimisations. + +--- + ## Les Opérateurs d'Appartenance Ces opérateurs testent si une valeur est présente dans une séquence (chaîne, liste, etc.). @@ -719,6 +904,8 @@ print(valeur == None) # Affiche : True (fonctionne mais moins idiomatique) **Note** : Pour comparer avec `None`, on utilise généralement `is None` plutôt que `== None`. +> ⚠️ **N'utilisez `is` que pour l'identité** (typiquement avec `None`), **jamais pour comparer des valeurs** — pour cela, utilisez `==`. `is` teste si deux variables désignent le **même objet en mémoire**, ce qui ne garantit pas l'égalité des valeurs (et dépend de détails d'implémentation, comme la mise en cache des petits entiers). Écrire `x is 5` déclenche d'ailleurs un avertissement en Python 3 : `SyntaxWarning: "is" with 'int' literal. Did you mean "=="?`. Règle simple : **`==` pour les valeurs, `is` pour `None`**. + --- ## Entrée Utilisateur avec input() @@ -820,6 +1007,32 @@ print(f"{nom:<10}") # Aligné à gauche sur 10 caractères print(f"{nom:^10}") # Centré sur 10 caractères ``` +### Expressions auto-documentées (`f"{...=}"`) + +Depuis Python 3.8, ajouter `=` à la fin d'une expression dans une f-string affiche à la fois l'expression **et** sa valeur — très pratique pour le débogage : + +```python +nom = "Alice" +age = 25 +print(f"{nom=}") # Affiche : nom='Alice' +print(f"{age=}") # Affiche : age=25 +print(f"{age * 2=}") # Affiche : age * 2=50 +``` + +### Les paramètres de `print()` : `sep` et `end` + +Par défaut, `print()` **sépare ses arguments par une espace** et **termine par un retour à la ligne**. Ces deux comportements se règlent avec `sep` et `end` : + +```python +print("a", "b", "c") # Affiche : a b c (séparés par une espace) +print("a", "b", "c", sep="-") # Affiche : a-b-c +print("2024", "12", "25", sep="/") # Affiche : 2024/12/25 + +# end : ce qui termine la ligne (par défaut un retour à la ligne) +print("Chargement", end="...") +print("terminé") # Affiche sur une seule ligne : Chargement...terminé +``` + --- ## Commentaires dans le Code @@ -835,13 +1048,13 @@ age = 25 # Commentaire après du code ### Commentaires multi-lignes -Pour des explications plus longues, utilisez trois guillemets : +Vous rencontrerez souvent des chaînes entre triple guillemets (`"""`) utilisées comme « commentaires » sur plusieurs lignes : ```python """ -Ceci est un commentaire -sur plusieurs lignes. -Python l'ignore complètement. +Ceci ressemble à un commentaire, +mais c'est en réalité une chaîne de caractères. +Python n'en fait rien ici (elle est créée puis ignorée). """ nom = "Alice" @@ -855,6 +1068,8 @@ Ou utilisez plusieurs lignes avec `#` : # avec des dièses ``` +> ⚠️ **Nuance importante** : une chaîne entre triple guillemets (`"""..."""`) n'est **pas** un commentaire au sens strict — c'est une *chaîne de caractères*. Python l'évalue bien (il crée l'objet `str`) puis la jette si elle n'est ni assignée ni utilisée. Le seul cas où une telle chaîne joue un rôle particulier est la **docstring** : la première instruction d'un module, d'une fonction ou d'une classe (voir la section 1.4). Pour de vrais commentaires, privilégiez toujours `#`. + ### Bonnes pratiques pour les commentaires ✅ **Bon** : Expliquer le "pourquoi" et le "comment" complexe diff --git a/01-fondamentaux-et-syntaxe/03-structures-de-controle.md b/01-fondamentaux-et-syntaxe/03-structures-de-controle.md index 8d69b68..cb2a534 100644 --- a/01-fondamentaux-et-syntaxe/03-structures-de-controle.md +++ b/01-fondamentaux-et-syntaxe/03-structures-de-controle.md @@ -67,6 +67,8 @@ print("Ligne en dehors du if") **Convention** : Utilisez toujours **4 espaces** pour l'indentation (la plupart des éditeurs de code le font automatiquement quand vous appuyez sur Tab). +> ⚠️ **Ne mélangez jamais tabulations et espaces** pour indenter un même bloc : Python lèverait une erreur `TabError: inconsistent use of tabs and spaces in indentation`. Le plus simple est de configurer votre éditeur pour qu'il insère 4 espaces quand vous appuyez sur Tab — VS Code le fait par défaut pour les fichiers `.py`. + ❌ **Erreur d'indentation** : ```python if age >= 18: @@ -179,6 +181,23 @@ else: --- +## Tester directement une valeur : la « véracité » + +Une condition n'a pas besoin d'être une comparaison : **n'importe quelle valeur** peut servir de condition. Python l'évalue alors selon sa *véracité* (les valeurs « vraies » / « fausses » vues à la section 1.2). Pour rappel, sont considérées comme **fausses** : `0`, `0.0`, `""` (chaîne vide), `None` et les collections vides ; tout le reste est **vrai**. + +```python +nom = input("Votre nom : ") + +if nom: # vrai si nom n'est PAS vide + print(f"Bonjour {nom} !") +else: + print("Vous n'avez rien saisi.") +``` + +C'est l'écriture **idiomatique** : `if nom:` équivaut à `if nom != "":` ou `if len(nom) > 0:`, mais en plus court et plus lisible. De même, `if not nom:` teste « `nom` est vide ». + +--- + ## Conditions Multiples Vous pouvez combiner plusieurs conditions avec les opérateurs logiques `and`, `or` et `not`. @@ -897,6 +916,8 @@ Externe : i = 2 Interne : j = 1 ``` +> ⚠️ **`break` et `continue` n'agissent que sur la boucle qui les contient directement** (la plus interne). Pour interrompre d'un coup plusieurs boucles imbriquées, extrayez le code dans une fonction et utilisez `return`, ou servez-vous d'un drapeau (variable booléenne). + --- ## La Clause `else` avec les Boucles @@ -941,6 +962,63 @@ else: --- +## L'opérateur *walrus* `:=` (Python 3.8+) + +L'opérateur `:=`, surnommé *walrus* (« morse » — il évoque deux yeux `:` et des défenses `=`), est un **opérateur d'affectation dans une expression** : il **affecte une valeur à une variable ET renvoie cette valeur**, au même endroit. Il évite de répéter un calcul ou une saisie et rend souvent les `while` et les `if` plus concis. + +### Dans une boucle `while` : lire et tester en une fois + +Sans le walrus, il faut écrire la saisie **deux fois** (avant la boucle, puis à la fin) : + +```python +ligne = input("Mot (ou 'fin' pour arrêter) : ") +while ligne != "fin": + print(f"Vous avez tapé : {ligne}") + ligne = input("Mot (ou 'fin' pour arrêter) : ") # répétition ! +``` + +Avec le walrus, la saisie et le test tiennent sur **une seule ligne** : + +```python +while (ligne := input("Mot (ou 'fin' pour arrêter) : ")) != "fin": + print(f"Vous avez tapé : {ligne}") +``` + +> 💡 Les parenthèses autour de `(ligne := ...)` sont **souvent nécessaires** : elles lèvent l'ambiguïté avec l'opérateur de comparaison. + +### Dans un `if` : calculer une fois, réutiliser ensuite + +```python +nombres = [3, 7, 2, 9, 4, 8, 1] + +if (n := len(nombres)) > 5: + print(f"La liste contient {n} éléments (plus de 5)") +# La liste contient 7 éléments (plus de 5) +``` + +Ici, `len(nombres)` n'est calculé **qu'une seule fois** : le résultat est stocké dans `n`, testé, puis réutilisé dans le message. + +### Dans une compréhension : éviter un double calcul + +```python +def carre(x): + return x * x + +# carre(x) n'est évalué qu'une seule fois par élément +resultats = [c for x in range(6) if (c := carre(x)) > 4] +print(resultats) # [9, 16, 25] +``` + +### Quand l'utiliser ? (avec parcimonie) + +✅ **Utile** quand il évite une vraie répétition : saisie dans un `while`, calcul réutilisé dans un `if`. + +❌ **À éviter** s'il rend la ligne difficile à lire — dans le doute, une affectation classique sur sa propre ligne reste parfaitement valable. + +⚠️ **Ne confondez pas** `=` (affectation classique : une instruction) et `:=` (affectation-expression : elle renvoie une valeur). `:=` ne remplace pas un simple `=` en début de ligne. + +--- + ## L'instruction `match/case` (Python 3.10+) Introduite dans Python 3.10, l'instruction `match/case` permet de comparer une valeur à plusieurs motifs. C'est une alternative élégante aux longues chaînes de `if/elif`. @@ -1013,7 +1091,7 @@ match code: - **Utilisez `match/case`** quand vous comparez une valeur à plusieurs cas distincts (menus, codes d'erreur, commandes, etc.) - **Préférez `if/elif`** pour des conditions avec des comparaisons complexes (`>=`, `<`, combinaisons avec `and`/`or`) -> 💡 `match/case` est bien plus puissant que ces exemples simples. Il supporte le *pattern matching* structurel (décomposition de listes, objets, etc.), que nous verrons dans les chapitres avancés. +> 💡 `match/case` est bien plus puissant que ces exemples simples. Il supporte le *pattern matching* structurel (décomposition de listes, objets, etc.) — un usage avancé que vous pourrez approfondir dans la [documentation officielle](https://docs.python.org/fr/3/tutorial/controlflow.html#match-statements). --- diff --git a/01-fondamentaux-et-syntaxe/04-fonctions-et-portee.md b/01-fondamentaux-et-syntaxe/04-fonctions-et-portee.md index 0ac09df..69a0fbe 100644 --- a/01-fondamentaux-et-syntaxe/04-fonctions-et-portee.md +++ b/01-fondamentaux-et-syntaxe/04-fonctions-et-portee.md @@ -413,7 +413,7 @@ option1: test kwargs: {'extra1': 'a', 'extra2': 'b'} ``` -**Ordre obligatoire** : paramètres normaux, `*args`, paramètres avec défaut, `**kwargs` +**Ordre dans la signature** : paramètres normaux, puis `*args`, puis d'éventuels paramètres supplémentaires, et enfin `**kwargs`. Un paramètre placé **après `*args`** (ici `option1`) doit obligatoirement être passé **par son nom** lors de l'appel (`option1="test"`) — on parle de paramètre *keyword-only*. --- @@ -649,6 +649,28 @@ print(bonsoir("Bob")) # Affiche : Bonsoir Bob ! print(hello("Charlie")) # Affiche : Hello Charlie ! ``` +### Modifier une variable englobante avec `nonlocal` + +Tout comme `global` donne accès à une variable globale, le mot-clé `nonlocal` permet à une fonction imbriquée de **modifier** une variable de la fonction qui l'englobe (au lieu d'en créer une nouvelle, locale) : + +```python +def compteur(): + total = 0 # variable de la fonction englobante + + def incrementer(): + nonlocal total # on modifie le `total` de compteur(), pas une nouvelle variable + total += 1 + return total + + print(incrementer()) # Affiche : 1 + print(incrementer()) # Affiche : 2 + print(incrementer()) # Affiche : 3 + +compteur() +``` + +Sans `nonlocal`, la ligne `total += 1` lèverait une `UnboundLocalError` : Python considérerait `total` comme une nouvelle variable locale à `incrementer`. Nous approfondirons ce mécanisme (les *closures*) au module 5. + --- ## Fonctions comme Objets de Première Classe @@ -783,7 +805,7 @@ print(somme_recursive(nombres)) # Affiche : 15 ### ⚠️ Attention : limite de récursion -Python a une limite au nombre d'appels récursifs (par défaut environ 1000). Pour des valeurs élevées, préférez une approche itérative : +Python limite le nombre d'appels récursifs imbriqués (**1000 par défaut** ; consultable avec `sys.getrecursionlimit()` et ajustable avec `sys.setrecursionlimit()`). Au-delà, il lève une `RecursionError`. Pour des valeurs élevées, préférez une approche itérative : ```python # Version récursive (limitée) @@ -881,7 +903,7 @@ def calcul(x): ## Annotations de Type (Type Hints) -Python permet d'ajouter des **annotations de type** pour indiquer le type attendu des paramètres et du retour. Ces annotations n'ont **aucun effet** sur l'exécution (Python reste dynamique), mais elles améliorent la lisibilité et permettent aux outils (comme mypy) de détecter des erreurs. +Python permet d'ajouter des **annotations de type** pour indiquer le type attendu des paramètres et du retour. Ces annotations ne changent rien au **comportement** de la fonction à l'exécution — Python ne vérifie pas les types et reste dynamique —, mais elles améliorent la lisibilité et permettent aux outils (comme mypy) de détecter des erreurs. ### Syntaxe de base @@ -908,7 +930,7 @@ def diviser(a: float, b: float) -> float: ### Types complexes -Depuis Python 3.10, les types génériques s'écrivent directement avec les types natifs (plus besoin d'importer depuis `typing`) : +Depuis Python 3.9, les types génériques s'écrivent directement avec les types natifs comme `list`, `dict` ou `tuple` (plus besoin d'importer `List`, `Dict`, etc. depuis `typing`) : ```python def traiter_nombres(nombres: list[int]) -> int: @@ -1093,7 +1115,7 @@ def calculer_imc(poids: float, taille: float) -> float: float: L'IMC calculé Exemple: - >>> calculer_imc(70, 1.75) + >>> round(calculer_imc(70, 1.75), 2) 22.86 """ return poids / (taille ** 2) @@ -1220,6 +1242,8 @@ print(generer_mot_de_passe(8, avec_symboles=False)) print(generer_mot_de_passe(16)) ``` +> ⚠️ **Sécurité** : le module `random` n'est **pas** adapté à la génération de secrets (sa suite de nombres est prévisible). Pour de vrais mots de passe ou jetons, utilisez le module `secrets` (cryptographiquement sûr) : remplacez `random.choice(caracteres)` par `secrets.choice(caracteres)`. + ### Exemple 4 : Calculateur de statistiques ```python @@ -1234,7 +1258,7 @@ def calculer_statistiques(nombres: list) -> dict: dict: Dictionnaire contenant les statistiques """ if not nombres: - return None + return {} # liste vide → dictionnaire vide (cohérent avec l'annotation -> dict) nombres_tries = sorted(nombres) n = len(nombres) @@ -1327,6 +1351,8 @@ print(ajouter_a_liste(1)) # [1] print(ajouter_a_liste(2)) # [1, 2] - la liste est partagée ! ``` +> 💡 **Pourquoi ?** Les valeurs par défaut sont évaluées **une seule fois**, au moment où Python **définit** la fonction (et non à chaque appel). La liste `[]` est donc créée une fois pour toutes, puis **partagée** entre tous les appels qui ne fournissent pas leur propre liste. + ✅ **Correct** ```python def ajouter_a_liste(element, liste=None): diff --git a/01-fondamentaux-et-syntaxe/05-gestion-des-erreurs.md b/01-fondamentaux-et-syntaxe/05-gestion-des-erreurs.md index f9f8075..68f633e 100644 --- a/01-fondamentaux-et-syntaxe/05-gestion-des-erreurs.md +++ b/01-fondamentaux-et-syntaxe/05-gestion-des-erreurs.md @@ -26,16 +26,16 @@ Les **erreurs de syntaxe** se produisent quand vous écrivez du code qui ne resp # Oublier les deux points if age >= 18 print("Majeur") -# SyntaxError: invalid syntax +# SyntaxError: expected ':' # Oublier de fermer une parenthèse print("Bonjour" -# SyntaxError: unexpected EOF while parsing +# SyntaxError: '(' was never closed # Indentation incorrecte def ma_fonction(): print("Erreur") -# IndentationError: expected an indented block +# IndentationError: expected an indented block after function definition ``` Ces erreurs doivent être **corrigées dans le code**. Elles ne peuvent pas être gérées avec try/except car le programme ne peut pas s'exécuter. @@ -149,7 +149,7 @@ Utiliser `except` sans spécifier le type d'erreur capture **toutes** les erreur try: # Code risqué pass -except TypeErreur: +except ExceptionType: # ExceptionType = à remplacer par le vrai type (ValueError, KeyError...) # Gérer ce type d'erreur spécifique pass ``` @@ -305,7 +305,7 @@ Le fichier sera fermé que l'opération réussisse ou non ! try: print("1. Dans try") # Code risqué -except: +except Exception: print("2. Dans except (si erreur)") else: print("3. Dans else (si pas d'erreur)") @@ -371,7 +371,7 @@ Vous pouvez **lever** (déclencher) volontairement une exception avec le mot-cl ### Syntaxe ```python -raise TypeException("Message d'erreur") +raise ExceptionType("Message d'erreur") # ExceptionType = ValueError, RuntimeError, une exception perso... ``` ### Exemples @@ -578,7 +578,7 @@ except KeyError: valeur = None ``` -**Exception** : Le principe EAFP ("Easier to Ask for Forgiveness than Permission") est parfois préféré en Python, mais avec modération. +**Exception** : Le principe EAFP (« Easier to Ask for Forgiveness than Permission ») est souvent préféré en Python (voir la section dédiée plus bas). Pour ce cas précis d'un dictionnaire, la méthode la plus idiomatique reste cependant `dictionnaire.get(cle)`, qui renvoie `None` (ou une valeur par défaut que vous précisez) lorsque la clé est absente. ### 3. Ne cachez pas les erreurs @@ -705,6 +705,8 @@ except LookupError: print("Erreur de recherche") ``` +> 💡 `SystemExit` (déclenché par `sys.exit()`) et `KeyboardInterrupt` (Ctrl+C pour interrompre le programme) héritent directement de `BaseException`, **pas** de `Exception`. Conséquence pratique : un `except Exception:` ne les capture **pas** — l'utilisateur peut donc toujours arrêter le programme avec Ctrl+C. C'est une raison de plus de préférer `except Exception:` au `except:` nu, qui, lui, attrape absolument tout. + ### Ordre des blocs except Mettez toujours les exceptions **les plus spécifiques en premier** : @@ -1110,18 +1112,28 @@ except FileNotFoundError: ## Erreurs Courantes à Éviter -### 1. Capturer Exception ou BaseException +### 1. Capturer `BaseException`, ou capturer large sans rien faire -❌ **Mauvais** +❌ **Mauvais** : capturer `BaseException` ```python try: # Code pass -except Exception: # Trop large +except BaseException: # Capture AUSSI KeyboardInterrupt (Ctrl+C) et SystemExit ! pass ``` -Cela capture presque tout, y compris des erreurs que vous ne devriez pas ignorer. +Capturer `BaseException` intercepte même `KeyboardInterrupt` et `SystemExit` : l'utilisateur ne peut plus interrompre le programme avec Ctrl+C. Et un `except ... : pass` qui **avale silencieusement** l'erreur masque les vrais problèmes. + +✅ **Acceptable** : capturer `Exception` (et non `BaseException`) **à condition de traiter, journaliser ou relancer** l'erreur — jamais de l'ignorer : +```python +try: + # Code + pass +except Exception as e: + print(f"Erreur inattendue : {e}") # au minimum, on informe + raise # et on relance si on ne sait pas vraiment la gérer +``` ### 2. Bloc except vide @@ -1174,7 +1186,7 @@ f = open("fichier.txt") try: # Traitement pass -except: +except Exception: # Gestion d'erreur pass f.close() # Ne sera pas exécuté si erreur dans except ! @@ -1186,7 +1198,7 @@ f = open("fichier.txt") try: # Traitement pass -except: +except Exception: # Gestion d'erreur pass finally: diff --git a/01-fondamentaux-et-syntaxe/06-type-hints-et-annotations.md b/01-fondamentaux-et-syntaxe/06-type-hints-et-annotations.md index 0fae0ee..2b2fc42 100644 --- a/01-fondamentaux-et-syntaxe/06-type-hints-et-annotations.md +++ b/01-fondamentaux-et-syntaxe/06-type-hints-et-annotations.md @@ -117,7 +117,7 @@ def afficher_message(texte: str) -> None: ### Types de Base ```python -def exemples_types_base(): +def exemples_types_base() -> None: # Nombres entiers nombre: int = 42 @@ -138,7 +138,7 @@ def exemples_types_base(): ## Types Génériques (Collections) -Depuis Python 3.10, les types génériques s'écrivent directement avec les types natifs. Plus besoin d'importer depuis le module `typing` pour les cas courants. +Depuis Python 3.9, les types génériques s'écrivent directement avec les types natifs. Plus besoin d'importer depuis le module `typing` pour les cas courants. > 💡 Dans du code plus ancien (Python < 3.9), vous verrez `from typing import List, Dict, Tuple, Set`. C'est la même chose, juste l'ancienne syntaxe. @@ -264,7 +264,7 @@ def diviser(a: int | float, b: int | float) -> float: # Fonction qui peut retourner str ou int def obtenir_valeur(cle: str) -> str | int: - valeurs = {"nom": "Alice", "age": 25} + valeurs: dict[str, str | int] = {"nom": "Alice", "age": 25} return valeurs.get(cle, "inconnu") ``` @@ -310,6 +310,8 @@ def afficher(valeur: object) -> None: `Callable` permet d'annoter des fonctions passées en paramètre. +> 💡 `Callable` peut s'importer depuis `typing` **ou**, de préférence depuis Python 3.9, depuis `collections.abc` (`from collections.abc import Callable`). Les deux sont équivalents ; `typing.Callable` est aujourd'hui considéré comme « doucement déprécié ». Vous croiserez les deux dans cette section. + ```python from typing import Callable @@ -367,7 +369,7 @@ Pour des types complexes répétés, vous pouvez créer des alias : ```python from typing import Any -# Créer un alias avec l'affectation simple (Python 3.10+) +# Créer un alias avec une simple affectation (Python 3.9+) Vector = list[float] Matrix = list[list[float]] JSON = dict[str, Any] @@ -429,19 +431,27 @@ afficher_personne(alice) ```python from typing import TypedDict -class Utilisateur(TypedDict, total=False): - nom: str # Obligatoire - age: int # Obligatoire - email: str | None # Optionnel - telephone: str | None # Optionnel +# Champs obligatoires (par défaut, total=True) +class UtilisateurBase(TypedDict): + nom: str + age: int + +# Champs facultatifs : regroupés dans un TypedDict total=False hérité +class Utilisateur(UtilisateurBase, total=False): + email: str # Facultatif (la clé peut être absente) + telephone: str # Facultatif ``` +> 💡 `total=False` rend **tous** les champs d'un `TypedDict` facultatifs. Pour mélanger champs obligatoires et facultatifs, on combine deux `TypedDict` par héritage (comme ci-dessus) ou — depuis Python 3.11 — on marque chaque champ avec `Required[...]` / `NotRequired[...]`. + --- ## Génériques (Generics) Les génériques permettent de créer des fonctions et classes qui fonctionnent avec n'importe quel type. +> 💡 **Deux syntaxes coexistent.** Cette section présente la syntaxe classique avec `TypeVar`, valable dans toutes les versions. Depuis **Python 3.12** (PEP 695), une syntaxe plus concise évite de déclarer `TypeVar` : `def premier_element[T](liste: list[T]) -> T:` et `class Pile[T]:`. Les deux produisent le même résultat. + ### TypeVar ```python @@ -665,8 +675,8 @@ resultat2 = additionner("5", "3") # Erreur ! **Exécution de mypy** : ```bash $ mypy calcul.py -calcul.py:7: error: Argument 1 to "additionner" has incompatible type "str"; expected "int" -calcul.py:7: error: Argument 2 to "additionner" has incompatible type "str"; expected "int" +calcul.py:8: error: Argument 1 to "additionner" has incompatible type "str"; expected "int" [arg-type] +calcul.py:8: error: Argument 2 to "additionner" has incompatible type "str"; expected "int" [arg-type] ``` ### Configuration de mypy @@ -921,7 +931,7 @@ def calculer_statistiques(donnees: list[Nombre]) -> Statistiques: "ecart_type": stdev(donnees) if len(donnees) > 1 else 0.0 } -def analyser_notes(notes: list[int]) -> tuple[float, list[str]]: +def analyser_notes(notes: list[Nombre]) -> tuple[float, list[str]]: """ Analyse une liste de notes et retourne la moyenne et les appréciations. @@ -950,7 +960,7 @@ def analyser_notes(notes: list[int]) -> tuple[float, list[str]]: return moyenne, appreciations # Utilisation -notes_classe = [12, 15, 8, 18, 14, 11, 16, 13] +notes_classe: list[Nombre] = [12, 15, 8, 18, 14, 11, 16, 13] moyenne, appreciations = analyser_notes(notes_classe) print(f"Moyenne de la classe : {moyenne:.2f}") ``` @@ -1063,8 +1073,14 @@ def rechercher_utilisateur( >>> rechercher_utilisateur("Bob", age_min=20, age_max=30) [{'nom': 'Bob', 'age': 28}] """ - # Implémentation - pass + # Données d'exemple (en pratique : une base de données) + base = [{"nom": "Alice", "age": 25}, {"nom": "Bob", "age": 28}] + resultats = [u for u in base if u["nom"].lower() == nom.lower()] + if age_min is not None: + resultats = [u for u in resultats if u["age"] >= age_min] + if age_max is not None: + resultats = [u for u in resultats if u["age"] <= age_max] + return resultats ``` --- diff --git a/01-fondamentaux-et-syntaxe/README.md b/01-fondamentaux-et-syntaxe/README.md index a92d857..b261866 100644 --- a/01-fondamentaux-et-syntaxe/README.md +++ b/01-fondamentaux-et-syntaxe/README.md @@ -16,7 +16,7 @@ Avant de plonger dans le code, prenons un moment pour comprendre pourquoi Python ### Un langage créé pour les humains -Python a été conçu dans les années 1990 par Guido van Rossum avec une philosophie claire : **la lisibilité compte**. Contrairement à de nombreux autres langages qui ressemblent à du charabia incompréhensible, Python se lit presque comme de l'anglais. +Python a été créé par Guido van Rossum, qui en a débuté le développement en décembre 1989 pour aboutir à une première version publique en 1991. Dès l'origine, le langage repose sur une philosophie claire : **la lisibilité compte**. Contrairement à de nombreux autres langages qui ressemblent à du charabia incompréhensible, Python se lit presque comme de l'anglais. Comparez par vous-même : @@ -53,7 +53,7 @@ Python n'est pas cantonné à un seul domaine. Il excelle dans de nombreux domai Python possède l'une des communautés les plus actives et bienveillantes : -- **Plus de 400 000 packages** disponibles sur PyPI (Python Package Index) +- **Plus de 800 000 packages** disponibles sur PyPI (Python Package Index) - Des millions de développeurs dans le monde - Une documentation abondante et des tutoriels pour tous les niveaux - Des forums d'entraide actifs (Stack Overflow, Reddit, Discord) @@ -74,19 +74,19 @@ Python a une philosophie, une sorte de guide spirituel pour les programmeurs Pyt Voici quelques principes clés qui guident Python : -> **Beautiful is better than ugly.** +> **Beautiful is better than ugly.** > La beauté vaut mieux que la laideur. (Un code propre et lisible) -> **Explicit is better than implicit.** +> **Explicit is better than implicit.** > L'explicite vaut mieux que l'implicite. (Soyez clair dans vos intentions) -> **Simple is better than complex.** +> **Simple is better than complex.** > La simplicité vaut mieux que la complexité. (Ne compliquez pas inutilement) -> **Readability counts.** +> **Readability counts.** > La lisibilité compte. (Votre code sera lu plus souvent qu'il ne sera écrit) -> **There should be one-- and preferably only one --obvious way to do it.** +> **There should be one-- and preferably only one --obvious way to do it.** > Il devrait y avoir une -- et de préférence une seule -- façon évidente de faire quelque chose. Ces principes vous guideront tout au long de votre apprentissage et de votre carrière Python. @@ -330,7 +330,7 @@ La programmation est une compétence qui se développe avec le temps et la prati ### Citation inspirante -> "Le seul moyen d'apprendre un nouveau langage de programmation est d'écrire des programmes dans ce langage." +> "Le seul moyen d'apprendre un nouveau langage de programmation est d'écrire des programmes dans ce langage." > — **Dennis Ritchie**, créateur du langage C --- @@ -354,14 +354,14 @@ Bonne chance et surtout... **amusez-vous bien !** 🚀 - [Tutoriel officiel Python](https://docs.python.org/fr/3/tutorial/) ### Communautés francophones -- [Discord Python France](https://discord.gg/python-fr) -- [Reddit r/FrancePython](https://www.reddit.com/r/FrancePython/) -- [Forum OpenClassrooms Python](https://openclassrooms.com/fr/courses) +- [AFPy - Association Francophone Python](https://www.afpy.org/) - La communauté Python francophone de référence (conférence PyConFR, traduction de la documentation officielle...) +- [Forum de l'AFPy](https://discuss.afpy.org/) - Entraide, discussions, offres d'emploi et événements +- [Forum OpenClassrooms Python](https://openclassrooms.com/fr/courses) - Cours et discussions en français ### Outils recommandés - [Python.org](https://www.python.org/) - Site officiel - [Visual Studio Code](https://code.visualstudio.com/) - Éditeur de code -- [Python Tutor](http://pythontutor.com/) - Visualiser l'exécution du code +- [Python Tutor](https://pythontutor.com/) - Visualiser l'exécution du code - [Repl.it](https://replit.com/) - Coder Python en ligne sans installation --- diff --git a/01-fondamentaux-et-syntaxe/exemples/02_02_regles_nommage.py b/01-fondamentaux-et-syntaxe/exemples/02_02_regles_nommage.py index 81ebb44..0d91e00 100644 --- a/01-fondamentaux-et-syntaxe/exemples/02_02_regles_nommage.py +++ b/01-fondamentaux-et-syntaxe/exemples/02_02_regles_nommage.py @@ -19,7 +19,9 @@ # 2age = 30 # Commence par un chiffre # mon-age = 25 # Contient un tiret # for = 10 # Mot réservé Python -# mon âge = 25 # Contient un espace et un accent +# mon âge = 25 # Contient un espace (c'est l'espace le problème, PAS l'accent) +# Remarque : depuis Python 3, les accents sont AUTORISÉS dans les noms (âge = 25 fonctionne) ; +# PEP 8 recommande toutefois de s'en tenir à l'ASCII (préférez 'age' à 'âge'). # --- Bons noms de variables --- nom_complet = "Alice Dupont" diff --git a/01-fondamentaux-et-syntaxe/exemples/02_04_nombres_flottants.py b/01-fondamentaux-et-syntaxe/exemples/02_04_nombres_flottants.py index 8a0af15..92f4b1c 100644 --- a/01-fondamentaux-et-syntaxe/exemples/02_04_nombres_flottants.py +++ b/01-fondamentaux-et-syntaxe/exemples/02_04_nombres_flottants.py @@ -20,7 +20,7 @@ print(resultat) # Affiche : 0.30000000000000004 (!) # --- Notation scientifique --- -grand_nombre = 3e8 # 3 × 10^8 = 300000000 +grand_nombre = 3e8 # 3 × 10^8 = 300000000.0 (la notation scientifique donne toujours un float) petit_nombre = 1.5e-4 # 1.5 × 10^-4 = 0.00015 print(grand_nombre) # 300000000.0 diff --git a/01-fondamentaux-et-syntaxe/exemples/02_12_operateurs_logiques.py b/01-fondamentaux-et-syntaxe/exemples/02_12_operateurs_logiques.py index 3da5e7e..458b2ab 100644 --- a/01-fondamentaux-et-syntaxe/exemples/02_12_operateurs_logiques.py +++ b/01-fondamentaux-et-syntaxe/exemples/02_12_operateurs_logiques.py @@ -49,7 +49,7 @@ # Avec parenthèses pour clarifier est_weekend = True a_argent = False -peut_sortir = est_weekend and (a_argent or not a_argent) # Toujours vrai ! +peut_sortir = est_weekend and (a_argent or not a_argent) # (a_argent or not a_argent) = toujours True, donc ceci vaut est_weekend (ici True) print(peut_sortir) # Affiche : True # --- Priorité des opérateurs logiques --- diff --git a/01-fondamentaux-et-syntaxe/exemples/02_14_operateurs_identite.py b/01-fondamentaux-et-syntaxe/exemples/02_14_operateurs_identite.py index f609dd5..dd301d2 100644 --- a/01-fondamentaux-et-syntaxe/exemples/02_14_operateurs_identite.py +++ b/01-fondamentaux-et-syntaxe/exemples/02_14_operateurs_identite.py @@ -16,3 +16,6 @@ valeur = None print(valeur is None) # Affiche : True (recommandé) print(valeur == None) # Affiche : True (fonctionne mais moins idiomatique) + +# Règle : 'is' uniquement pour l'identité (souvent avec None), '==' pour les valeurs. +# Écrire 'x is 5' déclenche un SyntaxWarning : "is" with 'int' literal. Did you mean "=="? diff --git a/01-fondamentaux-et-syntaxe/exemples/02_16_formatage_chaines.py b/01-fondamentaux-et-syntaxe/exemples/02_16_formatage_chaines.py index 44056ab..afae2dd 100644 --- a/01-fondamentaux-et-syntaxe/exemples/02_16_formatage_chaines.py +++ b/01-fondamentaux-et-syntaxe/exemples/02_16_formatage_chaines.py @@ -43,3 +43,10 @@ print(f"{nom:>10}") # Aligné à droite sur 10 caractères print(f"{nom:<10}") # Aligné à gauche sur 10 caractères print(f"{nom:^10}") # Centré sur 10 caractères + +# --- Expressions auto-documentées f"{...=}" (Python 3.8+, utile au débogage) --- +nom = "Alice" +age = 25 +print(f"{nom=}") # Affiche : nom='Alice' +print(f"{age=}") # Affiche : age=25 +print(f"{age * 2=}") # Affiche : age * 2=50 diff --git a/01-fondamentaux-et-syntaxe/exemples/02_17_commentaires.py b/01-fondamentaux-et-syntaxe/exemples/02_17_commentaires.py index d3ff9e1..c5d9d6b 100644 --- a/01-fondamentaux-et-syntaxe/exemples/02_17_commentaires.py +++ b/01-fondamentaux-et-syntaxe/exemples/02_17_commentaires.py @@ -9,16 +9,20 @@ age = 25 # Commentaire après du code print(age) # 25 -# --- Commentaires multi-lignes --- +# --- "Commentaires" multi-lignes : ATTENTION, ce ne sont pas de vrais commentaires ! --- +# Une chaîne entre triple guillemets n'est PAS un commentaire : c'est une chaîne de +# caractères que Python crée puis ignore si on ne l'utilise pas. Son seul rôle spécial +# est d'être une docstring (1re instruction d'une fonction/classe/module, voir 1.4). """ -Ceci est un commentaire -sur plusieurs lignes. -Python l'ignore complètement. +Ceci ressemble à un commentaire, +mais c'est en réalité une chaîne de caractères. +Python n'en fait rien ici (elle est créée puis ignorée). """ nom = "Alice" print(nom) # Alice +# La vraie façon de "commenter" sur plusieurs lignes : préfixer CHAQUE ligne par # # Ceci est également un commentaire # sur plusieurs lignes # avec des dièses diff --git a/01-fondamentaux-et-syntaxe/exemples/02_20_affectation_multiple.py b/01-fondamentaux-et-syntaxe/exemples/02_20_affectation_multiple.py new file mode 100644 index 0000000..f094923 --- /dev/null +++ b/01-fondamentaux-et-syntaxe/exemples/02_20_affectation_multiple.py @@ -0,0 +1,22 @@ +# ============================================================================ +# Section 2.20 : Affectation multiple +# Description : Affecter plusieurs variables d'un coup, affectation chaînée, +# échange de deux variables sans variable temporaire +# Fichier source : 02-variables-types-et-operateurs.md +# ============================================================================ + +# --- Affecter plusieurs variables à la fois --- +x, y, z = 1, 2, 3 +print(x) # Affiche : 1 +print(y) # Affiche : 2 +print(z) # Affiche : 3 + +# --- Donner la même valeur à plusieurs variables (affectation chaînée) --- +a = b = c = 0 +print(a, b, c) # Affiche : 0 0 0 + +# --- Échanger le contenu de deux variables (sans variable temporaire) --- +x = 10 +y = 20 +x, y = y, x # Python évalue d'abord la droite (y, x), puis affecte à gauche +print(x, y) # Affiche : 20 10 diff --git a/01-fondamentaux-et-syntaxe/exemples/02_21_operateurs_binaires.py b/01-fondamentaux-et-syntaxe/exemples/02_21_operateurs_binaires.py new file mode 100644 index 0000000..ab0442b --- /dev/null +++ b/01-fondamentaux-et-syntaxe/exemples/02_21_operateurs_binaires.py @@ -0,0 +1,20 @@ +# ============================================================================ +# Section 2.21 : Les Opérateurs Binaires (bit à bit) +# Description : &, |, ^, ~, <<, >> sur les entiers ; bin() pour visualiser +# la représentation binaire (section optionnelle, avancée) +# Fichier source : 02-variables-types-et-operateurs.md +# ============================================================================ + +# 12 s'écrit 0b1100 en binaire, 10 s'écrit 0b1010 +print(12 & 10) # Affiche : 8 (ET binaire : bits présents dans les deux) +print(12 | 10) # Affiche : 14 (OU binaire : bits présents dans l'un ou l'autre) +print(12 ^ 10) # Affiche : 6 (XOR : bits présents dans un seul des deux) +print(~5) # Affiche : -6 (NON binaire : complément à deux, ~x == -(x+1)) + +# --- Décalages : multiplier / diviser par des puissances de 2 --- +print(1 << 4) # Affiche : 16 (décalage à gauche : 1 * 2**4) +print(16 >> 2) # Affiche : 4 (décalage à droite : 16 // 2**2) + +# --- bin() montre la représentation binaire d'un entier --- +print(bin(12)) # Affiche : 0b1100 +print(bin(10)) # Affiche : 0b1010 diff --git a/01-fondamentaux-et-syntaxe/exemples/02_22_sequences_echappement.py b/01-fondamentaux-et-syntaxe/exemples/02_22_sequences_echappement.py new file mode 100644 index 0000000..1e22f6b --- /dev/null +++ b/01-fondamentaux-et-syntaxe/exemples/02_22_sequences_echappement.py @@ -0,0 +1,18 @@ +# ============================================================================ +# Section 2.22 : Séquences d'échappement et chaînes brutes +# Description : \n \t \\ \" \' (caractères spéciaux) ; chaîne brute r"..." +# Fichier source : 02-variables-types-et-operateurs.md +# ============================================================================ + +# --- Séquences d'échappement (le \ introduit un caractère spécial) --- +print("Ligne 1\nLigne 2") # \n = saut de ligne (affiche sur deux lignes) +print("Nom :\tAlice") # \t = tabulation +print("Un antislash : \\") # \\ = un seul antislash +print("Il a dit \"Bonjour\"") # \" = guillemet double dans la chaîne +print('J\'aime Python') # \' = apostrophe dans une chaîne entre ' + +# --- Chaîne brute r"..." : les \ ne sont PAS interprétés --- +chemin = "dossier\nouveau" # \n interprété comme un saut de ligne ! +chemin_brut = r"dossier\nouveau" # chaîne brute : le \ reste littéral +print(chemin) +print(chemin_brut) # Affiche : dossier\nouveau diff --git a/01-fondamentaux-et-syntaxe/exemples/02_23_comparaisons_chainees.py b/01-fondamentaux-et-syntaxe/exemples/02_23_comparaisons_chainees.py new file mode 100644 index 0000000..5f30e35 --- /dev/null +++ b/01-fondamentaux-et-syntaxe/exemples/02_23_comparaisons_chainees.py @@ -0,0 +1,21 @@ +# ============================================================================ +# Section 2.23 : Comparaisons enchaînées +# Description : enchaîner des comparaisons (18 <= age < 65), équivalence avec and +# Fichier source : 02-variables-types-et-operateurs.md +# ============================================================================ + +age = 25 + +# « age est-il entre 18 (inclus) et 65 (exclu) ? » +print(18 <= age < 65) # True + +# Équivaut à (mais plus court et plus lisible) : +print(18 <= age and age < 65) # True + +note = 14 +if 10 <= note < 16: + print("Mention assez bien ou bien") + +# L'enchaînement marche avec tous les opérateurs de comparaison +print(1 < 2 < 3 < 4) # True +print(0 <= age <= 17) # False diff --git a/01-fondamentaux-et-syntaxe/exemples/02_24_court_circuit_logique.py b/01-fondamentaux-et-syntaxe/exemples/02_24_court_circuit_logique.py new file mode 100644 index 0000000..1c467ee --- /dev/null +++ b/01-fondamentaux-et-syntaxe/exemples/02_24_court_circuit_logique.py @@ -0,0 +1,16 @@ +# ============================================================================ +# Section 2.24 : Court-circuit de and / or (valeurs de retour) +# Description : and/or renvoient l'une des valeurs ; idiome de la valeur par défaut +# Fichier source : 02-variables-types-et-operateurs.md +# ============================================================================ + +# and / or renvoient l'une des deux VALEURS, pas forcément un booléen +print(5 and 3) # 3 (1er vrai -> and renvoie le 2e) +print(0 and 3) # 0 (1er faux -> and s'arrete dessus) +print(0 or "défaut") # défaut (or renvoie la 1re valeur « vraie ») +print("Alice" or "Anonyme") # Alice + +# Idiome courant : la valeur par défaut +nom = "" # une chaîne vide est « fausse » +affichage = nom or "Anonyme" # si nom est vide/faux, on prend "Anonyme" +print(affichage) # Anonyme diff --git a/01-fondamentaux-et-syntaxe/exemples/02_25_isinstance.py b/01-fondamentaux-et-syntaxe/exemples/02_25_isinstance.py new file mode 100644 index 0000000..7447ecf --- /dev/null +++ b/01-fondamentaux-et-syntaxe/exemples/02_25_isinstance.py @@ -0,0 +1,20 @@ +# ============================================================================ +# Section 2.25 : isinstance() - tester le type d'une variable +# Description : isinstance(x, type) vs type() ; test multi-types ; héritage +# Fichier source : 02-variables-types-et-operateurs.md +# ============================================================================ + +age = 25 +print(isinstance(age, int)) # True +print(isinstance(age, str)) # False + +nom = "Alice" +print(isinstance(nom, str)) # True + +# Tester plusieurs types à la fois (avec un tuple) +valeur = 3.14 +print(isinstance(valeur, (int, float))) # True + +# isinstance tient compte de l'héritage : en Python, un bool EST un int +print(isinstance(True, int)) # True (bool hérite de int) +print(type(True) == int) # False (type() ne voit pas l'héritage) diff --git a/01-fondamentaux-et-syntaxe/exemples/02_26_immuabilite_chaines.py b/01-fondamentaux-et-syntaxe/exemples/02_26_immuabilite_chaines.py new file mode 100644 index 0000000..496ebd3 --- /dev/null +++ b/01-fondamentaux-et-syntaxe/exemples/02_26_immuabilite_chaines.py @@ -0,0 +1,23 @@ +# ============================================================================ +# Section 2.26 : Immuabilité des chaînes +# Description : les str sont immuables ; les méthodes renvoient une nouvelle +# chaîne ; mot[0] = ... lève une TypeError +# Fichier source : 02-variables-types-et-operateurs.md +# ============================================================================ + +# --- Les méthodes renvoient une NOUVELLE chaîne (l'originale est inchangée) --- +texte = "bonjour" +texte.upper() # renvoie "BONJOUR"... mais ne modifie PAS texte +print(texte) # bonjour (inchangé !) + +texte = texte.upper() # pour « changer » la chaîne, il faut la réaffecter +print(texte) # BONJOUR + +# --- Lecture par index : OK ; modification en place : interdite --- +mot = "Python" +print(mot[0]) # P (lecture) + +try: + mot[0] = "J" # une chaîne est immuable +except TypeError as e: + print(f"TypeError : {e}") # 'str' object does not support item assignment diff --git a/01-fondamentaux-et-syntaxe/exemples/02_27_comparer_flottants.py b/01-fondamentaux-et-syntaxe/exemples/02_27_comparer_flottants.py new file mode 100644 index 0000000..7410e30 --- /dev/null +++ b/01-fondamentaux-et-syntaxe/exemples/02_27_comparer_flottants.py @@ -0,0 +1,20 @@ +# ============================================================================ +# Section 2.27 : Comparer des nombres flottants +# Description : ne pas comparer des floats avec == ; math.isclose() ; +# decimal.Decimal pour des calculs décimaux exacts +# Fichier source : 02-variables-types-et-operateurs.md +# ============================================================================ + +import math +from decimal import Decimal + +# --- Le piège : == sur des flottants --- +print(0.1 + 0.2) # 0.30000000000000004 +print(0.1 + 0.2 == 0.3) # False (le piège !) + +# --- La bonne façon : math.isclose() --- +print(math.isclose(0.1 + 0.2, 0.3)) # True + +# --- Calculs décimaux exacts : module decimal (ex. sommes d'argent) --- +print(Decimal("0.1") + Decimal("0.2")) # 0.3 +print(Decimal("0.1") + Decimal("0.2") == Decimal("0.3")) # True diff --git a/01-fondamentaux-et-syntaxe/exemples/02_28_print_sep_end.py b/01-fondamentaux-et-syntaxe/exemples/02_28_print_sep_end.py new file mode 100644 index 0000000..df2234a --- /dev/null +++ b/01-fondamentaux-et-syntaxe/exemples/02_28_print_sep_end.py @@ -0,0 +1,20 @@ +# ============================================================================ +# Section 2.28 : Les paramètres de print() : sep et end +# Description : sep (séparateur entre arguments) et end (fin de ligne) ; +# comportement par défaut (espace entre args + retour à la ligne) +# Fichier source : 02-variables-types-et-operateurs.md +# ============================================================================ + +# Par défaut : arguments séparés par une espace, ligne finie par "\n" +print("a", "b", "c") # a b c + +# sep : changer le séparateur entre les arguments +print("a", "b", "c", sep="-") # a-b-c +print("2024", "12", "25", sep="/") # 2024/12/25 + +# end : changer ce qui termine la ligne (par défaut "\n") +print("Chargement", end="...") +print("terminé") # Chargement...terminé (sur une seule ligne) + +# Combiner sep et end +print("x", "y", sep="", end="!\n") # xy! diff --git a/01-fondamentaux-et-syntaxe/exemples/03_15_match_case.py b/01-fondamentaux-et-syntaxe/exemples/03_15_match_case.py index 1268fd8..502f9b0 100644 --- a/01-fondamentaux-et-syntaxe/exemples/03_15_match_case.py +++ b/01-fondamentaux-et-syntaxe/exemples/03_15_match_case.py @@ -1,10 +1,35 @@ # ============================================================================ # Section 3.15 : L'instruction match/case (Python 3.10+) -# Description : Pattern matching - codes HTTP, motifs multiples +# Description : Pattern matching - menu (motif simple), motifs multiples (|), +# codes HTTP # Fichier source : 03-structures-de-controle.md # ============================================================================ +# --- Menu de commande (motif simple) --- +commande = "thé" # en interactif : commande = input("Votre commande : ") +match commande: + case "café": + print("Voici votre café ☕") + case "thé": + print("Voici votre thé 🍵") + case "jus": + print("Voici votre jus 🧃") + case _: + print("Commande non disponible") + +# --- Motifs multiples avec | --- +print() +jour = "samedi" # en interactif : jour = input("Quel jour ? ") +match jour: + case "samedi" | "dimanche": + print("C'est le weekend !") + case "lundi" | "mardi" | "mercredi" | "jeudi" | "vendredi": + print("C'est un jour de semaine") + case _: + print("Jour non reconnu") + # --- Exemple : code HTTP --- +print() code = 404 match code: diff --git a/01-fondamentaux-et-syntaxe/exemples/03_17_exemples_pratiques.py b/01-fondamentaux-et-syntaxe/exemples/03_17_exemples_pratiques.py index 564f763..80daf74 100644 --- a/01-fondamentaux-et-syntaxe/exemples/03_17_exemples_pratiques.py +++ b/01-fondamentaux-et-syntaxe/exemples/03_17_exemples_pratiques.py @@ -142,6 +142,5 @@ else: print(f"{nombre} n'est pas premier") else: - if not (len(sys.argv) > 1 and sys.argv[1] == "--interactif"): - print("\nExemples interactifs disponibles avec --interactif") - print(" python3 03_17_exemples_pratiques.py --interactif") + print("\nExemples interactifs disponibles avec --interactif") + print(" python3 03_17_exemples_pratiques.py --interactif") diff --git a/01-fondamentaux-et-syntaxe/exemples/03_19_operateur_walrus.py b/01-fondamentaux-et-syntaxe/exemples/03_19_operateur_walrus.py new file mode 100644 index 0000000..4a691de --- /dev/null +++ b/01-fondamentaux-et-syntaxe/exemples/03_19_operateur_walrus.py @@ -0,0 +1,28 @@ +# ============================================================================ +# Section 3.19 : L'opérateur walrus := (Python 3.8+) +# Description : Affectation dans une expression - if (réutiliser une valeur), +# compréhension (éviter un double calcul), while (lire + tester) +# Fichier source : 03-structures-de-controle.md +# ============================================================================ + +# --- if : calculer une fois et réutiliser --- +nombres = [3, 7, 2, 9, 4, 8, 1] +if (n := len(nombres)) > 5: + print(f"La liste contient {n} éléments (plus de 5)") + +# --- Compréhension : éviter un double calcul --- +def carre(x): + return x * x + + +# carre(x) n'est évalué qu'une seule fois par élément +resultats = [c for x in range(6) if (c := carre(x)) > 4] +print(f"Carrés > 4 : {resultats}") # [9, 16, 25] + +# --- while : lire et tester en une seule expression --- +# Version non interactive : on consomme une file de saisies simulées. +# En interactif : while (mot := input("Mot (ou 'fin') : ")) != "fin": +saisies = iter(["bonjour", "python", "fin", "ignoré"]) +print("\nLecture jusqu'à 'fin' :") +while (mot := next(saisies, "fin")) != "fin": + print(f" Mot reçu : {mot}") diff --git a/01-fondamentaux-et-syntaxe/exemples/03_20_veracite_condition.py b/01-fondamentaux-et-syntaxe/exemples/03_20_veracite_condition.py new file mode 100644 index 0000000..0bee026 --- /dev/null +++ b/01-fondamentaux-et-syntaxe/exemples/03_20_veracite_condition.py @@ -0,0 +1,31 @@ +# ============================================================================ +# Section 3.20 : Tester directement une valeur (la « véracité ») +# Description : une condition peut être n'importe quelle valeur ; idiome +# if nom: / if not nom: plutôt que == "" ou len() > 0 +# Fichier source : 03-structures-de-controle.md +# ============================================================================ + +# --- Une valeur "vraie" / "fausse" sert directement de condition --- +nom = "Alice" +if nom: # vrai si nom n'est PAS vide + print(f"Bonjour {nom} !") +else: + print("Vous n'avez rien saisi.") + +# Chaîne vide = "fausse" +texte = "" +if texte: + print("Le texte contient quelque chose") +else: + print("Le texte est vide") # s'affiche + +# --- if not X teste "X est vide / faux" --- +saisie = "" +if not saisie: + print("Rien n'a ete saisi") # s'affiche + +# --- Équivalences (toutes vraies pour une chaîne non vide) --- +nom = "Bob" +print(bool(nom)) # True +print(nom != "") # True (équivalent verbeux) +print(len(nom) > 0) # True (équivalent verbeux) diff --git a/01-fondamentaux-et-syntaxe/exemples/04_09_fonctions_imbriquees.py b/01-fondamentaux-et-syntaxe/exemples/04_09_fonctions_imbriquees.py index 1644f8a..3745aa7 100644 --- a/01-fondamentaux-et-syntaxe/exemples/04_09_fonctions_imbriquees.py +++ b/01-fondamentaux-et-syntaxe/exemples/04_09_fonctions_imbriquees.py @@ -33,3 +33,20 @@ def saluer(nom): print(bonjour("Alice")) # Affiche : Bonjour Alice ! print(bonsoir("Bob")) # Affiche : Bonsoir Bob ! print(hello("Charlie")) # Affiche : Hello Charlie ! + +# --- nonlocal : modifier une variable de la fonction englobante --- +print() + +def compteur(): + total = 0 # variable de la fonction englobante + + def incrementer(): + nonlocal total # modifie le total de compteur(), pas une nouvelle variable + total += 1 + return total + + print(incrementer()) # 1 + print(incrementer()) # 2 + print(incrementer()) # 3 + +compteur() diff --git a/01-fondamentaux-et-syntaxe/exemples/04_14_exemples_pratiques.py b/01-fondamentaux-et-syntaxe/exemples/04_14_exemples_pratiques.py index c172986..9520b27 100644 --- a/01-fondamentaux-et-syntaxe/exemples/04_14_exemples_pratiques.py +++ b/01-fondamentaux-et-syntaxe/exemples/04_14_exemples_pratiques.py @@ -57,6 +57,8 @@ def calculer_prix_final(prix_ht: float, # 100*5 = 500, remise 10% = 50, HT après remise = 450, TVA = 90, TTC = 540 # --- Générateur de mot de passe --- +# NOTE SÉCURITÉ : 'random' n'est PAS sûr pour de vrais secrets (suite prévisible). +# Pour un vrai mot de passe / jeton, utilisez le module 'secrets' : secrets.choice(caracteres). print("\n=== Générateur de mot de passe ===") def generer_mot_de_passe(longueur: int = 12, @@ -86,7 +88,7 @@ def generer_mot_de_passe(longueur: int = 12, def calculer_statistiques(nombres: list) -> dict: """Calcule diverses statistiques sur une liste de nombres.""" if not nombres: - return None + return {} # liste vide → dictionnaire vide (cohérent avec -> dict) nombres_tries = sorted(nombres) n = len(nombres) diff --git a/01-fondamentaux-et-syntaxe/exemples/04_15_erreur_mutable_defaut.py b/01-fondamentaux-et-syntaxe/exemples/04_15_erreur_mutable_defaut.py index 6837e35..8d55093 100644 --- a/01-fondamentaux-et-syntaxe/exemples/04_15_erreur_mutable_defaut.py +++ b/01-fondamentaux-et-syntaxe/exemples/04_15_erreur_mutable_defaut.py @@ -5,6 +5,8 @@ # ============================================================================ # --- Erreur classique --- +# Les valeurs par défaut sont évaluées UNE SEULE FOIS, à la définition de la +# fonction (pas à chaque appel) : la liste [] est donc partagée entre les appels. def ajouter_a_liste_bug(element, liste=[]): liste.append(element) return liste diff --git a/01-fondamentaux-et-syntaxe/exemples/05_06_clause_finally.py b/01-fondamentaux-et-syntaxe/exemples/05_06_clause_finally.py index c1a0da9..8c20cfc 100644 --- a/01-fondamentaux-et-syntaxe/exemples/05_06_clause_finally.py +++ b/01-fondamentaux-et-syntaxe/exemples/05_06_clause_finally.py @@ -8,7 +8,7 @@ print("=== Sans erreur ===") try: print("1. Dans try") -except: +except Exception: print("2. Dans except (si erreur)") else: print("3. Dans else (si pas d'erreur)") @@ -20,7 +20,7 @@ try: print("1. Dans try") resultat = 10 / 0 # Erreur ! -except: +except Exception: print("2. Dans except (si erreur)") else: print("3. Dans else (si pas d'erreur)") diff --git a/01-fondamentaux-et-syntaxe/exemples/05_12_eafp_vs_lbyl.py b/01-fondamentaux-et-syntaxe/exemples/05_12_eafp_vs_lbyl.py index d1ca48e..c386582 100644 --- a/01-fondamentaux-et-syntaxe/exemples/05_12_eafp_vs_lbyl.py +++ b/01-fondamentaux-et-syntaxe/exemples/05_12_eafp_vs_lbyl.py @@ -4,6 +4,8 @@ # Fichier source : 05-gestion-des-erreurs.md # ============================================================================ +import os + # --- LBYL : Look Before You Leap (vérifier avant d'agir) --- dictionnaire = {"nom": "Alice", "age": 25} @@ -26,19 +28,18 @@ print() # LBYL -import os fichier = "test_inexistant.txt" if os.path.exists(fichier): with open(fichier) as f: contenu = f.read() - print(f"LBYL - contenu lu") + print("LBYL - contenu lu") else: - print(f"LBYL - fichier n'existe pas") + print("LBYL - fichier n'existe pas") # EAFP try: with open(fichier) as f: contenu = f.read() - print(f"EAFP - contenu lu") + print("EAFP - contenu lu") except FileNotFoundError: - print(f"EAFP - fichier n'existe pas") + print("EAFP - fichier n'existe pas") diff --git a/01-fondamentaux-et-syntaxe/exemples/06_01_type_hints_base.py b/01-fondamentaux-et-syntaxe/exemples/06_01_type_hints_base.py index 6f2ebd6..01dc3da 100644 --- a/01-fondamentaux-et-syntaxe/exemples/06_01_type_hints_base.py +++ b/01-fondamentaux-et-syntaxe/exemples/06_01_type_hints_base.py @@ -6,11 +6,15 @@ # ============================================================================ # --- Typage dynamique de Python --- -age = 25 # int -nom = "Alice" # str -prix = 19.99 # float - -print(type(age), type(nom), type(prix)) +# (dans une fonction pour ne pas entrer en conflit avec les variables +# annotées plus bas : Python infère les types tout seul) +def demo_typage_dynamique() -> None: + age = 25 # int + nom = "Alice" # str + prix = 19.99 # float + print(type(age), type(nom), type(prix)) + +demo_typage_dynamique() # --- Sans vs avec type hints --- # Sans type hints diff --git a/01-fondamentaux-et-syntaxe/exemples/06_03_types_optionnels_unions.py b/01-fondamentaux-et-syntaxe/exemples/06_03_types_optionnels_unions.py index bc92695..4ac11f2 100644 --- a/01-fondamentaux-et-syntaxe/exemples/06_03_types_optionnels_unions.py +++ b/01-fondamentaux-et-syntaxe/exemples/06_03_types_optionnels_unions.py @@ -38,7 +38,7 @@ def diviser(a: int | float, b: int | float) -> float: print(f"diviser(10, 3) = {diviser(10, 3):.4f}") def obtenir_valeur(cle: str) -> str | int: - valeurs = {"nom": "Alice", "age": 25} + valeurs: dict[str, str | int] = {"nom": "Alice", "age": 25} return valeurs.get(cle, "inconnu") print(f"obtenir_valeur('nom') = {obtenir_valeur('nom')}") diff --git a/01-fondamentaux-et-syntaxe/exemples/06_06_typeddict.py b/01-fondamentaux-et-syntaxe/exemples/06_06_typeddict.py index 8bbe2d3..3de18ac 100644 --- a/01-fondamentaux-et-syntaxe/exemples/06_06_typeddict.py +++ b/01-fondamentaux-et-syntaxe/exemples/06_06_typeddict.py @@ -28,12 +28,20 @@ def afficher_personne(personne: Personne) -> None: afficher_personne(alice) afficher_personne(creer_personne("Bob", 30, "Lyon")) -# --- TypedDict avec champs optionnels --- -class Utilisateur(TypedDict, total=False): +# --- TypedDict avec champs obligatoires ET facultatifs --- +# Champs obligatoires (par défaut, total=True) +class UtilisateurBase(TypedDict): nom: str age: int - email: str | None - telephone: str | None +# Champs facultatifs regroupés dans un TypedDict total=False hérité +class Utilisateur(UtilisateurBase, total=False): + email: str # Facultatif (la clé peut être absente) + telephone: str # Facultatif + +# nom et age obligatoires ; email / telephone facultatifs user: Utilisateur = {"nom": "Charlie", "age": 28} print(f"Utilisateur : {user}") + +user2: Utilisateur = {"nom": "Dina", "age": 35, "email": "dina@example.com"} +print(f"Utilisateur : {user2}") diff --git a/01-fondamentaux-et-syntaxe/exemples/06_14_statistiques.py b/01-fondamentaux-et-syntaxe/exemples/06_14_statistiques.py index 969e708..5f76fc6 100644 --- a/01-fondamentaux-et-syntaxe/exemples/06_14_statistiques.py +++ b/01-fondamentaux-et-syntaxe/exemples/06_14_statistiques.py @@ -34,7 +34,7 @@ def calculer_statistiques(donnees: list[Nombre]) -> Statistiques: "ecart_type": stdev(donnees) if len(donnees) > 1 else 0.0 } -def analyser_notes(notes: list[int]) -> tuple[float, list[str]]: +def analyser_notes(notes: list[Nombre]) -> tuple[float, list[str]]: """ Analyse une liste de notes et retourne la moyenne et les appréciations. @@ -63,7 +63,7 @@ def analyser_notes(notes: list[int]) -> tuple[float, list[str]]: return moyenne, appreciations # Utilisation -notes_classe = [12, 15, 8, 18, 14, 11, 16, 13] +notes_classe: list[Nombre] = [12, 15, 8, 18, 14, 11, 16, 13] moyenne, appreciations = analyser_notes(notes_classe) print(f"Moyenne de la classe : {moyenne:.2f}") diff --git a/01-fondamentaux-et-syntaxe/exemples/06_17_documentation_type_hints.py b/01-fondamentaux-et-syntaxe/exemples/06_17_documentation_type_hints.py new file mode 100644 index 0000000..2e96e3a --- /dev/null +++ b/01-fondamentaux-et-syntaxe/exemples/06_17_documentation_type_hints.py @@ -0,0 +1,46 @@ +# ============================================================================ +# Section 6.17 : Type hints et documentation (docstring + doctest) +# Description : combiner type hints + docstring détaillée + exemples doctest +# exécutables (python -m doctest 06_17_documentation_type_hints.py) +# Fichier source : 06-type-hints-et-annotations.md +# ============================================================================ + + +def rechercher_utilisateur( + nom: str, + age_min: int | None = None, + age_max: int | None = None, +) -> list[dict[str, str | int]]: + """ + Recherche des utilisateurs selon des critères. + + Les type hints indiquent les types, la docstring explique la logique + et les cas particuliers. + + Args: + nom: Nom à rechercher (insensible à la casse). + age_min: Âge minimum inclus. Si None, pas de limite inférieure. + age_max: Âge maximum inclus. Si None, pas de limite supérieure. + + Returns: + Liste de dictionnaires {'nom': str, 'age': int} correspondants. + + Examples: + >>> rechercher_utilisateur("Alice") + [{'nom': 'Alice', 'age': 25}] + >>> rechercher_utilisateur("Bob", age_min=20, age_max=30) + [{'nom': 'Bob', 'age': 28}] + """ + # Données d'exemple (en pratique : une base de données) + base = [{"nom": "Alice", "age": 25}, {"nom": "Bob", "age": 28}] + resultats = [u for u in base if u["nom"].lower() == nom.lower()] + if age_min is not None: + resultats = [u for u in resultats if u["age"] >= age_min] + if age_max is not None: + resultats = [u for u in resultats if u["age"] <= age_max] + return resultats + + +print(rechercher_utilisateur("Alice")) # [{'nom': 'Alice', 'age': 25}] +print(rechercher_utilisateur("Bob", age_min=20, age_max=30)) # [{'nom': 'Bob', 'age': 28}] +print(rechercher_utilisateur("Inconnu")) # [] diff --git a/01-fondamentaux-et-syntaxe/exemples/README.md b/01-fondamentaux-et-syntaxe/exemples/README.md index 8e02a4a..2a291ba 100644 --- a/01-fondamentaux-et-syntaxe/exemples/README.md +++ b/01-fondamentaux-et-syntaxe/exemples/README.md @@ -1,6 +1,8 @@ # Exemples - Chapitre 01 : Fondamentaux et syntaxe -Ce dossier contient **84 fichiers** d'exemples exécutables correspondant aux 6 sections du chapitre 01. +Ce dossier contient **96 fichiers** d'exemples exécutables correspondant aux 6 sections du chapitre 01. + +**Convention de nommage** : `SS_NN_description.py`, où `SS` est le numéro de section (01 à 06) et `NN` l'ordre de l'exemple dans la section. Exemple : `03_07_boucle_while.py` = section 3, 7ᵉ exemple. ## Exécution @@ -8,15 +10,30 @@ Ce dossier contient **84 fichiers** d'exemples exécutables correspondant aux 6 python3 nom_du_fichier.py ``` -Certains fichiers contiennent des exemples interactifs (utilisant `input()`). Pour les exécuter : +Deux fichiers proposent des démonstrations **interactives** (avec `input()`) : `02_15_entree_utilisateur.py` et `03_17_exemples_pratiques.py`. Sans argument, ils affichent une démonstration non-interactive ; ajoutez `--interactif` pour saisir les valeurs au clavier : ```bash -python3 nom_du_fichier.py --interactif +python3 02_15_entree_utilisateur.py --interactif +python3 03_17_exemples_pratiques.py --interactif ``` ## Prérequis -- Python 3.10+ (pour la syntaxe `match/case` et `type | None`) +- **Python 3.10+** (pour la syntaxe `match/case` et `type | None`) +- **Aucune dépendance externe** : tous les exemples n'utilisent que la bibliothèque standard +- Tous les exemples sont autonomes et ont été testés (exécution sans erreur, de Python 3.10 à 3.14) + +--- + +## Correspondance avec le cours + +Chaque exemple reprend le code de son fichier `.md` source (colonne « Source »). Pour rester **exécutables et autonomes**, les `.py` adaptent parfois le cours : + +- les appels `input()` sont remplacés par des valeurs fixes (ou placés derrière `--interactif`) ; +- des `print()` et des séparateurs sont ajoutés pour visualiser les résultats ; +- les docstrings peuvent être abrégées, et plusieurs extraits d'une même section sont regroupés dans un seul fichier. + +En revanche, la **logique et les valeurs** des exemples restent identiques à celles du cours. --- @@ -52,6 +69,15 @@ python3 nom_du_fichier.py --interactif | `02_17_commentaires.py` | Commentaires en ligne et multi-lignes | 02-variables-types-et-operateurs.md | | `02_18_conventions_pep8.py` | Conventions PEP 8 | 02-variables-types-et-operateurs.md | | `02_19_erreurs_courantes.py` | Erreurs fréquentes : = vs ==, conversions, etc. | 02-variables-types-et-operateurs.md | +| `02_20_affectation_multiple.py` | Affectation multiple, chaînée et échange de variables | 02-variables-types-et-operateurs.md | +| `02_21_operateurs_binaires.py` | Opérateurs binaires &, \|, ^, ~, <<, >> et bin() | 02-variables-types-et-operateurs.md | +| `02_22_sequences_echappement.py` | Séquences d'échappement (\n, \t, \\) et chaînes brutes r"..." | 02-variables-types-et-operateurs.md | +| `02_23_comparaisons_chainees.py` | Comparaisons enchaînées (18 <= age < 65) | 02-variables-types-et-operateurs.md | +| `02_24_court_circuit_logique.py` | Court-circuit de and/or et idiome de valeur par défaut | 02-variables-types-et-operateurs.md | +| `02_25_isinstance.py` | Tester un type avec isinstance() (vs type(), héritage) | 02-variables-types-et-operateurs.md | +| `02_26_immuabilite_chaines.py` | Immuabilité des chaînes : méthodes renvoyant une nouvelle chaîne, mot[0]= interdit | 02-variables-types-et-operateurs.md | +| `02_27_comparer_flottants.py` | Comparer des flottants : math.isclose() au lieu de ==, decimal.Decimal | 02-variables-types-et-operateurs.md | +| `02_28_print_sep_end.py` | Paramètres de print() : sep (séparateur) et end (fin de ligne) | 02-variables-types-et-operateurs.md | --- @@ -66,17 +92,19 @@ python3 nom_du_fichier.py --interactif | `03_05_conditions_imbriquees.py` | Conditions imbriquées | 03-structures-de-controle.md | | `03_06_operateur_ternaire.py` | Expression conditionnelle (ternaire) | 03-structures-de-controle.md | | `03_07_boucle_while.py` | Boucle while avec compteur, somme, etc. | 03-structures-de-controle.md | -| `03_08_boucle_for.py` | Boucle for, range(), enumerate(), zip() | 03-structures-de-controle.md | +| `03_08_boucle_for.py` | Boucle for : parcourir une chaîne, range(), somme, table de multiplication, triangle | 03-structures-de-controle.md | | `03_09_for_vs_while.py` | Comparaison for vs while | 03-structures-de-controle.md | | `03_10_instruction_break.py` | Instruction break | 03-structures-de-controle.md | | `03_11_instruction_continue.py` | Instruction continue | 03-structures-de-controle.md | | `03_12_instruction_pass.py` | Instruction pass | 03-structures-de-controle.md | | `03_13_boucles_imbriquees.py` | Boucles imbriquées et tables de multiplication | 03-structures-de-controle.md | | `03_14_else_avec_boucles.py` | Clause else avec for et while | 03-structures-de-controle.md | -| `03_15_match_case.py` | Pattern matching (Python 3.10+) | 03-structures-de-controle.md | +| `03_15_match_case.py` | Pattern matching : menu, motifs multiples (`\|`), codes HTTP (Python 3.10+) | 03-structures-de-controle.md | | `03_16_bonnes_pratiques.py` | Bonnes pratiques des structures de contrôle | 03-structures-de-controle.md | | `03_17_exemples_pratiques.py` | Exemples pratiques : nombres premiers, Pascal, PGCD (--interactif) | 03-structures-de-controle.md | | `03_18_piege_range.py` | Piège courant avec range() | 03-structures-de-controle.md | +| `03_19_operateur_walrus.py` | Opérateur walrus `:=` : `if` (réutiliser une valeur), compréhension, `while` (lire et tester) — Python 3.8+ | 03-structures-de-controle.md | +| `03_20_veracite_condition.py` | Tester directement une valeur (véracité) : `if nom:` au lieu de `== ""` / `len() > 0` | 03-structures-de-controle.md | --- @@ -92,7 +120,7 @@ python3 nom_du_fichier.py --interactif | `04_06_args_kwargs.py` | *args et **kwargs | 04-fonctions-et-portee.md | | `04_07_docstrings.py` | Documentation avec docstrings | 04-fonctions-et-portee.md | | `04_08_portee_variables.py` | Portée des variables (locale, globale, LEGB) | 04-fonctions-et-portee.md | -| `04_09_fonctions_imbriquees.py` | Fonctions imbriquées et closures | 04-fonctions-et-portee.md | +| `04_09_fonctions_imbriquees.py` | Fonctions imbriquées, closures et `nonlocal` | 04-fonctions-et-portee.md | | `04_10_fonctions_premiere_classe.py` | Fonctions comme objets de première classe | 04-fonctions-et-portee.md | | `04_11_fonctions_recursives.py` | Récursivité : factorielle, fibonacci, somme | 04-fonctions-et-portee.md | | `04_12_fonctions_lambda.py` | Fonctions lambda | 04-fonctions-et-portee.md | @@ -132,7 +160,7 @@ python3 nom_du_fichier.py --interactif | `06_03_types_optionnels_unions.py` | Type \| None, unions de types | 06-type-hints-et-annotations.md | | `06_04_any_callable.py` | Any, object et Callable | 06-type-hints-et-annotations.md | | `06_05_type_aliases.py` | Alias de types : Vector, Matrix, JSON | 06-type-hints-et-annotations.md | -| `06_06_typeddict.py` | TypedDict : dictionnaires avec structure fixe | 06-type-hints-et-annotations.md | +| `06_06_typeddict.py` | TypedDict : structure fixe, champs obligatoires et facultatifs | 06-type-hints-et-annotations.md | | `06_07_generiques.py` | Génériques : TypeVar, Generic, classe Pile | 06-type-hints-et-annotations.md | | `06_08_literal_final.py` | Literal pour valeurs exactes, Final pour constantes | 06-type-hints-et-annotations.md | | `06_09_classvar.py` | ClassVar pour attributs de classe | 06-type-hints-et-annotations.md | @@ -143,3 +171,4 @@ python3 nom_du_fichier.py --interactif | `06_14_statistiques.py` | Exemple pratique : calculateur de statistiques | 06-type-hints-et-annotations.md | | `06_15_cache_generique.py` | Exemple pratique : cache générique avec expiration | 06-type-hints-et-annotations.md | | `06_16_bonnes_pratiques.py` | Bonnes pratiques des type hints | 06-type-hints-et-annotations.md | +| `06_17_documentation_type_hints.py` | Type hints + docstring détaillée et doctests exécutables | 06-type-hints-et-annotations.md | diff --git a/02-structures-de-donnees/01-listes-tuples-dicts-sets.md b/02-structures-de-donnees/01-listes-tuples-dicts-sets.md index 6ad51ca..3f0122e 100644 --- a/02-structures-de-donnees/01-listes-tuples-dicts-sets.md +++ b/02-structures-de-donnees/01-listes-tuples-dicts-sets.md @@ -154,6 +154,29 @@ nombres.reverse() print(nombres) # [1, 1, 2, 3, 4, 5, 6, 9] ``` +### Trier avec une clé (`key`) + +Le paramètre `key` accepte une fonction appliquée à chaque élément pour décider de l'ordre. C'est l'une des fonctionnalités de tri les plus utiles. + +```python +# Trier des mots par longueur +mots = ["python", "go", "javascript", "c"] +print(sorted(mots, key=len)) # ['c', 'go', 'python', 'javascript'] + +# Trier sans tenir compte de la casse +noms = ["alice", "Bob", "charlie", "David"] +print(sorted(noms, key=str.lower)) # ['alice', 'Bob', 'charlie', 'David'] + +# Trier une liste de tuples par le 2e élément (l'âge) +personnes = [("Alice", 30), ("Bob", 25), ("Charlie", 35)] +print(sorted(personnes, key=lambda p: p[1])) +# [('Bob', 25), ('Alice', 30), ('Charlie', 35)] + +# Combiner key et reverse (du plus âgé au plus jeune) +print(sorted(personnes, key=lambda p: p[1], reverse=True)) +# [('Charlie', 35), ('Alice', 30), ('Bob', 25)] +``` + ### Copier une liste Attention : l'affectation simple ne crée pas une copie ! @@ -178,6 +201,28 @@ print(liste1) # [1, 2, 3] - liste1 n'est pas modifiée print(liste2) # [1, 2, 3, 4] ``` +### Copie superficielle vs copie profonde + +Les méthodes ci-dessus font une **copie superficielle** (*shallow copy*) : elles dupliquent la liste de premier niveau, mais **pas** les objets imbriqués. Pour une liste de listes, les sous-listes restent donc partagées : + +```python +original = [[1, 2], [3, 4]] +copie = original.copy() +copie[0].append(99) # on modifie la sous-liste partagée +print(original) # [[1, 2, 99], [3, 4]] — l'original est touché ! +``` + +Pour une copie totalement indépendante (sous-objets inclus), utilisez `copy.deepcopy` : + +```python +import copy + +original = [[1, 2], [3, 4]] +copie = copy.deepcopy(original) +copie[0].append(99) +print(original) # [[1, 2], [3, 4]] — l'original est intact +``` + ### Listes imbriquées Les listes peuvent contenir d'autres listes, ce qui permet de créer des structures de données plus complexes. @@ -207,11 +252,61 @@ for etudiant in etudiants: --- +## Itérer avec `enumerate()` et `zip()` + +Deux fonctions intégrées rendent l'itération sur les collections beaucoup plus pratique et lisible. + +### `enumerate()` : obtenir l'index **et** la valeur + +Plutôt que de gérer un compteur à la main, `enumerate()` fournit directement la position et l'élément : + +```python +fruits = ["pomme", "banane", "orange"] + +for index, fruit in enumerate(fruits): + print(f"{index}: {fruit}") +# 0: pomme +# 1: banane +# 2: orange + +# Démarrer la numérotation à 1 avec start= +for numero, fruit in enumerate(fruits, start=1): + print(f"{numero}. {fruit}") +# 1. pomme +# 2. banane +# 3. orange +``` + +C'est plus lisible et plus sûr que `for i in range(len(fruits)): ... fruits[i]`. + +### `zip()` : parcourir plusieurs collections en parallèle + +`zip()` associe les éléments de plusieurs collections, position par position : + +```python +noms = ["Alice", "Bob", "Charlie"] +ages = [30, 25, 35] + +for nom, age in zip(noms, ages): + print(f"{nom} a {age} ans") +# Alice a 30 ans +# Bob a 25 ans +# Charlie a 35 ans + +# Construire un dictionnaire à partir de deux listes +personnes = dict(zip(noms, ages)) +print(personnes) # {'Alice': 30, 'Bob': 25, 'Charlie': 35} +``` + +> 💡 `zip()` s'arrête à la collection la plus **courte**. Pour imposer des longueurs égales (et obtenir une `ValueError` sinon), utilisez `zip(a, b, strict=True)` (Python 3.10+). + +--- + ## Les Tuples ### Qu'est-ce qu'un tuple ? -Un tuple est similaire à une liste, mais il est **immuable** (non modifiable). Une fois créé, vous ne pouvez pas modifier, ajouter ou supprimer ses éléments. Les tuples sont plus rapides que les listes et protègent vos données contre les modifications accidentelles. +Un tuple est similaire à une liste, mais il est **immuable** (non modifiable). Une fois créé, vous ne pouvez pas modifier, ajouter ou supprimer ses éléments. Les tuples sont légèrement plus rapides à créer et plus économes en mémoire que les listes, mais leur véritable atout est ailleurs : ils protègent vos données contre les modifications accidentelles et peuvent servir de clés de dictionnaire (contrairement aux listes). ### Créer un tuple @@ -258,6 +353,30 @@ coordonnees = (10, 20) # coordonnees.append(30) # AttributeError ``` +Mais attention : cette immuabilité est **« de surface »**. Un tuple fige les *références* qu'il contient, pas le contenu des objets pointés. S'il contient un objet **mutable** (une liste, par exemple), cet objet-là reste modifiable : + +```python +donnees = (1, [2, 3]) + +# Remplacer l'élément est interdit (la référence est figée)... +# donnees[1] = [9] # TypeError: 'tuple' object does not support item assignment + +# ... mais modifier la liste pointée fonctionne, car elle est mutable +donnees[1].append(4) +print(donnees) # (1, [2, 3, 4]) — le contenu du tuple « immuable » a changé ! +``` + +Conséquence concrète : un tuple n'est **hachable** (utilisable comme clé de dictionnaire ou élément d'un set) que si **tous** ses éléments le sont aussi. Dès qu'il contient une liste, il perd cette propriété : + +```python +print(hash((1, 2, 3))) # OK : tous les éléments sont immuables + +# hash((1, [2, 3])) # TypeError: unhashable type: 'list' +# {(1, [2, 3])} # même erreur : impossible comme élément d'un set +``` + +Pour garantir un tuple réellement immuable de bout en bout, n'y placez que des éléments eux-mêmes immuables (nombres, chaînes, autres tuples…). + ### Unpacking (déballage) Le unpacking permet d'assigner les éléments d'un tuple à plusieurs variables en une seule ligne. @@ -566,6 +685,23 @@ print(personne.setdefault("ville", "Paris")) # 'Paris' print(personne) # {'nom': 'Alice', 'age': 25, 'ville': 'Paris'} ``` +### Fusionner des dictionnaires (Python 3.9+) + +Depuis Python 3.9, on peut fusionner deux dictionnaires avec l'opérateur `|` (et `|=` pour fusionner en place) : + +```python +defaut = {"couleur": "noir", "taille": "M"} +choix = {"taille": "L", "motif": "rayé"} + +# | crée un NOUVEAU dictionnaire ; en cas de clé commune, la valeur de droite l'emporte +fusion = defaut | choix +print(fusion) # {'couleur': 'noir', 'taille': 'L', 'motif': 'rayé'} + +# |= met à jour le dictionnaire de gauche en place (équivalent à update()) +defaut |= choix +print(defaut) # {'couleur': 'noir', 'taille': 'L', 'motif': 'rayé'} +``` + --- ## Les Sets (Ensembles) @@ -591,7 +727,7 @@ print(nombres_uniques) # {1, 2, 3, 4} # Créer un set à partir d'une chaîne lettres = set("hello") -print(lettres) # {'h', 'e', 'l', 'o'} +print(lettres) # ex. {'h', 'e', 'l', 'o'} (l'ordre d'affichage varie) ``` ### Caractéristiques importantes @@ -606,13 +742,17 @@ nombres = {1, 2, 2, 3, 3, 3} print(nombres) # {1, 2, 3} # Les éléments doivent être immuables (hashables) -# Vous pouvez avoir des nombres, des chaînes, des tuples -valide = {1, "texte", (1, 2), True} +# Vous pouvez avoir des nombres (int, float), des booléens, des chaînes, des tuples +valide = {1, "texte", (1, 2), 3.14, True} # Mais pas de listes ou de dictionnaires # invalide = {[1, 2, 3]} # TypeError ``` +> 💡 **Ordre d'affichage** : un set n'ayant pas d'ordre, son affichage ne montre qu'**un** ordre possible parmi d'autres. Pour les ensembles de **chaînes**, cet ordre **change même d'une exécution à l'autre** (à cause de la randomisation du hachage des chaînes). Les commentaires `# {...}` qui suivent illustrent donc un résultat possible, jamais un ordre garanti. + +> 💡 Subtilité : `True` et `1` partagent la même valeur et le même hachage ; dans un ensemble, ils comptent donc comme **un seul** élément. L'ensemble `valide` ci-dessus contient ainsi 4 éléments distincts (`True` se confond avec `1`). + ### Ajouter et supprimer des éléments ```python @@ -782,6 +922,51 @@ set_de_sets = { --- +## Dépaqueter avec `*` et `**` : passer et combiner des collections + +Les opérateurs `*` (pour les séquences : listes, tuples) et `**` (pour les dictionnaires) permettent de **dépaqueter** une collection — soit pour la passer à une fonction, soit pour la combiner avec d'autres. + +### Passer une collection à une fonction + +```python +nombres = [3, 1, 4, 1, 5] + +# Sans * : la liste est UN seul argument +print(max(nombres)) # 5 + +# Avec * : chaque élément devient un argument séparé +print(*nombres) # 3 1 4 1 5 + +def afficher(a, b, c): + print(f"{a}, {b}, {c}") + +afficher(*[10, 20, 30]) # 10, 20, 30 (* dépaquète la liste) + +# ** dépaquète un dictionnaire en arguments nommés (clé=valeur) +infos = {"a": 1, "b": 2, "c": 3} +afficher(**infos) # 1, 2, 3 +``` + +### Combiner des collections + +```python +# Combiner des listes (ou tuples) avec * +debut = [1, 2] +fin = [4, 5] +tout = [*debut, 3, *fin] +print(tout) # [1, 2, 3, 4, 5] + +# Combiner des dictionnaires avec ** (la valeur de droite l'emporte) +defaut = {"couleur": "noir", "taille": "M"} +choix = {"taille": "L"} +fusion = {**defaut, **choix} +print(fusion) # {'couleur': 'noir', 'taille': 'L'} +``` + +> 💡 `{**a, **b}` produit le même résultat que l'opérateur `a | b` vu plus haut, mais fonctionne aussi sur les versions de Python **antérieures à 3.9**. + +--- + ## Tableau récapitulatif | Structure | Ordonné | Modifiable | Doublons | Syntaxe | Cas d'usage principal | diff --git a/02-structures-de-donnees/02-comprehensions.md b/02-structures-de-donnees/02-comprehensions.md index e374396..e24e62e 100644 --- a/02-structures-de-donnees/02-comprehensions.md +++ b/02-structures-de-donnees/02-comprehensions.md @@ -189,7 +189,7 @@ print(combinaisons) # ('vert', 'S'), ('vert', 'M'), ('vert', 'L'), # ('bleu', 'S'), ('bleu', 'M'), ('bleu', 'L')] -# Multiplication de matrices (liste de listes) +# Parcourir tous les éléments d'une matrice (liste de listes), ligne par ligne matrice = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] elements = [element for ligne in matrice for element in ligne] print(elements) # [1, 2, 3, 4, 5, 6, 7, 8, 9] @@ -318,7 +318,7 @@ print(bonnes_notes) # {'Alice': 18, 'Charlie': 15} nombres = {f"n{i}": i for i in range(10) if i % 2 == 0} print(nombres) # {'n0': 0, 'n2': 2, 'n4': 4, 'n6': 6, 'n8': 8} -# Créer un dictionnaire de mots avec plus de 4 lettres +# Créer un dictionnaire de mots de plus de 2 lettres mots = ["le", "chat", "et", "le", "chien"] mots_longs = {mot: len(mot) for mot in mots if len(mot) > 2} print(mots_longs) # {'chat': 4, 'chien': 5} @@ -329,7 +329,7 @@ print(mots_longs) # {'chat': 4, 'chien': 5} ```python # Appliquer une réduction de 20% sur tous les prix prix = {"pomme": 2.5, "banane": 1.8, "orange": 3.0} -prix_soldes = {produit: prix * 0.8 for produit, prix in prix.items()} +prix_soldes = {produit: round(prix * 0.8, 2) for produit, prix in prix.items()} print(prix_soldes) # {'pomme': 2.0, 'banane': 1.44, 'orange': 2.4} # Convertir toutes les valeurs en chaînes de caractères @@ -439,7 +439,7 @@ print(carres) # {0, 1, 4, 9, 16} # Extraire les caractères uniques d'une chaîne texte = "hello world" caracteres_uniques = {c for c in texte if c != ' '} -print(caracteres_uniques) # {'h', 'e', 'l', 'o', 'w', 'r', 'd'} +print(caracteres_uniques) # {'h', 'e', 'l', 'o', 'w', 'r', 'd'} (ordre variable) # Obtenir les longueurs uniques des mots mots = ["chat", "chien", "oiseau", "chat", "lion"] @@ -463,7 +463,7 @@ print(pairs_uniques) # {2, 4, 6} # Voyelles présentes dans un texte texte = "Python est un excellent langage" voyelles = {c.lower() for c in texte if c.lower() in 'aeiouy'} -print(voyelles) # {'e', 'a', 'o', 'u'} +print(voyelles) # {'e', 'a', 'o', 'u', 'y'} (le 'y' de « Python » compte ; ordre variable) # Domaines uniques d'emails emails = ["alice@example.com", "bob@test.com", "charlie@example.com"] @@ -562,6 +562,31 @@ print(longueurs) --- +## Tester une collection : `any()` et `all()` + +`any()` et `all()` répondent à deux questions courantes sur une collection : +- `any(iterable)` : **au moins un** élément est-il vrai ? (comme un grand `or`) +- `all(iterable)` : **tous** les éléments sont-ils vrais ? (comme un grand `and`) + +Combinés à une expression génératrice, ils testent une condition sur toute une collection en une seule ligne : + +```python +nombres = [2, 4, 6, 8] + +print(all(n % 2 == 0 for n in nombres)) # True — tous pairs ? +print(any(n > 5 for n in nombres)) # True — au moins un > 5 ? +print(any(n < 0 for n in nombres)) # False — au moins un négatif ? + +# Valider toute une collection +notes = [12, 15, 8, 18] +print(all(0 <= note <= 20 for note in notes)) # True (toutes valides) +print(any(note < 10 for note in notes)) # True (au moins un échec) +``` + +> 💡 Sur une collection **vide**, `all([])` vaut `True` (aucun élément ne contredit la condition) et `any([])` vaut `False`. Et grâce à l'**évaluation en court-circuit**, `any`/`all` s'arrêtent dès que la réponse est connue. + +--- + ## Expressions génératrices (Generator Expressions) Les expressions génératrices ressemblent aux compréhensions de listes, mais utilisent des parenthèses `()` au lieu de crochets `[]`. Elles créent des générateurs qui produisent des valeurs à la demande, ce qui est plus efficace en mémoire. @@ -590,6 +615,16 @@ max_abs = max(abs(x) for x in nombres) print(max_abs) # 8 ``` +**⚠️ Un générateur ne se parcourt qu'une seule fois.** Une fois épuisé, il ne produit plus rien : + +```python +gen = (x**2 for x in range(5)) +print(list(gen)) # [0, 1, 4, 9, 16] +print(list(gen)) # [] — le générateur est déjà épuisé ! +``` + +Si vous devez parcourir les données plusieurs fois, conservez-les dans une liste. + **Quand utiliser les expressions génératrices ?** - Quand vous traitez de grandes quantités de données - Quand vous n'avez besoin de parcourir les éléments qu'une seule fois @@ -770,6 +805,8 @@ print(transposee) # [[1, 4], [2, 5], [3, 6]] ``` +> 💡 **Astuce idiomatique** : en combinant `zip()` et le dépaquetage `*`, on transpose une matrice en une seule ligne — `list(zip(*matrice))` — qui renvoie des tuples : `[(1, 4), (2, 5), (3, 6)]`. + --- ## Récapitulatif diff --git a/02-structures-de-donnees/03-collections-specialisees.md b/02-structures-de-donnees/03-collections-specialisees.md index 276a5ec..594b064 100644 --- a/02-structures-de-donnees/03-collections-specialisees.md +++ b/02-structures-de-donnees/03-collections-specialisees.md @@ -279,6 +279,8 @@ nom = personne_classe.nom - Vous n'avez pas besoin de méthodes personnalisées - Vous voulez économiser de la mémoire par rapport à une classe complète +> 💡 **Formes plus modernes (pour plus tard)** : une fois les classes vues (module 3), vous croiserez deux alternatives typées — `class Point(NamedTuple)` (module `typing`, avec champs annotés ; immuable comme ici) et `@dataclass` (mutable, avec méthodes). La forme fonctionnelle `namedtuple(...)` présentée ici reste tout à fait valable et n'exige pas encore de connaître les classes. + --- ## defaultdict - Dictionnaires avec Valeurs par Défaut @@ -356,6 +358,8 @@ dd_custom = defaultdict(valeur_par_defaut) print(dd_custom['cle_inexistante']) # 'N/A' ``` +> ⚠️ **Effet de bord** : accéder à une clé absente d'un `defaultdict` (même seulement pour la lire) **crée** cette clé avec la valeur par défaut. Après `print(dd_int['cle_inexistante'])`, la clé `'cle_inexistante'` existe donc désormais dans le dictionnaire (avec la valeur `0`). Pour tester l'existence d'une clé **sans la créer**, utilisez `'cle' in dd` ou `dd.get('cle')`. + ### Valeurs par défaut courantes **1. defaultdict(int) - pour compter** @@ -681,7 +685,7 @@ c1 = Counter(a=4, b=3, c=2) c2 = Counter(a=1, b=2, d=1) c1.subtract(c2) -print(c1) # Counter({'a': 3, 'b': 1, 'c': 2, 'd': -1}) +print(c1) # Counter({'a': 3, 'c': 2, 'b': 1, 'd': -1}) # Notez que les valeurs peuvent être négatives ! ``` @@ -773,7 +777,7 @@ ventes = Counter(pommes=10, bananes=5, oranges=15) # Mettre à jour le stock stock_restant = stock - ventes print("Stock restant :", stock_restant) -# Counter({'pommes': 40, 'oranges': 25, 'bananes': 25}) +# Counter({'pommes': 40, 'bananes': 25, 'oranges': 25}) # Nouvelle livraison livraison = Counter(pommes=20, bananes=15, kiwis=10) @@ -916,13 +920,15 @@ od['b'] = 2 od['a'] = 1 od['c'] = 3 -print(od) # OrderedDict([('b', 2), ('a', 1), ('c', 3)]) +print(od) # OrderedDict({'b': 2, 'a': 1, 'c': 3}) # Méthode spéciale : move_to_end od.move_to_end('a') -print(od) # OrderedDict([('b', 2), ('c', 3), ('a', 1)]) +print(od) # OrderedDict({'b': 2, 'c': 3, 'a': 1}) ``` +> 💡 Le format d'affichage d'`OrderedDict` a été **simplifié en Python 3.12**. Sur Python 3.10/3.11, le même objet s'affiche sous l'ancienne forme `OrderedDict([('b', 2), ('a', 1), ('c', 3)])` (liste de paires). + ### ChainMap - Chaîner plusieurs dictionnaires `ChainMap` groupe plusieurs dictionnaires en une seule vue. diff --git a/02-structures-de-donnees/04-chaines-et-regex.md b/02-structures-de-donnees/04-chaines-et-regex.md index d94ae3a..0929c77 100644 --- a/02-structures-de-donnees/04-chaines-et-regex.md +++ b/02-structures-de-donnees/04-chaines-et-regex.md @@ -577,6 +577,24 @@ parties = re.split(r'-', texte3, maxsplit=2) print(parties) # ['a', 'b', 'c-d-e'] ``` +### Quantificateurs gourmands et non-gourmands + +Par défaut, les quantificateurs `*`, `+`, `?` et `{n,m}` sont **gourmands** (*greedy*) : ils capturent le plus de caractères possible. En ajoutant `?` juste après, ils deviennent **non-gourmands** (*lazy*) et capturent le moins possible. + +```python +import re + +texte = " contenu " + +# Gourmand : .* va jusqu'au DERNIER '>' +print(re.findall(r'<.*>', texte)) # [' contenu '] + +# Non-gourmand : .*? s'arrête au PREMIER '>' +print(re.findall(r'<.*?>', texte)) # ['', ''] +``` + +La version non-gourmande est indispensable pour extraire des balises, des portions entre guillemets, ou tout motif délimité. + ### Groupes de capture Les parenthèses `()` créent des groupes qui peuvent être extraits séparément. @@ -712,7 +730,7 @@ print(texte_nettoye.strip()) # "Python est un langage génial!!!" # Supprimer la ponctuation excessive texte_nettoye = re.sub(r'[!?]{2,}', '.', texte_nettoye) -print(texte_nettoye) # "Python est un langage génial." +print(texte_nettoye.strip()) # "Python est un langage génial." # Supprimer tous les caractères non-alphanumériques sauf espaces texte = "Python@2024! est #1" diff --git a/02-structures-de-donnees/README.md b/02-structures-de-donnees/README.md index 6578bdb..684ba83 100644 --- a/02-structures-de-donnees/README.md +++ b/02-structures-de-donnees/README.md @@ -418,7 +418,7 @@ print(ajouter_entree("bob")) # {'bob': 1} — un nouveau dict à chaque appe Pour approfondir votre apprentissage : - **Documentation officielle Python** : [docs.python.org](https://docs.python.org/fr/3/) -- **Python Tutor** : Visualisez l'exécution de votre code étape par étape +- **[Python Tutor](https://pythontutor.com/)** : Visualisez l'exécution de votre code étape par étape - **StackOverflow** : Une mine d'or pour les questions spécifiques - **Real Python** : Tutoriels de qualité sur tous les aspects de Python diff --git a/02-structures-de-donnees/exemples/01_05_operations_listes.py b/02-structures-de-donnees/exemples/01_05_operations_listes.py index c3f04ae..6e38af9 100644 --- a/02-structures-de-donnees/exemples/01_05_operations_listes.py +++ b/02-structures-de-donnees/exemples/01_05_operations_listes.py @@ -1,6 +1,6 @@ # ============================================================================ # Section 2.1 : Les Listes - Opérations courantes -# Description : len, in, count, index, sort, sorted, reverse +# Description : len, in, count, index, sort, sorted, reverse, tri par clé (key) # Fichier source : 01-listes-tuples-dicts-sets.md # ============================================================================ @@ -36,3 +36,15 @@ # Inverser l'ordre de la liste nombres.reverse() print(nombres) # [1, 1, 2, 3, 4, 5, 6, 9] + +# --- Trier avec une clé (key) --- +print() +mots = ["python", "go", "javascript", "c"] +print(sorted(mots, key=len)) # ['c', 'go', 'python', 'javascript'] + +noms = ["alice", "Bob", "charlie", "David"] +print(sorted(noms, key=str.lower)) # ['alice', 'Bob', 'charlie', 'David'] + +personnes = [("Alice", 30), ("Bob", 25), ("Charlie", 35)] +print(sorted(personnes, key=lambda p: p[1])) # [('Bob', 25), ('Alice', 30), ('Charlie', 35)] +print(sorted(personnes, key=lambda p: p[1], reverse=True)) # [('Charlie', 35), ('Alice', 30), ('Bob', 25)] diff --git a/02-structures-de-donnees/exemples/01_06_copier_liste.py b/02-structures-de-donnees/exemples/01_06_copier_liste.py index 6569aba..0429016 100644 --- a/02-structures-de-donnees/exemples/01_06_copier_liste.py +++ b/02-structures-de-donnees/exemples/01_06_copier_liste.py @@ -1,6 +1,6 @@ # ============================================================================ # Section 2.1 : Les Listes - Copier une liste -# Description : Référence vs copie, copy(), slicing [:], list() +# Description : Référence vs copie, copy(), slicing [:], list(), deepcopy # Fichier source : 01-listes-tuples-dicts-sets.md # ============================================================================ @@ -23,3 +23,18 @@ print(liste2) # [1, 2, 3, 4] print(liste3) # [1, 2, 3] print(liste4) # [1, 2, 3] + +# --- Copie superficielle vs copie profonde --- +import copy + +# copy() est superficielle : les sous-listes restent partagées +original = [[1, 2], [3, 4]] +copie = original.copy() +copie[0].append(99) +print(original) # [[1, 2, 99], [3, 4]] - l'original est touché ! + +# deepcopy copie en profondeur (sous-objets inclus) +original = [[1, 2], [3, 4]] +copie = copy.deepcopy(original) +copie[0].append(99) +print(original) # [[1, 2], [3, 4]] - l'original est intact diff --git a/02-structures-de-donnees/exemples/01_14_modifier_dictionnaire.py b/02-structures-de-donnees/exemples/01_14_modifier_dictionnaire.py index 291b105..25e60d7 100644 --- a/02-structures-de-donnees/exemples/01_14_modifier_dictionnaire.py +++ b/02-structures-de-donnees/exemples/01_14_modifier_dictionnaire.py @@ -1,6 +1,6 @@ # ============================================================================ # Section 2.1 : Les Dictionnaires - Modifier un dictionnaire -# Description : Modifier valeurs, ajouter clés, update() +# Description : Modifier valeurs, ajouter clés, update(), fusion avec | (3.9+) # Fichier source : 01-listes-tuples-dicts-sets.md # ============================================================================ @@ -21,3 +21,13 @@ # Mettre à jour plusieurs valeurs à la fois personne.update({"age": 27, "ville": "Lyon", "telephone": "0123456789"}) print(personne) + +# --- Fusionner des dictionnaires avec | (Python 3.9+) --- +defaut = {"couleur": "noir", "taille": "M"} +choix = {"taille": "L", "motif": "rayé"} + +fusion = defaut | choix # nouveau dict ; en cas de clé commune, la droite l'emporte +print(fusion) # {'couleur': 'noir', 'taille': 'L', 'motif': 'rayé'} + +defaut |= choix # fusion en place (comme update()) +print(defaut) # {'couleur': 'noir', 'taille': 'L', 'motif': 'rayé'} diff --git a/02-structures-de-donnees/exemples/01_19_creer_set.py b/02-structures-de-donnees/exemples/01_19_creer_set.py index f9e12e7..74ff98d 100644 --- a/02-structures-de-donnees/exemples/01_19_creer_set.py +++ b/02-structures-de-donnees/exemples/01_19_creer_set.py @@ -25,9 +25,11 @@ nombres = {1, 2, 2, 3, 3, 3} print(nombres) # {1, 2, 3} -# Les éléments doivent être immuables (hashables) -valide = {1, "texte", (1, 2), True} +# Les éléments doivent être immuables (hashables) : nombres (int, float), booléens, chaînes, tuples +valide = {1, "texte", (1, 2), 3.14, True} print(f"Set valide : {sorted(str(e) for e in valide)}") +# Note : True et 1 partagent la même valeur et le même hachage → ils comptent comme UN +# seul élément (l'ensemble ci-dessus contient donc 4 éléments distincts, pas 5) # Les listes ne peuvent pas être dans un set try: diff --git a/02-structures-de-donnees/exemples/01_26_enumerate_zip.py b/02-structures-de-donnees/exemples/01_26_enumerate_zip.py new file mode 100644 index 0000000..d95d9b9 --- /dev/null +++ b/02-structures-de-donnees/exemples/01_26_enumerate_zip.py @@ -0,0 +1,34 @@ +# ============================================================================ +# Section 2.1 : Itérer avec enumerate() et zip() +# Description : enumerate (index + valeur, start=), zip (en parallèle, +# construction de dict, strict= 3.10+) +# Fichier source : 01-listes-tuples-dicts-sets.md +# ============================================================================ + +# --- enumerate() : index ET valeur --- +fruits = ["pomme", "banane", "orange"] +for index, fruit in enumerate(fruits): + print(f"{index}: {fruit}") + +# Démarrer la numérotation à 1 +print("---") +for numero, fruit in enumerate(fruits, start=1): + print(f"{numero}. {fruit}") + +# --- zip() : parcourir plusieurs collections en parallèle --- +print("---") +noms = ["Alice", "Bob", "Charlie"] +ages = [30, 25, 35] +for nom, age in zip(noms, ages): + print(f"{nom} a {age} ans") + +# Construire un dictionnaire à partir de deux listes +personnes = dict(zip(noms, ages)) +print(personnes) # {'Alice': 30, 'Bob': 25, 'Charlie': 35} + +# zip() s'arrête à la collection la plus courte ; strict=True (3.10+) exige +# des longueurs égales (sinon ValueError) +try: + list(zip([1, 2, 3], [1, 2], strict=True)) +except ValueError as e: + print(f"ValueError : {e}") diff --git a/02-structures-de-donnees/exemples/01_27_depaquetage_etoile.py b/02-structures-de-donnees/exemples/01_27_depaquetage_etoile.py new file mode 100644 index 0000000..889062c --- /dev/null +++ b/02-structures-de-donnees/exemples/01_27_depaquetage_etoile.py @@ -0,0 +1,26 @@ +# ============================================================================ +# Section 2.1 : Dépaqueter avec * et ** (passer et combiner des collections) +# Description : * pour les séquences, ** pour les dicts ; dépaquetage dans un +# appel de fonction et fusion de collections ([*a,*b], {**a,**b}) +# Fichier source : 01-listes-tuples-dicts-sets.md +# ============================================================================ + +# --- Passer une collection à une fonction --- +nombres = [3, 1, 4, 1, 5] +print(max(nombres)) # 5 (la liste = un seul argument) +print(*nombres) # 3 1 4 1 5 (chaque élément = un argument de print) + + +def afficher(a, b, c): + print(f"{a}, {b}, {c}") + + +afficher(*[10, 20, 30]) # 10, 20, 30 (* dépaquète la liste) +afficher(**{"a": 1, "b": 2, "c": 3}) # 1, 2, 3 (** dépaquète le dict) + +# --- Combiner des collections --- +tout = [*[1, 2], 3, *[4, 5]] +print(tout) # [1, 2, 3, 4, 5] + +fusion = {**{"couleur": "noir", "taille": "M"}, **{"taille": "L"}} +print(fusion) # {'couleur': 'noir', 'taille': 'L'} (droite l'emporte) diff --git a/02-structures-de-donnees/exemples/01_28_immuabilite_surface.py b/02-structures-de-donnees/exemples/01_28_immuabilite_surface.py new file mode 100644 index 0000000..5f22f22 --- /dev/null +++ b/02-structures-de-donnees/exemples/01_28_immuabilite_surface.py @@ -0,0 +1,36 @@ +# ============================================================================ +# Section 2.1 : Immuabilité « de surface » des tuples +# Description : un tuple fige ses références, pas le contenu des objets +# mutables qu'il pointe ; conséquence sur la hachabilité +# Fichier source : 01-listes-tuples-dicts-sets.md +# ============================================================================ + +# --- Un tuple contenant une liste : la liste reste modifiable --- +donnees = (1, [2, 3]) + +# Remplacer l'élément est interdit (la référence est figée) : +try: + donnees[1] = [9] +except TypeError as e: + print("Réassignation interdite :", e) + # 'tuple' object does not support item assignment + +# Mais modifier la liste pointée fonctionne, car elle est mutable : +donnees[1].append(4) +print(donnees) # (1, [2, 3, 4]) -- le contenu a changé ! + +# --- Conséquence : la hachabilité --- +# Un tuple n'est hachable que si TOUS ses éléments le sont aussi. +print(hash((1, 2, 3)) is not None) # True : que des éléments immuables + +# Dès qu'il contient une liste, il n'est plus hachable : +try: + hash((1, [2, 3])) +except TypeError as e: + print("Non hachable :", e) # unhashable type: 'list' + +# Donc impossible comme clé de dict ou élément d'un set : +try: + _ = {(1, [2, 3])} +except TypeError as e: + print("Impossible dans un set :", e) # unhashable type: 'list' diff --git a/02-structures-de-donnees/exemples/02_06_comprehension_dictionnaire.py b/02-structures-de-donnees/exemples/02_06_comprehension_dictionnaire.py index 45e195c..ecb91bd 100644 --- a/02-structures-de-donnees/exemples/02_06_comprehension_dictionnaire.py +++ b/02-structures-de-donnees/exemples/02_06_comprehension_dictionnaire.py @@ -45,7 +45,7 @@ print() # Réduction de 20% sur tous les prix prix = {"pomme": 2.5, "banane": 1.8, "orange": 3.0} -prix_soldes = {produit: prix * 0.8 for produit, prix in prix.items()} +prix_soldes = {produit: round(prix * 0.8, 2) for produit, prix in prix.items()} print(prix_soldes) # {'pomme': 2.0, 'banane': 1.44, 'orange': 2.4} # Convertir valeurs en chaînes diff --git a/02-structures-de-donnees/exemples/02_11_expressions_generatrices.py b/02-structures-de-donnees/exemples/02_11_expressions_generatrices.py index 6e4468c..7994d33 100644 --- a/02-structures-de-donnees/exemples/02_11_expressions_generatrices.py +++ b/02-structures-de-donnees/exemples/02_11_expressions_generatrices.py @@ -1,7 +1,7 @@ # ============================================================================ # Section 2.2 : Expressions génératrices (Generator Expressions) # Description : Parenthèses au lieu de crochets, utilisation avec sum/max, -# valeurs produites à la demande +# valeurs produites à la demande, épuisement (usage unique) # Fichier source : 02-comprehensions.md # ============================================================================ @@ -25,3 +25,8 @@ nombres = [-5, 2, -8, 3] max_abs = max(abs(x) for x in nombres) print(f"Max absolu : {max_abs}") # 8 + +# --- Un générateur ne se parcourt qu'une seule fois --- +gen = (x**2 for x in range(5)) +print(list(gen)) # [0, 1, 4, 9, 16] +print(list(gen)) # [] - le générateur est déjà épuisé ! diff --git a/02-structures-de-donnees/exemples/02_17_any_all.py b/02-structures-de-donnees/exemples/02_17_any_all.py new file mode 100644 index 0000000..c0ea4a6 --- /dev/null +++ b/02-structures-de-donnees/exemples/02_17_any_all.py @@ -0,0 +1,21 @@ +# ============================================================================ +# Section 2.2 : Tester une collection avec any() et all() +# Description : any (au moins un vrai), all (tous vrais), souvent avec une +# expression génératrice ; cas particulier de la collection vide +# Fichier source : 02-comprehensions.md +# ============================================================================ + +nombres = [2, 4, 6, 8] + +print(all(n % 2 == 0 for n in nombres)) # True (tous pairs ?) +print(any(n > 5 for n in nombres)) # True (au moins un > 5 ?) +print(any(n < 0 for n in nombres)) # False (au moins un négatif ?) + +# Valider toute une collection +notes = [12, 15, 8, 18] +print(all(0 <= note <= 20 for note in notes)) # True (toutes valides) +print(any(note < 10 for note in notes)) # True (au moins un échec) + +# Collection vide : all([]) -> True, any([]) -> False +print(all([])) # True +print(any([])) # False diff --git a/02-structures-de-donnees/exemples/03_01_namedtuple_base.py b/02-structures-de-donnees/exemples/03_01_namedtuple_base.py index 17ef3e0..fff1113 100644 --- a/02-structures-de-donnees/exemples/03_01_namedtuple_base.py +++ b/02-structures-de-donnees/exemples/03_01_namedtuple_base.py @@ -1,7 +1,7 @@ # ============================================================================ # Section 2.3 : namedtuple - Tuples avec des noms # Description : Créer, accéder, immuabilité, unpacking, _asdict, _replace, -# valeurs par défaut +# valeurs par défaut, comparaison tuple / namedtuple / classe # Fichier source : 03-collections-specialisees.md # ============================================================================ @@ -53,3 +53,23 @@ print(f"Défaut : {p1}") # Personne2(nom='Alice', age=25, ville='Inconnu') p2 = Personne2('Bob', 30, 'Lyon') print(f"Fourni : {p2}") # Personne2(nom='Bob', age=30, ville='Lyon') + +# --- Comparaison : tuple vs namedtuple vs classe --- +print() + +# 1. Tuple classique : accès par indice (peu lisible) +personne_tuple = ('Alice', 25, 'Paris') +print(f"Tuple : {personne_tuple[0]}") # indice « magique » + +# 2. namedtuple : accès par nom (lisible et léger) +print(f"namedtuple : {alice.nom}") + +# 3. Classe : plus de possibilités mais plus verbeux +class PersonneClasse: + def __init__(self, nom, age, ville): + self.nom = nom + self.age = age + self.ville = ville + +pc = PersonneClasse('Alice', 25, 'Paris') +print(f"Classe : {pc.nom}") diff --git a/02-structures-de-donnees/exemples/03_04_defaultdict_cas_usage.py b/02-structures-de-donnees/exemples/03_04_defaultdict_cas_usage.py index 01f0900..1307398 100644 --- a/02-structures-de-donnees/exemples/03_04_defaultdict_cas_usage.py +++ b/02-structures-de-donnees/exemples/03_04_defaultdict_cas_usage.py @@ -66,7 +66,15 @@ graphe['B'].append('C') graphe['C'].append('D') print(f"Graphe : {dict(graphe)}") -print(f"Depuis Z : {graphe['Z']}") # [] (pas d'erreur !) + + +# Parcourir le graphe : un sommet absent renvoie [] sans lever KeyError +def parcourir(graphe, depart): + print(f"Depuis {depart}, on peut aller vers : {graphe[depart]}") + + +parcourir(graphe, 'A') # Depuis A, on peut aller vers : ['B', 'C'] +parcourir(graphe, 'Z') # Depuis Z, on peut aller vers : [] (pas d'erreur !) # --- 7. Multi-niveaux --- print() @@ -83,3 +91,13 @@ print(f"{region}:") for produit, montants in sorted(produits.items()): print(f" {produit}: {sum(montants)}€ ({len(montants)} ventes)") + +# --- 8. Comptage avec filtrage par seuil --- +print() +visites = ['alice', 'bob', 'alice', 'charlie', 'alice', 'bob', 'alice'] +compteur_visites = defaultdict(int) +for utilisateur in visites: + compteur_visites[utilisateur] += 1 +# Garder les utilisateurs actifs (plus de 2 visites) +actifs = {user: count for user, count in compteur_visites.items() if count > 2} +print(f"Utilisateurs actifs : {actifs}") # {'alice': 4} diff --git a/02-structures-de-donnees/exemples/03_06_counter_cas_usage.py b/02-structures-de-donnees/exemples/03_06_counter_cas_usage.py index d3f2257..243bb48 100644 --- a/02-structures-de-donnees/exemples/03_06_counter_cas_usage.py +++ b/02-structures-de-donnees/exemples/03_06_counter_cas_usage.py @@ -17,7 +17,7 @@ print(f"Nombre total de mots : {sum(compteur_mots.values())}") print(f"Nombre de mots uniques : {len(compteur_mots)}") -print(f"Top 5 :") +print("Top 5 :") for mot, freq in compteur_mots.most_common(5): print(f" {mot}: {freq}") diff --git a/02-structures-de-donnees/exemples/04_12_formatage_fstrings.py b/02-structures-de-donnees/exemples/04_12_formatage_fstrings.py index b0b4dc6..950ae9a 100644 --- a/02-structures-de-donnees/exemples/04_12_formatage_fstrings.py +++ b/02-structures-de-donnees/exemples/04_12_formatage_fstrings.py @@ -27,7 +27,7 @@ # Largeur et alignement print(f"{'Gauche':<10}|") # Gauche | -print(f"{'Centre':^10}|") # Centre | +print(f"{'Centre':^10}|") # Centre | print(f"{'Droite':>10}|") # Droite| # Formatage avec séparateurs de milliers diff --git a/02-structures-de-donnees/exemples/04_16_regex_findall_finditer.py b/02-structures-de-donnees/exemples/04_16_regex_findall_finditer.py index c1d9a69..a5e77f8 100644 --- a/02-structures-de-donnees/exemples/04_16_regex_findall_finditer.py +++ b/02-structures-de-donnees/exemples/04_16_regex_findall_finditer.py @@ -1,6 +1,6 @@ # ============================================================================ # Section 2.4 : Regex - re.findall() et re.finditer() -# Description : Trouver toutes les occurrences, itérer sur les correspondances +# Description : Trouver toutes les occurrences, itérer ; gourmand vs non-gourmand # Fichier source : 04-chaines-et-regex.md # ============================================================================ @@ -23,3 +23,9 @@ for match in re.finditer(r'\d+', texte): print(f"Trouvé '{match.group()}' à la position {match.start()}") + +# --- Quantificateurs gourmands vs non-gourmands --- +print() +balises = " contenu " +print(re.findall(r'<.*>', balises)) # [' contenu '] (gourmand) +print(re.findall(r'<.*?>', balises)) # ['', ''] (non-gourmand) diff --git a/02-structures-de-donnees/exemples/README.md b/02-structures-de-donnees/exemples/README.md index df53e90..7683a09 100644 --- a/02-structures-de-donnees/exemples/README.md +++ b/02-structures-de-donnees/exemples/README.md @@ -1,6 +1,8 @@ # Chapitre 2 : Structures de données - Exemples -Ce dossier contient **80 fichiers** d'exemples exécutables correspondant aux 4 sections du chapitre 2. +Ce dossier contient **84 fichiers** d'exemples exécutables correspondant aux 4 sections du chapitre 2. + +**Convention de nommage** : `SS_NN_description.py`, où `SS` est le numéro de section (01 à 04) et `NN` l'ordre de l'exemple. Exemple : `03_05_counter_base.py` = section 2.3, 5ᵉ exemple. ## Exécution @@ -8,6 +10,23 @@ Ce dossier contient **80 fichiers** d'exemples exécutables correspondant aux 4 python3 nom_du_fichier.py ``` +Aucun de ces exemples n'est interactif : ils s'exécutent directement et affichent leur résultat. + +## Prérequis + +- **Python 3.10+** (le cours utilise la syntaxe moderne : `removeprefix`/`removesuffix`, f-strings avancées, etc.) +- **Aucune dépendance externe** : uniquement la bibliothèque standard (`collections`, `re`, `datetime`) + +## Correspondance avec le cours + +Chaque exemple reprend le code de son fichier `.md` source (colonne « Source »). Pour rester **exécutables et lisibles**, les `.py` adaptent parfois le cours : + +- sorties formatées avec des **f-strings** (`print(f"...")`) au lieu de `print()` bruts ; +- `sorted(...)` appliqué aux sets pour rendre l'affichage **déterministe** ; +- exemples volontairement invalides mis en commentaire ou encadrés par `try/except`. + +La **logique et les valeurs** restent identiques à celles du cours. + --- ## Section 2.1 : Listes, Tuples, Dictionnaires et Sets @@ -20,7 +39,7 @@ Source : `01-listes-tuples-dicts-sets.md` | `01_02_acces_slicing.py` | Accès par indice et slicing | Indices positifs/négatifs, slices, pas | | `01_03_modifier_liste.py` | Modifier une liste | append, insert, extend | | `01_04_supprimer_elements_liste.py` | Supprimer des éléments | remove, pop, del, clear | -| `01_05_operations_listes.py` | Opérations sur les listes | len, in, count, sort, sorted, reverse | +| `01_05_operations_listes.py` | Opérations sur les listes | len, in, count, sort/sorted (avec `key`), reverse | | `01_06_copier_liste.py` | Copier une liste | Référence vs copie, copy(), deepcopy | | `01_07_listes_imbriquees.py` | Listes imbriquées | Matrice 3x3, accès, modification | | `01_08_creer_tuple.py` | Créer des tuples | Tuples, tuple d'un élément, packing | @@ -29,7 +48,7 @@ Source : `01-listes-tuples-dicts-sets.md` | `01_11_operations_tuples.py` | Opérations sur les tuples | count, index, clés dict, retours multiples | | `01_12_creer_dictionnaire.py` | Créer des dictionnaires | {}, dict(), fromkeys, zip | | `01_13_acceder_valeurs_dict.py` | Accéder aux valeurs | [], get(), KeyError | -| `01_14_modifier_dictionnaire.py` | Modifier un dictionnaire | Ajout, modification, update() | +| `01_14_modifier_dictionnaire.py` | Modifier un dictionnaire | Ajout, modification, update(), fusion `\|` (3.9+) | | `01_15_supprimer_elements_dict.py` | Supprimer des éléments | del, pop(), clear() | | `01_16_parcourir_dictionnaire.py` | Parcourir un dictionnaire | keys(), values(), items() | | `01_17_operations_dict.py` | Opérations sur les dicts | len, in, copy, setdefault | @@ -41,6 +60,9 @@ Source : `01-listes-tuples-dicts-sets.md` | `01_23_inventaire.py` | Exemple : inventaire | Stock: pommes=35, bananes=25, oranges=25 | | `01_24_analyse_texte.py` | Exemple : analyse texte | 9 mots uniques, python et est: 2 fois | | `01_25_gestion_etudiants.py` | Exemple : gestion étudiants | Étudiants par matière, multi-inscrits | +| `01_26_enumerate_zip.py` | Itérer avec enumerate() et zip() | index+valeur (start=1), parcours parallèle, dict(zip), strict= (ValueError) | +| `01_27_depaquetage_etoile.py` | Dépaqueter avec `*` et `**` | spread dans un appel (max, print), fusion `[*a,*b]` / `{**a,**b}` | +| `01_28_immuabilite_surface.py` | Immuabilité « de surface » des tuples | la liste interne reste modifiable `(1, [2, 3, 4])` ; tuple+liste non hachable (TypeError) | --- @@ -60,12 +82,13 @@ Source : `02-comprehensions.md` | `02_08_comprehension_sets.py` | Compréhension de sets | Voyelles, longueurs uniques | | `02_09_comparaison_comprehensions.py` | Comparaison list/dict/set | Même donnée, 3 types | | `02_10_comprehensions_avancees.py` | Avancées | Matrice identité, filtrage imbriqué | -| `02_11_expressions_generatrices.py` | Expressions génératrices | Mémoire, sum, max, any, all | +| `02_11_expressions_generatrices.py` | Expressions génératrices | Mémoire, sum, max, épuisement (usage unique) | | `02_12_bonnes_pratiques_comprehensions.py` | Bonnes pratiques | Lisibilité, étapes intermédiaires | | `02_13_exemple_analyse_texte.py` | Exemple : analyse texte | Fréquence des mots | | `02_14_exemple_transformation_donnees.py` | Exemple : transformation | Salaires augmentés | | `02_15_exemple_filtrage_regroupement.py` | Exemple : filtrage | Fruits: 2.0€, Légumes: 1.9€ | | `02_16_exemple_operations_matricielles.py` | Exemple : matrices | Addition, transposition | +| `02_17_any_all.py` | Tester avec any() et all() | tous / au moins un (+ génératrice), all([])=True, any([])=False | --- @@ -75,10 +98,10 @@ Source : `03-collections-specialisees.md` | Fichier | Description | Sortie attendue | |---------|-------------|-----------------| -| `03_01_namedtuple_base.py` | namedtuple : base | Création, accès, _asdict, _replace, defaults | +| `03_01_namedtuple_base.py` | namedtuple : base | Création, accès, _asdict, _replace, defaults, comparaison tuple/classe | | `03_02_namedtuple_cas_usage.py` | namedtuple : cas d'usage | Distance 11.18, salaire moyen 51667 | | `03_03_defaultdict_base.py` | defaultdict : base | Problème KeyError, factory int/list/set/str | -| `03_04_defaultdict_cas_usage.py` | defaultdict : cas d'usage | Comptage mots, groupement, graphe | +| `03_04_defaultdict_cas_usage.py` | defaultdict : cas d'usage | Comptage mots, groupement, graphe, comptage avec seuil, multi-niveaux | | `03_05_counter_base.py` | Counter : base | most_common, elements, update, subtract, arithmétique | | `03_06_counter_cas_usage.py` | Counter : cas d'usage | Votes Alice 54.5%, inventaire, logs | | `03_07_autres_collections.py` | deque, OrderedDict, ChainMap | Rotation, move_to_end, priorité config | @@ -107,7 +130,7 @@ Source : `04-chaines-et-regex.md` | `04_13_formatage_ancien.py` | format() et % | Indices, noms, dicts, style C | | `04_14_fstrings_avances.py` | f-strings avancés | Dates, binaire/octal/hex, debug =, multi-lignes | | `04_15_regex_search_match.py` | Regex : search et match | Trouvé 2020 position 25, début de chaîne | -| `04_16_regex_findall_finditer.py` | Regex : findall et finditer | Tous les nombres, positions | +| `04_16_regex_findall_finditer.py` | Regex : findall et finditer | Tous les nombres, positions, gourmand vs non-gourmand | | `04_17_regex_sub_split.py` | Regex : sub et split | Remplacement, fonction doubler, split multi-séparateurs | | `04_18_groupes_capture.py` | Regex : groupes de capture | Email décomposé, groupes nommés, groupdict | | `04_19_compilation_flags.py` | Regex : compilation et flags | compile, IGNORECASE, MULTILINE, DOTALL | diff --git a/03-programmation-orientee-objet/01-classes-et-objets.md b/03-programmation-orientee-objet/01-classes-et-objets.md index 18e7cd8..2e683cc 100644 --- a/03-programmation-orientee-objet/01-classes-et-objets.md +++ b/03-programmation-orientee-objet/01-classes-et-objets.md @@ -76,6 +76,8 @@ print(mon_chien.nom) # Affiche : Rex print(mon_chien.age) # Affiche : 5 ``` +> 📝 **Précision** : on parle souvent de `__init__` comme du « constructeur », et c'est l'usage courant. En toute rigueur, `__init__` ne *construit* pas l'objet : il l'**initialise** (il remplit un objet déjà créé). La création proprement dite revient à une autre méthode spéciale, `__new__(cls, ...)`, qu'on **redéfinit rarement** — surtout pour sous-classer un type immuable (`int`, `str`, `tuple`…) ou implémenter un singleton. La section avancée [3.5 Métaclasses](/03-programmation-orientee-objet/05-metaclasses-et-prog-avancee.md) en montre une variante appliquée aux **métaclasses** (où `__new__` crée non pas un objet, mais une *classe*). Pour débuter, retenez simplement que `__init__` est appelée automatiquement à chaque création d'objet. + ### Comprendre `self` Le mot `self` représente **l'instance elle-même**. C'est une référence à l'objet qui est en train d'être manipulé. @@ -177,6 +179,47 @@ print(Chien.nombre_pattes) # 4 - **Attributs de classe** : partagés par tous les objets, même valeur pour tous - **Attributs d'instance** : propres à chaque objet, peuvent être différents +### ⚠️ Piège : les attributs de classe mutables + +N'utilisez **jamais** un objet *mutable* (liste, dictionnaire, set) comme attribut de classe pour stocker l'état propre à chaque instance : il serait **partagé** par toutes les instances. + +```python +class Panier: + articles = [] # ❌ attribut de CLASSE, partagé par toutes les instances ! + +p1 = Panier() +p2 = Panier() +p1.articles.append("pomme") +print(p2.articles) # ['pomme'] — p2 est affecté lui aussi ! +``` + +La bonne pratique est d'initialiser ces collections **dans `__init__`** (`self.articles = []`), comme l'attribut `self.historique = []` de l'exemple `CompteBancaire` ci-dessous. Réservez les attributs de classe aux valeurs **partagées et constantes** (comme `taux_interet` ou `espece`). + +### Lire vs écrire un attribut de classe via une instance + +Une subtilité du même mécanisme : **lire** un attribut de classe via une instance fonctionne (`chien1.espece`), mais lui **affecter** une valeur via l'instance **ne modifie pas la classe** — cela crée un nouvel attribut *d'instance* qui **masque** celui de la classe pour ce seul objet : + +```python +class Chien: + espece = "Canis familiaris" # attribut de classe + +chien1 = Chien() +chien2 = Chien() + +# Affecter via l'instance crée un attribut d'INSTANCE (la classe n'est pas touchée) +chien1.espece = "Loup" +print(chien1.espece) # Loup (attribut d'instance, masque la classe) +print(chien2.espece) # Canis familiaris (inchangé) +print(Chien.espece) # Canis familiaris (la classe est intacte) + +# Pour modifier la valeur partagée, il faut passer par la CLASSE : +Chien.espece = "Canis lupus" +print(chien2.espece) # Canis lupus (chien2 n'a pas d'attribut d'instance -> voit la classe) +print(chien1.espece) # Loup (chien1 garde son attribut d'instance) +``` + +C'est aussi ce qui éclaire le piège précédent : `liste.append(...)` **modifie en place** l'objet partagé (toutes les instances le voient), alors que `self.liste = [...]` serait une **réaffectation** qui créerait un attribut d'instance distinct. Pour savoir d'où vient une valeur, inspectez `instance.__dict__` : il ne contient **que** les attributs d'instance. + ## Exemple Complet : Classe Compte Bancaire Voici un exemple plus complet qui illustre tous les concepts : @@ -296,6 +339,44 @@ ma_voiture.kilometrage = 15000 ma_voiture.afficher_info() # Renault bleu, 15000 km ``` +## Accès Dynamique aux Attributs : `getattr`, `setattr`, `hasattr` + +Jusqu'ici, on accède aux attributs en écrivant leur nom directement : `objet.attribut`. Mais il arrive que le **nom de l'attribut ne soit connu qu'à l'exécution** (par exemple parce qu'il se trouve dans une variable). Python fournit trois fonctions intégrées pour manipuler les attributs **par leur nom donné sous forme de chaîne** : + +- `getattr(objet, "nom")` : lit l'attribut (équivaut à `objet.nom`) +- `setattr(objet, "nom", valeur)` : écrit l'attribut (équivaut à `objet.nom = valeur`) +- `hasattr(objet, "nom")` : indique si l'attribut existe (`True` / `False`) + +```python +class Personne: + def __init__(self, nom, age): + self.nom = nom + self.age = age + +p = Personne("Alice", 30) + +# Accès classique et accès dynamique donnent le même résultat +print(p.nom) # Alice +print(getattr(p, "nom")) # Alice (mais ici le nom est une chaîne) + +# Le nom de l'attribut peut venir d'une variable +champ = "age" +print(getattr(p, champ)) # 30 + +# getattr accepte une valeur par défaut si l'attribut n'existe pas (pas d'erreur) +print(getattr(p, "ville", "Inconnue")) # Inconnue + +# setattr crée ou modifie un attribut dynamiquement +setattr(p, "ville", "Paris") +print(p.ville) # Paris + +# hasattr teste l'existence d'un attribut +print(hasattr(p, "email")) # False +print(hasattr(p, "nom")) # True +``` + +> 💡 Ces fonctions sont précieuses pour écrire du code **générique** qui s'adapte à n'importe quelle classe — par exemple remplir un objet à partir d'un dictionnaire `{champ: valeur}`. On les retrouvera dans la section avancée [3.5](/03-programmation-orientee-objet/05-metaclasses-et-prog-avancee.md) (mini-ORM). Il existe aussi `delattr(objet, "nom")` pour supprimer un attribut. + ## Plusieurs Instances Indépendantes Il est important de comprendre que chaque instance est **indépendante** : @@ -510,7 +591,7 @@ print(f"\nProgression : {gestionnaire.nombre_taches_terminees()}/{gestionnaire.n ### `__init__` (Constructeur) - Méthode spéciale appelée automatiquement lors de la création d'un objet - Permet d'initialiser les attributs de l'objet -- Toujours le premier paramètre est `self` +- Le premier paramètre est toujours `self` ### `self` - Référence à l'instance courante diff --git a/03-programmation-orientee-objet/02-heritage-et-polymorphisme.md b/03-programmation-orientee-objet/02-heritage-et-polymorphisme.md index eddf288..a00a969 100644 --- a/03-programmation-orientee-objet/02-heritage-et-polymorphisme.md +++ b/03-programmation-orientee-objet/02-heritage-et-polymorphisme.md @@ -260,6 +260,8 @@ marguerite.faire_bruit() # Marguerite meugle : Meuh meuh ! Chaque classe enfant **redéfinit** la méthode `faire_bruit()` avec son propre comportement. +> 📝 **Vocabulaire : « surcharge » ou « redéfinition » ?** Le terme français exact pour ce mécanisme (*overriding* en anglais) est la **redéfinition** (ou *substitution*) : une classe enfant remplace une méthode héritée. À ne pas confondre avec la **surcharge** (*overloading*) qui désigne plusieurs méthodes de même nom mais de signatures différentes — un concept que **Python ne possède pas** : si vous définissez deux fois `faire_bruit` dans la même classe, seule la dernière définition est conservée. Pour adapter le comportement selon les arguments, on utilise plutôt les paramètres par défaut, `*args`/`**kwargs`, ou `functools.singledispatch`. Le mot « surcharge » reste très répandu dans la pratique francophone, mais gardez la distinction à l'esprit. + ## Le Polymorphisme : Concept Fondamental Le **polymorphisme** (du grec "plusieurs formes") est la capacité d'utiliser une même interface pour différents types d'objets. @@ -373,7 +375,7 @@ class Especes(MoyenPaiement): super().__init__(montant) def payer(self): - print(f"Paiement en espèces") + print("Paiement en espèces") self.afficher_recu() @@ -503,6 +505,8 @@ Je vole dans les airs ! Je nage dans l'eau ! ``` +> 📝 **Le motif *mixin*.** Des classes comme `Volant` et `Nageant`, conçues pour **ajouter des capacités** à d'autres classes sans être destinées à être instanciées seules, portent un nom : ce sont des **mixins**. C'est l'un des usages les plus sains de l'héritage multiple, très répandu dans les frameworks Python (Django, Django REST Framework…). Par convention, un mixin ne définit que quelques méthodes liées à une seule responsabilité et ne possède souvent pas de `__init__`. + ### Attention avec l'Héritage Multiple L'héritage multiple peut devenir complexe. En cas de conflit (deux classes parentes ont une méthode de même nom), Python utilise le **MRO** (Method Resolution Order) pour déterminer quelle méthode appeler. @@ -528,6 +532,24 @@ print(C.__mro__) # (, , , ) ``` +> 📝 **`super()` suit le MRO, pas « le parent direct ».** On présente souvent `super()` comme « la classe parente », ce qui est exact en héritage *simple*. En héritage *multiple*, c'est plus subtil : `super()` renvoie à la **classe suivante dans le MRO** de l'objet réel. Ainsi, le `super()` écrit dans une classe peut pointer vers une classe « sœur » que cette classe ne connaît même pas : +> +> ```python +> class A: +> def saluer(self): +> print("A") +> super().saluer() # depuis une instance de C, ce super() appelle B ! +> class B: +> def saluer(self): +> print("B") # pas de super() ici → la chaîne s'arrête +> class C(A, B): +> pass +> +> C().saluer() # Affiche : A puis B (MRO : C → A → B → object) +> ``` +> +> C'est ce mécanisme « coopératif » qui rend le MRO essentiel : chaque `super()` passe le relais au maillon suivant de la chaîne. + **Bonne pratique** : L'héritage multiple est puissant mais peut rendre le code difficile à comprendre. Utilisez-le avec parcimonie. ## Exemple Avancé : Système de Fichiers @@ -587,7 +609,7 @@ class Dossier(ElementSysteme): def afficher_info(self): super().afficher_info() - print(f"Type : Dossier") + print("Type : Dossier") print(f"Contient {len(self.contenu)} éléments") def obtenir_type(self): @@ -761,6 +783,9 @@ class Animal: def __init__(self, nom): self.nom = nom + def manger(self): + print(f"{self.nom} mange.") + # Ajouter de nouveaux types d'animaux est facile class Oiseau(Animal): def voler(self): @@ -779,8 +804,11 @@ def nourrir_animaux(liste_animaux): for animal in liste_animaux: animal.manger() # Peu importe le type exact -animaux = [Chien("Rex"), Chat("Felix"), Oiseau("Tweety")] -nourrir_animaux(animaux) +animaux = [Chien("Rex"), Chat("Felix"), Oiseau("Tweety")] +nourrir_animaux(animaux) +# Rex mange. +# Felix mange. +# Tweety mange. ``` ## Bonnes Pratiques diff --git a/03-programmation-orientee-objet/03-methodes-speciales.md b/03-programmation-orientee-objet/03-methodes-speciales.md index 21decb1..093ebc4 100644 --- a/03-programmation-orientee-objet/03-methodes-speciales.md +++ b/03-programmation-orientee-objet/03-methodes-speciales.md @@ -244,10 +244,40 @@ double = prix1 * 2 print(double) # 100.00 EUR ``` +> 📝 **Et `2 * prix1` ? Les opérateurs réfléchis.** Avec la définition ci-dessus, `prix1 * 2` fonctionne (Python appelle `prix1.__mul__(2)`), mais `2 * prix1` **échoue** : +> +> ```python +> 2 * prix1 # ❌ TypeError: unsupported operand type(s) for *: 'int' and 'Argent' +> ``` +> +> En effet, Python essaie d'abord `(2).__mul__(prix1)` ; l'entier ne sait pas multiplier un `Argent` et renvoie `NotImplemented`. Python tente alors la version **réfléchie** (*reflected*) sur l'opérande de droite : `prix1.__rmul__(2)`. Comme `__rmul__` n'est pas défini, l'opération échoue. Pour rendre l'opérateur **commutatif**, ajoutez la méthode réfléchie : +> +> ```python +> def __rmul__(self, facteur): # appelée quand l'objet est à droite : 2 * prix1 +> return self.__mul__(facteur) +> ``` +> +> Chaque opérateur binaire possède ainsi sa variante réfléchie : `__radd__`, `__rsub__`, `__rmul__`, `__rtruediv__`, etc. + ## Méthodes de Comparaison Ces méthodes permettent de comparer vos objets avec les opérateurs de comparaison. +**Pourquoi en a-t-on besoin ?** Par défaut, `==` entre deux objets compare leur **identité** (sont-ils le *même* objet en mémoire ?), pas leur contenu. Deux objets distincts ayant la même valeur sont donc considérés comme différents : + +```python +class Point: + def __init__(self, x, y): + self.x, self.y = x, y + +a = Point(1, 2) +b = Point(1, 2) +print(a == b) # False ! (a et b ont la même valeur, mais ce sont deux objets distincts) +print(a is b) # False (`is` teste l'identité : « est-ce le même objet ? ») +``` + +Définir `__eq__` permet de comparer par **valeur** plutôt que par identité. Les autres méthodes (`__lt__`, `__le__`…) font de même pour `<`, `<=`, etc. : + ```python class Personne: def __init__(self, nom, age): @@ -308,6 +338,8 @@ Alice (30 ans) Charlie (30 ans) ``` +> 💡 **`==` vs `is`** : `==` compare la **valeur** et se personnalise via `__eq__` (ci-dessus, `alice == charlie` est `True` car même âge) ; `is` compare l'**identité** (« est-ce exactement le même objet ? ») et ne se redéfinit **jamais**. Deux objets peuvent donc être `==` sans être `is` (`alice is charlie` reste `False` : ce sont deux objets distincts). C'est précisément ce que garantit le motif *singleton* de la [section 3.5](/03-programmation-orientee-objet/05-metaclasses-et-prog-avancee.md), où `config1 is config2` assure une instance **unique**. + ### Tableau des Opérateurs de Comparaison | Opérateur | Méthode | Exemple | @@ -319,7 +351,37 @@ Charlie (30 ans) | `>` | `__gt__(self, other)` | `a > b` | | `>=` | `__ge__(self, other)` | `a >= b` | -**Astuce** : Python peut déduire certaines comparaisons. Si vous définissez `__eq__` et `__lt__`, Python peut souvent déduire les autres. Vous pouvez utiliser le décorateur `@functools.total_ordering` pour cela. +**Astuce — générer les comparaisons avec `@functools.total_ordering`** : écrire les six méthodes à la main est fastidieux et source d'incohérences. Le décorateur `@functools.total_ordering` génère automatiquement `__le__`, `__gt__` et `__ge__` à partir des **deux seules** méthodes `__eq__` et `__lt__` : + +```python +from functools import total_ordering + +@total_ordering +class Temperature: + def __init__(self, degres): + self.degres = degres + + def __eq__(self, autre): + return self.degres == autre.degres + + def __lt__(self, autre): + return self.degres < autre.degres + +t1 = Temperature(20) +t2 = Temperature(25) + +# Avec seulement __eq__ et __lt__, les six comparaisons fonctionnent : +print(t1 < t2) # True +print(t1 <= t2) # True (généré par total_ordering) +print(t1 > t2) # False (généré) +print(t1 >= t2) # False (généré) +print(t1 == t2) # False +print(t1 != t2) # True (déduit de __eq__) +``` + +(`__ne__` est, lui, toujours déduit automatiquement de `__eq__` depuis Python 3.) Ce décorateur est aussi présenté au chapitre [7 (module `functools`)](/07-bibliotheques-standard/04-itertools-et-functools.md). + +> ⚠️ **`__eq__` et `__hash__` vont de pair.** Dès que vous définissez `__eq__`, Python met `__hash__` à `None` : vos instances deviennent **non hashables** (impossible de les utiliser comme clés de dictionnaire ou de les mettre dans un `set`). Si vous en avez besoin, définissez aussi `__hash__` à partir des **mêmes** attributs que `__eq__` (deux objets égaux doivent avoir le même hash), par exemple `def __hash__(self): return hash((self.nom, self.age))`. ## `__len__` : Longueur d'un Objet @@ -1041,7 +1103,38 @@ class Fraction: # ... suite du code ``` -### 5. Maintenir la Cohérence +### 5. Retourner `NotImplemented` pour les Types Incompatibles + +Les exemples de ce chapitre supposent, pour rester lisibles, que l'autre opérande est du même type. En réalité, comparer ou additionner avec un type imprévu provoque une erreur peu claire : + +```python +class Vecteur: + def __init__(self, x, y): + self.x, self.y = x, y + def __eq__(self, autre): + return self.x == autre.x and self.y == autre.y + +Vecteur(3, 4) == 5 # ❌ AttributeError: 'int' object has no attribute 'x' +``` + +La bonne pratique consiste à retourner la valeur spéciale **`NotImplemented`** quand on ne sait pas traiter l'autre opérande. Python se charge alors de la suite : il renvoie `False` pour `==`, ou lève un `TypeError` clair pour `<`, `+`, etc. + +```python +class Vecteur: + def __init__(self, x, y): + self.x, self.y = x, y + def __eq__(self, autre): + if not isinstance(autre, Vecteur): + return NotImplemented # ✅ on laisse Python décider + return self.x == autre.x and self.y == autre.y + +print(Vecteur(3, 4) == 5) # False (au lieu de planter) +print(Vecteur(3, 4) == Vecteur(3, 4)) # True +``` + +> 💡 Ne confondez pas `NotImplemented` (une **valeur** à *retourner*) avec l'exception `NotImplementedError` (que l'on *lève*, par exemple dans une méthode abstraite). + +### 6. Maintenir la Cohérence Depuis Python 3, `__ne__` est automatiquement déduit de `__eq__` (il n'est plus nécessaire de le définir manuellement). Si vous définissez `__eq__` et `__lt__`, utilisez `@functools.total_ordering` pour générer les autres comparaisons. diff --git a/03-programmation-orientee-objet/04-proprietes-et-decorateurs.md b/03-programmation-orientee-objet/04-proprietes-et-decorateurs.md index e14a529..13c5d84 100644 --- a/03-programmation-orientee-objet/04-proprietes-et-decorateurs.md +++ b/03-programmation-orientee-objet/04-proprietes-et-decorateurs.md @@ -89,6 +89,25 @@ print(compte.solde) # 1500 **Magie** : On utilise la syntaxe simple `compte.solde`, mais Python appelle automatiquement les bonnes méthodes en arrière-plan ! +### Encapsulation : les conventions `_` et `__` + +Vous avez remarqué le `_` devant `self._solde`. Contrairement à Java ou C++, **Python n'a pas de mot-clé `private`** : tout attribut reste techniquement accessible de l'extérieur. L'encapsulation repose donc sur deux conventions : + +- **Un underscore `_attribut`** : signale « usage interne, ne pas toucher de l'extérieur ». C'est une simple **convention**, non imposée — `compte._solde` fonctionne quand même. C'est ce niveau qu'on utilise avec les propriétés (`_solde` est la zone de stockage, `solde` l'accès public contrôlé). +- **Deux underscores `__attribut`** : déclenche le *name mangling*. Python renomme en coulisses `__attribut` en `_NomDeClasse__attribut` : + +```python +class CompteBancaire: + def __init__(self, solde): + self.__solde = solde # devient _CompteBancaire__solde + +compte = CompteBancaire(1000) +# print(compte.__solde) # ❌ AttributeError +print(compte._CompteBancaire__solde) # 1000 — accessible via le nom transformé +``` + +Le but du *mangling* n'est **pas** la sécurité (on l'a contourné ci-dessus), mais d'**éviter les collisions de noms en héritage** : un attribut `__x` défini dans une classe parente et un autre `__x` défini dans une sous-classe ne s'écrasent pas, car ils sont renommés différemment. En pratique, **préférez le simple `_`** (plus lisible et pythonique) ; réservez `__` aux rares cas où vous devez protéger un attribut contre une redéfinition accidentelle dans les sous-classes. + ## Le Décorateur `@property` ### Qu'est-ce qu'un Décorateur ? @@ -138,6 +157,35 @@ print(f"Surface : {cercle.surface}") # 78.53975 - Les valeurs sont calculées à la demande (pas stockées inutilement) - Impossible de modifier `diametre`, `circonference` ou `surface` directement +### `@cached_property` : Calculer une Seule Fois + +Un `@property` **recalcule** sa valeur à *chaque* accès. C'est parfait quand le résultat peut changer, mais inutilement coûteux quand le calcul est lourd et que les données ne bougent pas. Le décorateur `functools.cached_property` (disponible depuis Python 3.8) calcule la valeur **au premier accès**, puis la **mémorise** sur l'instance : + +```python +from functools import cached_property + +class AnalyseTexte: + def __init__(self, texte): + self.texte = texte + + @cached_property + def nb_mots_uniques(self): + print("Calcul coûteux en cours...") # pour voir QUAND le calcul a lieu + return len(set(self.texte.lower().split())) + +analyse = AnalyseTexte("le chat et le chien et le chat") +print(analyse.nb_mots_uniques) # Calcul coûteux en cours... puis 4 +print(analyse.nb_mots_uniques) # 4 → AUCUN recalcul, valeur en cache +``` + +La valeur est stockée dans `instance.__dict__` sous le nom de la propriété. Points à connaître : + +- **C'est un cache, pas un calcul à la demande** : si les données sources changent (`analyse.texte = ...`), la valeur en cache **ne se met pas à jour** automatiquement. Réservez `cached_property` aux objets dont l'état pertinent ne change plus. +- **Invalider le cache** : `del analyse.nb_mots_uniques` force un nouveau calcul au prochain accès. +- **Pas de setter** : contrairement à `@property`, `cached_property` ne se combine pas avec `.setter`/`.deleter`. +- **Incompatible avec `__slots__`** (sans `__dict__`) : la mise en cache a besoin du `__dict__` de l'instance, sinon Python lève un `TypeError`. +- **Non thread-safe** par défaut (le verrou interne a été retiré en Python 3.12) : dans un contexte multi-thread, le calcul peut s'exécuter plusieurs fois. + ## Le Trio : `@property`, `@setter`, `@deleter` ### `@property` : Getter (Lecture) @@ -363,6 +411,8 @@ personne.age = 30 # OK # personne.email = "invalide" # ValueError ! ``` +> 💡 **Initialisez via la propriété, pas via l'attribut interne.** Dans `__init__`, on écrit `self.nom = nom` (et non `self._nom = nom`) : l'affectation passe alors par le **setter**, donc la validation et le formatage s'appliquent **dès la création** de l'objet. C'est pourquoi `Personne(" dupont ", …)` produit directement `nom = "DUPONT"`. Écrire `self._nom = nom` court-circuiterait le setter (aucune validation) — à réserver aux rares cas où l'on veut délibérément éviter ce contrôle. + ## Les Décorateurs : Concepts Fondamentaux ### Qu'est-ce qu'un Décorateur ? @@ -505,6 +555,18 @@ print() print(f"fibonacci(5) = {fibonacci(5)}") ``` +> 💡 **En pratique, ne réimplémentez pas ce cache.** La bibliothèque standard fournit `functools.lru_cache` (et son alias `functools.cache` sans limite de taille, depuis Python 3.9) qui fait exactement cela, de façon optimisée : +> +> ```python +> from functools import cache +> +> @cache # = lru_cache(maxsize=None) : mémorise tous les appels +> def fibonacci(n): +> return n if n <= 1 else fibonacci(n - 1) + fibonacci(n - 2) +> ``` +> +> Préférez `@lru_cache(maxsize=128)` pour **borner** la mémoire (les entrées les moins récemment utilisées sont alors évincées). Le cache maison ci-dessus ne sert qu'à *comprendre le mécanisme* ; ces outils sont approfondis aux chapitres [5 (décorateurs avancés)](/05-programmation-fonctionnelle/03-decorateurs-avances.md) et [7 (module `functools`)](/07-bibliotheques-standard/04-itertools-et-functools.md). + ### 2. Décorateur de Validation ```python @@ -915,7 +977,7 @@ class Utilisateur: # Utilisation user1 = Utilisateur("Alice Dupont", "alice@example.com", datetime(1995, 5, 15)) -print(user1) # Alice Dupont (29/30 ans) - alice@example.com +print(user1) # Alice Dupont (âge calculé selon l'année courante) - alice@example.com print(f"Majeur : {user1.est_majeur}") # True # Factory method diff --git a/03-programmation-orientee-objet/05-metaclasses-et-prog-avancee.md b/03-programmation-orientee-objet/05-metaclasses-et-prog-avancee.md index 78cc8fd..c42cff1 100644 --- a/03-programmation-orientee-objet/05-metaclasses-et-prog-avancee.md +++ b/03-programmation-orientee-objet/05-metaclasses-et-prog-avancee.md @@ -904,7 +904,8 @@ class Version: patch: int = 0 versions = [Version(2, 0), Version(1, 9, 1), Version(1, 9)] -print(sorted(versions)) # [Version(1, 9, 0), Version(1, 9, 1), Version(2, 0, 0)] +print(sorted(versions)) +# [Version(majeure=1, mineure=9, patch=0), Version(majeure=1, mineure=9, patch=1), Version(majeure=2, mineure=0, patch=0)] # Tous les paramètres disponibles : # @dataclass(init=True, repr=True, eq=True, order=False, frozen=False, slots=False) @@ -974,7 +975,22 @@ rex = Chien("Rex", 5, "Berger Allemand", dresse=True) print(rex) # Chien(nom='Rex', age=5, race='Berger Allemand', dresse=True) ``` -> ⚠️ **Attention** : dans l'héritage de dataclasses, les champs avec valeur par défaut de la classe parente empêchent d'ajouter des champs *sans* valeur par défaut dans la classe enfant (un champ sans défaut ne peut pas suivre un champ avec défaut dans `__init__`). +> ⚠️ **Attention** : dans l'héritage de dataclasses, les champs avec valeur par défaut de la classe parente empêchent d'ajouter des champs *sans* valeur par défaut dans la classe enfant (un champ sans défaut ne peut pas suivre un champ avec défaut dans `__init__`). On obtient alors `TypeError: non-default argument ... follows default argument`. +> +> **Solution moderne (Python 3.10+)** : passez `kw_only=True` au décorateur. Tous les champs deviennent **passables uniquement par mot-clé**, ce qui supprime la contrainte d'ordre : +> +> ```python +> @dataclass(kw_only=True) +> class Animal: +> nom: str +> actif: bool = True +> +> @dataclass(kw_only=True) +> class Chien(Animal): +> race: str # ✅ plus d'erreur, même sans valeur par défaut +> +> Chien(nom="Rex", race="Berger") # les arguments doivent être nommés +> ``` ### Dataclass vs Alternatives @@ -1062,6 +1078,8 @@ print(f"Taille avec slots : {sys.getsizeof(obj2)} bytes") - Moins flexible (pas de __dict__) - Ne peut pas ajouter d'attributs dynamiquement +> ⚠️ **`__slots__` et héritage** : le `__dict__` n'est éliminé que si **toute la hiérarchie** déclare `__slots__`. Si une sous-classe **omet** `__slots__`, ses instances retrouvent un `__dict__` et peuvent de nouveau recevoir n'importe quel attribut — le gain mémoire est alors perdu. Pour le conserver, **chaque** classe de la chaîne doit déclarer son propre `__slots__`, en n'y mettant que les **nouveaux** attributs (sans répéter ceux déjà déclarés dans les classes parentes). + ## Protocoles et Duck Typing ### Duck Typing diff --git a/03-programmation-orientee-objet/README.md b/03-programmation-orientee-objet/README.md index b63c475..f5620e8 100644 --- a/03-programmation-orientee-objet/README.md +++ b/03-programmation-orientee-objet/README.md @@ -395,7 +395,7 @@ Ce changement de perspective prend du temps, mais une fois que vous l'aurez maî Pendant votre apprentissage de ce chapitre, vous pouvez consulter : ### Documentation Officielle Python -- La documentation officielle sur les classes : https://docs.python.org/fr/3/tutorial/classes.html +- [La documentation officielle sur les classes](https://docs.python.org/fr/3/tutorial/classes.html) ### Pratique Interactive - Essayez les exemples dans un notebook Jupyter ou dans l'interpréteur Python interactif diff --git a/03-programmation-orientee-objet/exemples/01_03_attributs_classe.py b/03-programmation-orientee-objet/exemples/01_03_attributs_classe.py index d7302b8..63041eb 100644 --- a/03-programmation-orientee-objet/exemples/01_03_attributs_classe.py +++ b/03-programmation-orientee-objet/exemples/01_03_attributs_classe.py @@ -1,7 +1,7 @@ # ============================================================================ # Section 3.1 : Attributs de classe # Description : Attributs partagés par toutes les instances vs attributs -# d'instance propres à chaque objet +# d'instance propres à chaque objet ; piège des mutables # Fichier source : 01-classes-et-objets.md # ============================================================================ @@ -22,3 +22,27 @@ def __init__(self, nom, age): print(chien2.espece) # Canis familiaris print(Chien.espece) # Canis familiaris print(Chien.nombre_pattes) # 4 + +# --- ⚠️ Piège : un attribut de classe MUTABLE est partagé par toutes les instances --- +print() + +class Panier: + articles = [] # ❌ attribut de CLASSE, partagé par toutes les instances ! + +p1 = Panier() +p2 = Panier() +p1.articles.append("pomme") +print(p2.articles) # ['pomme'] — p2 est affecté lui aussi ! + +# --- ✅ La bonne pratique : initialiser la collection dans __init__ --- +print() + +class PanierCorrect: + def __init__(self): + self.articles = [] # propre à CHAQUE instance + +p3 = PanierCorrect() +p4 = PanierCorrect() +p3.articles.append("pomme") +print(p3.articles) # ['pomme'] +print(p4.articles) # [] — indépendant diff --git a/03-programmation-orientee-objet/exemples/01_09_gestionnaire_taches.py b/03-programmation-orientee-objet/exemples/01_09_gestionnaire_taches.py index 0716acf..ef74291 100644 --- a/03-programmation-orientee-objet/exemples/01_09_gestionnaire_taches.py +++ b/03-programmation-orientee-objet/exemples/01_09_gestionnaire_taches.py @@ -15,14 +15,14 @@ def __init__(self, titre, description=""): def marquer_terminee(self): self.terminee = True - print(f"Tâche '{self.titre}' marquée comme terminée.") + print(f"✓ Tâche '{self.titre}' marquée comme terminée.") def marquer_non_terminee(self): self.terminee = False - print(f"Tâche '{self.titre}' marquée comme non terminée.") + print(f"○ Tâche '{self.titre}' marquée comme non terminée.") def afficher(self): - statut = "[x]" if self.terminee else "[ ]" + statut = "✓" if self.terminee else "○" print(f"{statut} {self.titre}") if self.description: print(f" Description : {self.description}") @@ -52,7 +52,7 @@ def afficher_toutes(self): def afficher_non_terminees(self): taches_non_terminees = [t for t in self.taches if not t.terminee] if not taches_non_terminees: - print("Toutes les tâches sont terminées !") + print("Toutes les tâches sont terminées ! 🎉") return print("\n=== Tâches à faire ===") diff --git a/03-programmation-orientee-objet/exemples/01_10_acces_dynamique.py b/03-programmation-orientee-objet/exemples/01_10_acces_dynamique.py new file mode 100644 index 0000000..cf54eaa --- /dev/null +++ b/03-programmation-orientee-objet/exemples/01_10_acces_dynamique.py @@ -0,0 +1,50 @@ +# ============================================================================ +# Section 3.1 : Accès dynamique aux attributs (getattr, setattr, hasattr) +# Description : Lire/écrire/tester un attribut par son nom (chaîne) ; +# valeur par défaut, delattr, remplir un objet depuis un dict +# Fichier source : 01-classes-et-objets.md +# ============================================================================ + +class Personne: + def __init__(self, nom, age): + self.nom = nom + self.age = age + + +p = Personne("Alice", 30) + +# Accès classique et accès dynamique donnent le même résultat +print(p.nom) # Alice +print(getattr(p, "nom")) # Alice (mais ici le nom est une chaîne) + +# Le nom de l'attribut peut venir d'une variable +champ = "age" +print(getattr(p, champ)) # 30 + +# getattr accepte une valeur par défaut si l'attribut n'existe pas +print(getattr(p, "ville", "Inconnue")) # Inconnue + +# Sans valeur par défaut, accéder à un attribut absent lève une erreur +try: + print(p.ville) +except AttributeError as e: + print(f"AttributeError : {e}") + +# setattr crée ou modifie un attribut dynamiquement +setattr(p, "ville", "Paris") +print(p.ville) # Paris + +# hasattr teste l'existence ; delattr supprime +print(hasattr(p, "email")) # False +delattr(p, "ville") +print(hasattr(p, "ville")) # False + +# Cas d'usage : remplir un objet à partir d'un dictionnaire {champ: valeur} +class Config: + pass + +donnees = {"hote": "localhost", "port": 8000, "debug": True} +config = Config() +for cle, valeur in donnees.items(): + setattr(config, cle, valeur) +print(f"{config.hote}:{config.port} (debug={config.debug})") # localhost:8000 (debug=True) diff --git a/03-programmation-orientee-objet/exemples/01_11_shadowing_attribut_classe.py b/03-programmation-orientee-objet/exemples/01_11_shadowing_attribut_classe.py new file mode 100644 index 0000000..34315ae --- /dev/null +++ b/03-programmation-orientee-objet/exemples/01_11_shadowing_attribut_classe.py @@ -0,0 +1,28 @@ +# ============================================================================ +# Section 3.1 : Lire vs écrire un attribut de classe via une instance +# Description : affecter un attribut de classe via l'instance crée un attribut +# d'instance qui MASQUE celui de la classe (shadowing) +# Fichier source : 01-classes-et-objets.md +# ============================================================================ + +class Chien: + espece = "Canis familiaris" # attribut de classe + + +chien1 = Chien() +chien2 = Chien() + +# Affecter via l'instance crée un attribut d'INSTANCE (la classe n'est pas touchée) +chien1.espece = "Loup" +print(chien1.espece) # Loup (attribut d'instance, masque la classe) +print(chien2.espece) # Canis familiaris (inchangé) +print(Chien.espece) # Canis familiaris (la classe est intacte) + +# Pour modifier la valeur partagée, il faut passer par la CLASSE : +Chien.espece = "Canis lupus" +print(chien2.espece) # Canis lupus (chien2 voit la classe) +print(chien1.espece) # Loup (chien1 garde son attribut d'instance) + +# instance.__dict__ ne contient QUE les attributs d'instance +print(chien1.__dict__) # {'espece': 'Loup'} +print(chien2.__dict__) # {} diff --git a/03-programmation-orientee-objet/exemples/02_06_systeme_paiement.py b/03-programmation-orientee-objet/exemples/02_06_systeme_paiement.py index e41feb4..410a5a1 100644 --- a/03-programmation-orientee-objet/exemples/02_06_systeme_paiement.py +++ b/03-programmation-orientee-objet/exemples/02_06_systeme_paiement.py @@ -40,7 +40,7 @@ def payer(self): class Especes(MoyenPaiement): def payer(self): - print(f"Paiement en espèces") + print("Paiement en espèces") self.afficher_recu() diff --git a/03-programmation-orientee-objet/exemples/02_08_heritage_multiple.py b/03-programmation-orientee-objet/exemples/02_08_heritage_multiple.py index b385874..fca525f 100644 --- a/03-programmation-orientee-objet/exemples/02_08_heritage_multiple.py +++ b/03-programmation-orientee-objet/exemples/02_08_heritage_multiple.py @@ -47,3 +47,20 @@ class C(A, B): # Voir l'ordre de résolution des méthodes print(C.__mro__) + +# --- super() suit le MRO, pas "le parent direct" --- +print() + +class Salut_A: + def saluer(self): + print("A") + super().saluer() # depuis une instance de Salut_C, appelle Salut_B ! + +class Salut_B: + def saluer(self): + print("B") # pas de super() ici -> la chaîne s'arrête + +class Salut_C(Salut_A, Salut_B): + pass + +Salut_C().saluer() # Affiche : A puis B (MRO : C -> A -> B -> object) diff --git a/03-programmation-orientee-objet/exemples/02_09_systeme_fichiers.py b/03-programmation-orientee-objet/exemples/02_09_systeme_fichiers.py index 18dff1d..3540843 100644 --- a/03-programmation-orientee-objet/exemples/02_09_systeme_fichiers.py +++ b/03-programmation-orientee-objet/exemples/02_09_systeme_fichiers.py @@ -55,7 +55,7 @@ def ajouter(self, element): def afficher_info(self): super().afficher_info() - print(f"Type : Dossier") + print("Type : Dossier") print(f"Contient {len(self.contenu)} éléments") def obtenir_type(self): diff --git a/03-programmation-orientee-objet/exemples/03_03_classe_argent.py b/03-programmation-orientee-objet/exemples/03_03_classe_argent.py index 951b650..d7bfe8e 100644 --- a/03-programmation-orientee-objet/exemples/03_03_classe_argent.py +++ b/03-programmation-orientee-objet/exemples/03_03_classe_argent.py @@ -1,7 +1,7 @@ # ============================================================================ # Section 3.3 : Classe Argent -# Description : Exemple pratique avec addition, soustraction, multiplication -# et vérification de devise +# Description : Exemple pratique avec addition, soustraction, multiplication, +# vérification de devise et opérateur réfléchi (__rmul__) # Fichier source : 03-methodes-speciales.md # ============================================================================ @@ -23,6 +23,11 @@ def __sub__(self, autre): def __mul__(self, facteur): return Argent(self.montant * facteur, self.devise) + def __rmul__(self, facteur): + # Appelée quand l'objet est à DROITE : 2 * prix1 + # (sans elle, 2 * prix1 lèverait TypeError) + return self.__mul__(facteur) + def __str__(self): return f"{self.montant:.2f} {self.devise}" @@ -40,3 +45,6 @@ def __repr__(self): double = prix1 * 2 print(double) # 100.00 EUR + +# --- Opérateur réfléchi : l'objet à droite du * --- +print(2 * prix1) # 100.00 EUR (grâce à __rmul__) diff --git a/03-programmation-orientee-objet/exemples/03_04_comparaisons.py b/03-programmation-orientee-objet/exemples/03_04_comparaisons.py index a2f884d..6440580 100644 --- a/03-programmation-orientee-objet/exemples/03_04_comparaisons.py +++ b/03-programmation-orientee-objet/exemples/03_04_comparaisons.py @@ -45,3 +45,26 @@ def __str__(self): personnes_triees = sorted(personnes) # Trie par âge grâce à __lt__ for p in personnes_triees: print(p) + +# --- ⚠️ Piège : définir __eq__ rend les instances NON hashables --- +print() +try: + ensemble = {alice, bob} # impossible : Personne n'a plus de __hash__ +except TypeError as e: + print(f"TypeError : {e}") + +# --- ✅ Pour rester hashable, définir __hash__ sur les MÊMES attributs que __eq__ --- +class PersonneHashable: + def __init__(self, nom, age): + self.nom = nom + self.age = age + + def __eq__(self, autre): + return self.age == autre.age + + def __hash__(self): + return hash(self.age) # cohérent avec __eq__ (qui compare l'âge) + +p1 = PersonneHashable("Alice", 30) +p2 = PersonneHashable("Bob", 25) +print(len({p1, p2})) # 2 — utilisable dans un set diff --git a/03-programmation-orientee-objet/exemples/03_11_total_ordering.py b/03-programmation-orientee-objet/exemples/03_11_total_ordering.py new file mode 100644 index 0000000..03f415e --- /dev/null +++ b/03-programmation-orientee-objet/exemples/03_11_total_ordering.py @@ -0,0 +1,41 @@ +# ============================================================================ +# Section 3.3 : Générer les comparaisons avec @functools.total_ordering +# Description : à partir de __eq__ et __lt__ seulement, le décorateur génère +# __le__, __gt__, __ge__ (et __ne__ vient de __eq__) +# Fichier source : 03-methodes-speciales.md +# ============================================================================ + +from functools import total_ordering + + +@total_ordering +class Temperature: + def __init__(self, degres): + self.degres = degres + + def __eq__(self, autre): + return self.degres == autre.degres + + def __lt__(self, autre): + return self.degres < autre.degres + + def __repr__(self): + return f"Temperature({self.degres})" + + +t1 = Temperature(20) +t2 = Temperature(25) + +# Avec seulement __eq__ et __lt__, les six comparaisons fonctionnent : +print(t1 < t2) # True +print(t1 <= t2) # True (généré par total_ordering) +print(t1 > t2) # False (généré) +print(t1 >= t2) # False (généré) +print(t1 == t2) # False +print(t1 != t2) # True (déduit de __eq__) + +# Conséquence pratique : tri et min/max fonctionnent aussi +temperatures = [Temperature(30), Temperature(15), Temperature(22)] +print(sorted(temperatures)) # [Temperature(15), Temperature(22), Temperature(30)] +print(min(temperatures)) # Temperature(15) +print(max(temperatures)) # Temperature(30) diff --git a/03-programmation-orientee-objet/exemples/03_12_egalite_vs_identite.py b/03-programmation-orientee-objet/exemples/03_12_egalite_vs_identite.py new file mode 100644 index 0000000..dcc8c50 --- /dev/null +++ b/03-programmation-orientee-objet/exemples/03_12_egalite_vs_identite.py @@ -0,0 +1,39 @@ +# ============================================================================ +# Section 3.3 : Égalité (==) vs identité (is) pour les objets +# Description : par défaut == compare l'IDENTITÉ (même objet) ; __eq__ permet +# de comparer la VALEUR. is teste toujours l'identité. +# Fichier source : 03-methodes-speciales.md +# ============================================================================ + +# --- Sans __eq__ : == compare l'IDENTITÉ (comme is) --- +class Point: + def __init__(self, x, y): + self.x, self.y = x, y + + +a = Point(1, 2) +b = Point(1, 2) # même valeur, mais objet distinct +c = a # même objet que a + +print(a == b) # False (deux objets distincts, même si même valeur) +print(a is b) # False (is : est-ce le même objet ?) +print(a == c) # True (c est le même objet que a) +print(a is c) # True + + +# --- Avec __eq__ : == compare la VALEUR --- +class PointEq: + def __init__(self, x, y): + self.x, self.y = x, y + + def __eq__(self, autre): + if not isinstance(autre, PointEq): + return NotImplemented + return (self.x, self.y) == (autre.x, autre.y) + + +d = PointEq(1, 2) +e = PointEq(1, 2) + +print(d == e) # True (même valeur) +print(d is e) # False (toujours deux objets distincts en mémoire) diff --git a/03-programmation-orientee-objet/exemples/04_16_encapsulation.py b/03-programmation-orientee-objet/exemples/04_16_encapsulation.py new file mode 100644 index 0000000..05da3e5 --- /dev/null +++ b/03-programmation-orientee-objet/exemples/04_16_encapsulation.py @@ -0,0 +1,52 @@ +# ============================================================================ +# Section 3.4 : Encapsulation - les conventions _ et __ +# Description : Python n'a pas de "private" ; conventions _attribut (usage +# interne) et __attribut (name mangling, anti-collision en héritage) +# Fichier source : 04-proprietes-et-decorateurs.md +# ============================================================================ + +# --- Un underscore : convention "usage interne" (NON imposée) --- +class CompteBancaire: + def __init__(self, solde): + self._solde = solde # convention : "ne pas toucher de l'extérieur" + +compte = CompteBancaire(1000) +print(compte._solde) # 1000 — accessible : ce n'est qu'une convention + +# --- Deux underscores : name mangling --- +class CompteSecurise: + def __init__(self, solde): + self.__solde = solde # devient _CompteSecurise__solde + +compte2 = CompteSecurise(1000) + +# Accès direct impossible +try: + print(compte2.__solde) +except AttributeError as e: + print(f"AttributeError : {e}") + +# ... mais le nom est juste transformé : +print(compte2._CompteSecurise__solde) # 1000 +print(compte2.__dict__) # {'_CompteSecurise__solde': 1000} + +# --- Intérêt réel du mangling : éviter les collisions en héritage --- +print() + +class Base: + def __init__(self): + self.__valeur = "base" # -> _Base__valeur + def get_base(self): + return self.__valeur + +class Derivee(Base): + def __init__(self): + super().__init__() + self.__valeur = "derivee" # -> _Derivee__valeur (PAS de collision) + def get_derivee(self): + return self.__valeur + +d = Derivee() +print(f"get_base() : {d.get_base()}") # base (non écrasé) +print(f"get_derivee() : {d.get_derivee()}") # derivee +print(f"__dict__ : {d.__dict__}") # les deux attributs coexistent diff --git a/03-programmation-orientee-objet/exemples/04_17_cached_property.py b/03-programmation-orientee-objet/exemples/04_17_cached_property.py new file mode 100644 index 0000000..9de26ac --- /dev/null +++ b/03-programmation-orientee-objet/exemples/04_17_cached_property.py @@ -0,0 +1,32 @@ +# ============================================================================ +# Section 3.4 : @cached_property - calculer une seule fois +# Description : Mémorise le résultat au premier accès (functools, 3.8+), +# invalidation par del, différence avec @property +# Fichier source : 04-proprietes-et-decorateurs.md +# ============================================================================ + +from functools import cached_property + +class AnalyseTexte: + def __init__(self, texte): + self.texte = texte + + @cached_property + def nb_mots_uniques(self): + print("Calcul coûteux en cours...") # pour voir QUAND le calcul a lieu + return len(set(self.texte.lower().split())) + +analyse = AnalyseTexte("le chat et le chien et le chat") + +# Premier accès : le calcul a lieu +print(analyse.nb_mots_uniques) # Calcul coûteux en cours... puis 4 +# Accès suivants : valeur en cache, AUCUN recalcul +print(analyse.nb_mots_uniques) # 4 + +# La valeur est stockée dans l'instance +print("'nb_mots_uniques' in __dict__ :", 'nb_mots_uniques' in analyse.__dict__) + +# --- Invalider le cache : del force un nouveau calcul --- +print() +del analyse.nb_mots_uniques +print(analyse.nb_mots_uniques) # Calcul coûteux en cours... puis 4 (recalcul) diff --git a/03-programmation-orientee-objet/exemples/04_18_lru_cache.py b/03-programmation-orientee-objet/exemples/04_18_lru_cache.py new file mode 100644 index 0000000..5b03da3 --- /dev/null +++ b/03-programmation-orientee-objet/exemples/04_18_lru_cache.py @@ -0,0 +1,38 @@ +# ============================================================================ +# Section 3.4 : Cache standard avec functools (cache / lru_cache) +# Description : Équivalent standard du décorateur de cache maison ; +# functools.cache (3.9+) et lru_cache(maxsize=...) + cache_info +# Fichier source : 04-proprietes-et-decorateurs.md +# ============================================================================ + +from functools import cache, lru_cache + + +# --- functools.cache : mémorise TOUS les appels (= lru_cache(maxsize=None)) --- +@cache +def fibonacci(n): + return n if n <= 1 else fibonacci(n - 1) + fibonacci(n - 2) + + +print(f"fibonacci(30) = {fibonacci(30)}") # 832040, calculé en un éclair +print(f"fibonacci(35) = {fibonacci(35)}") # 9227465 (réutilise le cache) +print("Cache fibonacci :", fibonacci.cache_info()) +# CacheInfo(hits=..., misses=36, maxsize=None, currsize=36) + + +# --- lru_cache(maxsize=N) : borne la mémoire (éviction des moins récents) --- +@lru_cache(maxsize=128) +def carre(n): + print(f" (calcul de carre({n}))") + return n * n + + +print("\nAvec lru_cache(maxsize=128) :") +print("carre(4) =", carre(4)) # calculé +print("carre(4) =", carre(4)) # en cache : aucun recalcul +print("carre(5) =", carre(5)) # calculé +print("Statistiques :", carre.cache_info()) # hits=1, misses=2, currsize=2 + +# Vider le cache si besoin +carre.cache_clear() +print("Après cache_clear :", carre.cache_info()) # hits=0, misses=0, currsize=0 diff --git a/03-programmation-orientee-objet/exemples/05_20_dataclass_heritage.py b/03-programmation-orientee-objet/exemples/05_20_dataclass_heritage.py index 60c679f..f2a25ab 100644 --- a/03-programmation-orientee-objet/exemples/05_20_dataclass_heritage.py +++ b/03-programmation-orientee-objet/exemples/05_20_dataclass_heritage.py @@ -1,6 +1,7 @@ # ============================================================================ # Section 3.5 : Héritage de dataclasses -# Description : Dataclass Animal héritée par Chien avec champs supplémentaires +# Description : Dataclass Animal héritée par Chien ; piège de l'ordre des +# champs et solution kw_only=True (Python 3.10+) # Fichier source : 05-metaclasses-et-prog-avancee.md # ============================================================================ @@ -18,3 +19,30 @@ class Chien(Animal): rex = Chien("Rex", 5, "Berger Allemand", dresse=True) print(rex) # Chien(nom='Rex', age=5, race='Berger Allemand', dresse=True) + +# --- ⚠️ Piège : un champ SANS défaut ne peut pas suivre un champ AVEC défaut --- +print() +try: + @dataclass + class Base: + nom: str + actif: bool = True # champ AVEC défaut + + @dataclass + class Derivee(Base): + priorite: int # champ SANS défaut après un champ AVEC défaut +except TypeError as e: + print(f"TypeError : {e}") + +# --- ✅ Solution (Python 3.10+) : kw_only=True supprime la contrainte d'ordre --- +@dataclass(kw_only=True) +class BaseKw: + nom: str + actif: bool = True + +@dataclass(kw_only=True) +class DeriveeKw(BaseKw): + priorite: int # OK : tous les champs sont passés par mot-clé + +tache = DeriveeKw(nom="tâche", priorite=5) +print(tache) # DeriveeKw(nom='tâche', actif=True, priorite=5) diff --git a/03-programmation-orientee-objet/exemples/05_22_duck_typing_protocoles.py b/03-programmation-orientee-objet/exemples/05_22_duck_typing_protocoles.py index fe31d5e..8dc3e02 100644 --- a/03-programmation-orientee-objet/exemples/05_22_duck_typing_protocoles.py +++ b/03-programmation-orientee-objet/exemples/05_22_duck_typing_protocoles.py @@ -44,16 +44,16 @@ def draw(self) -> str: class Circle: def draw(self) -> str: - return "O" + return "○" class Square: def draw(self) -> str: - return "[]" + return "□" def render(shape: Drawable) -> None: """Accepte n'importe quel objet qui a une méthode draw()""" print(shape.draw()) # Fonctionne sans que Circle ou Square héritent de Drawable -render(Circle()) # O -render(Square()) # [] +render(Circle()) # ○ +render(Square()) # □ diff --git a/03-programmation-orientee-objet/exemples/05_23_bonnes_pratiques.py b/03-programmation-orientee-objet/exemples/05_23_bonnes_pratiques.py index 9f8b417..6f03c8f 100644 --- a/03-programmation-orientee-objet/exemples/05_23_bonnes_pratiques.py +++ b/03-programmation-orientee-objet/exemples/05_23_bonnes_pratiques.py @@ -61,7 +61,7 @@ def __init__(self, x, y): # Utile si vous créez des milliers de points points = [Point(i, i*2) for i in range(10000)] -print(f"10000 points créés avec __slots__") +print("10000 points créés avec __slots__") # --- Documenter les métaclasses --- class MyMeta(type): diff --git a/03-programmation-orientee-objet/exemples/README.md b/03-programmation-orientee-objet/exemples/README.md index b63e2c6..b6fd8d3 100644 --- a/03-programmation-orientee-objet/exemples/README.md +++ b/03-programmation-orientee-objet/exemples/README.md @@ -1,20 +1,39 @@ # Exemples - Chapitre 03 : Programmation orientée objet -67 fichiers d'exemples exécutables, répartis sur 5 fichiers source. +74 fichiers d'exemples exécutables, répartis sur 5 fichiers source. -## Fichier 01 : Classes et objets (9 fichiers) +**Convention de nommage** : `SS_NN_description.py`, où `SS` est le numéro de section (01 à 05) et `NN` l'ordre de l'exemple. Exemple : `03_05_len_getitem_setitem.py` = section 3.3, 5ᵉ exemple. + +## Prérequis + +- **Python 3.10+** (le cours s'appuie sur la syntaxe moderne ; `@dataclass` requiert 3.7+, l'affichage des erreurs d'`abstractmethod` suit le format de Python 3.12). +- **Aucune dépendance externe** : uniquement la bibliothèque standard (`dataclasses`, `abc`, `functools`, `typing`, `datetime`, `math`). + +## Correspondance avec le cours + +Chaque exemple reprend le code de son fichier `.md` source (indiqué sous chaque tableau). Pour rester **exécutables et autonomes**, les `.py` adaptent parfois le cours : + +- les erreurs volontaires (`ValueError`, `TypeError`, `AttributeError`…) sont encadrées par `try/except` au lieu d'être laissées en commentaire ; +- plusieurs redéfinitions successives d'une même classe dans le cours sont parfois **fusionnées** en une seule classe cohérente (ex. `Playlist`) ; +- certains calculs longs (`time.sleep`) sont remplacés par un calcul rapide équivalent. + +La **logique et les valeurs** restent identiques à celles du cours. + +## Fichier 01 : Classes et objets (11 fichiers) | Fichier | Section | Description | Sortie attendue | |---------|---------|-------------|-----------------| -| `01_01_premiere_classe.py` | 3.1 | Première classe, `__init__`, attributs d'instance | Rex (Labrador), 3 ans | -| `01_02_methodes_instance.py` | 3.1 | Méthodes d'instance (aboyer, se_presenter, vieillir) | Rex aboie, se présente, vieillit à 4 ans | -| `01_03_attributs_classe.py` | 3.1 | Attributs de classe vs instance | espece=Canis familiaris, nb_pattes=4 partagés | +| `01_01_premiere_classe.py` | 3.1 | Première classe, `__init__`, attributs d'instance | Rex 5 ans (Berger Allemand), Bella 3 ans (Labrador) | +| `01_02_methodes_instance.py` | 3.1 | Méthodes d'instance (aboyer, se_presenter, vieillir) | Rex aboie, se présente, vieillit à 6 ans | +| `01_03_attributs_classe.py` | 3.1 | Attributs de classe vs instance (+ piège des mutables) | Canis familiaris, nb_pattes=4 ; liste partagée vs init dans `__init__` | | `01_04_compte_bancaire.py` | 3.1 | Classe CompteBancaire complète | 1500 -> 1300 -> intérêts 26.00 -> historique | -| `01_05_classe_personne.py` | 3.1 | Classe Personne avec anniversaire et majorité | Alice 30 ans, est_majeur, anniversaire -> 31 | +| `01_05_classe_personne.py` | 3.1 | Classe Personne avec anniversaire et majorité | Marie 25 ans / Pierre 17 ans (majorité True/False), Pierre -> 18 ans | | `01_06_modification_attributs.py` | 3.1 | Modification directe d'attributs | Voiture change de couleur et kilométrage | | `01_07_instances_independantes.py` | 3.1 | Instances indépendantes (Compteur) | compteur1=2, compteur2=11 (indépendants) | -| `01_08_bonnes_pratiques.py` | 3.1 | Bonnes pratiques (Livre, Rectangle, Etudiant) | Surface 15/70, moyenne 15.0/16.0 | -| `01_09_gestionnaire_taches.py` | 3.1 | Gestionnaire de tâches complet | 3 tâches, 1/3 terminées, filtrage par priorité | +| `01_08_bonnes_pratiques.py` | 3.1 | Bonnes pratiques (Livre, Rectangle, Etudiant) | Surface 15/70, moyenne 15.0 | +| `01_09_gestionnaire_taches.py` | 3.1 | Gestionnaire de tâches complet | 3 tâches, 1/3 terminées, filtrage des non-terminées | +| `01_10_acces_dynamique.py` | 3.1 | Accès dynamique : `getattr`/`setattr`/`hasattr`/`delattr` | Alice, 30, Inconnue, AttributeError, Paris, dict→objet | +| `01_11_shadowing_attribut_classe.py` | 3.1 | Écrire un attribut de classe via l'instance (shadowing) | c1=Loup (instance) ; c2/classe inchangés ; `__dict__` | **Fichier source** : `01-classes-et-objets.md` @@ -29,37 +48,39 @@ | `02_05_polymorphisme.py` | 3.2 | Polymorphisme des formes géométriques | Cercle 78.54, Carré 16, Triangle 9.0 | | `02_06_systeme_paiement.py` | 3.2 | Système de paiement polymorphe | 4 types de paiement traités | | `02_07_isinstance_issubclass.py` | 3.2 | isinstance et issubclass | Vérifications de type True/False | -| `02_08_heritage_multiple.py` | 3.2 | Héritage multiple et MRO | Ordre de résolution des méthodes | +| `02_08_heritage_multiple.py` | 3.2 | Héritage multiple, MRO et `super()` coopératif | Ordre de résolution ; `super()` suit le MRO (A puis B) | | `02_09_systeme_fichiers.py` | 3.2 | Système de fichiers (Fichier, Dossier) | Taille totale 3000 Ko | | `02_10_heritage_vs_composition.py` | 3.2 | Héritage vs composition | Comparaison des deux approches | **Fichier source** : `02-heritage-et-polymorphisme.md` -## Fichier 03 : Méthodes spéciales (10 fichiers) +## Fichier 03 : Méthodes spéciales (12 fichiers) | Fichier | Section | Description | Sortie attendue | |---------|---------|-------------|-----------------| | `03_01_str_repr.py` | 3.3 | `__str__` et `__repr__` | Personne affichée, Livre avec liste | | `03_02_operateurs_arithmetiques.py` | 3.3 | `__add__`, `__sub__`, etc. (Vecteur, Nombre) | Vecteur(3,7), Nombre 13/7/30/3.33 | -| `03_03_classe_argent.py` | 3.3 | Classe Argent avec opérateurs | 80.50, 70.50, 100.00 EUR | -| `03_04_comparaisons.py` | 3.3 | Opérateurs de comparaison (Personne) | Tri par âge : Bob 25, Alice 30, Charlie 30 | +| `03_03_classe_argent.py` | 3.3 | Classe Argent avec opérateurs (+ réfléchi `__rmul__`) | 80.50, 70.50, 100.00 EUR ; `2 * prix` = 100.00 | +| `03_04_comparaisons.py` | 3.3 | Comparaisons (Personne) + piège `__eq__`/`__hash__` | Tri Bob/Alice/Charlie ; non hashable puis correction | | `03_05_len_getitem_setitem.py` | 3.3 | Playlist avec indexation et slicing | Accès par index, slice, del | | `03_06_iter_next.py` | 3.3 | `__iter__`/`__next__` (Compte, Bibliothèque) | Compte 1-5, itération livres | | `03_07_contains_call_bool.py` | 3.3 | `__contains__`, `__call__`, `__bool__` | Equipe/Multiplicateur/Panier | | `03_08_context_manager.py` | 3.3 | `__enter__`/`__exit__` (FichierLog, Chronomètre) | Log écrit, calcul chronométré | | `03_09_vecteur_complet.py` | 3.3 | Classe Vecteur complète | abs=5.0, toutes les opérations | | `03_10_fraction.py` | 3.3 | Classe Fraction avec arithmétique | 1/2+1/3=5/6, simplification 4/8=1/2 | +| `03_11_total_ordering.py` | 3.3 | `@functools.total_ordering` (6 comparaisons depuis 2) | True/True/False/False/False/True, tri + min/max | +| `03_12_egalite_vs_identite.py` | 3.3 | Égalité `==` (`__eq__`, valeur) vs identité `is` | sans `__eq__` : a==b False ; avec : d==e True mais d is e False | **Fichier source** : `03-methodes-speciales.md` -## Fichier 04 : Propriétés et décorateurs (15 fichiers) +## Fichier 04 : Propriétés et décorateurs (18 fichiers) | Fichier | Section | Description | Sortie attendue | |---------|---------|-------------|-----------------| -| `04_01_property_base.py` | 3.4 | Problème d'accès direct vs @property | Accès contrôlé à l'âge | +| `04_01_property_base.py` | 3.4 | Problème d'accès direct vs @property | Accès contrôlé au solde (validation < 0) | | `04_02_cercle_proprietes.py` | 3.4 | Propriétés en lecture seule (Cercle) | Diamètre, surface, périmètre calculés | -| `04_03_property_setter_deleter.py` | 3.4 | Setter et deleter de propriétés | Temperature °C/°F, Personne avec deleter | -| `04_04_rectangle_proprietes.py` | 3.4 | Rectangle avec propriétés calculées | 5*3=15, 10*4=40, est_carre=True/False | +| `04_03_property_setter_deleter.py` | 3.4 | Setter et deleter de propriétés | Temperature °C (validation zéro absolu), Personne avec deleter | +| `04_04_rectangle_proprietes.py` | 3.4 | Rectangle avec propriétés calculées | 5*3=15, 10*4=40, validation des dimensions | | `04_05_personne_validation.py` | 3.4 | Validation dans les setters | Nom DUPONT, prénom Marie, email lowercase | | `04_06_decorateur_base.py` | 3.4 | Premier décorateur (avant/après, chronomètre) | Messages avant/après, temps d'exécution | | `04_07_decorateur_arguments.py` | 3.4 | Décorateur avec *args et **kwargs | Logger affichant arguments et résultat | @@ -71,6 +92,9 @@ | `04_13_staticmethod_classmethod.py` | 3.4 | @staticmethod et @classmethod | 8, 28, factory method, Demo comparaison | | `04_14_exemple_complet_utilisateur.py` | 3.4 | Classe Utilisateur complète | Properties + décorateurs combinés | | `04_15_functools_wraps.py` | 3.4 | functools.wraps pour métadonnées | __name__ et __doc__ préservés | +| `04_16_encapsulation.py` | 3.4 | Conventions `_` (interne) et `__` (name mangling) | `_solde` accessible, `__solde` manglé, anti-collision en héritage | +| `04_17_cached_property.py` | 3.4 | `@cached_property` : calcul mémorisé une fois | « Calcul… » une seule fois puis 4, invalidation par `del` | +| `04_18_lru_cache.py` | 3.4 | Cache standard `functools.cache` / `lru_cache` | fibonacci(30)=832040, `cache_info` (hits/misses), `cache_clear` | **Fichier source** : `04-proprietes-et-decorateurs.md` @@ -97,7 +121,7 @@ | `05_17_dataclass_parametres.py` | 3.5 | frozen=True, order=True | FrozenInstanceError, versions triées | | `05_18_dataclass_field.py` | 3.5 | field() et default_factory | Configuration avec options et metadata | | `05_19_dataclass_post_init.py` | 3.5 | `__post_init__` (champs calculés) | email généré automatiquement | -| `05_20_dataclass_heritage.py` | 3.5 | Héritage de dataclasses | Chien(nom='Rex', age=5, race=...) | +| `05_20_dataclass_heritage.py` | 3.5 | Héritage de dataclasses (+ piège d'ordre, `kw_only`) | Chien(...) ; TypeError d'ordre puis solution `kw_only=True` | | `05_21_slots.py` | 3.5 | `__slots__` optimisation mémoire | 344 bytes vs 48 bytes | | `05_22_duck_typing_protocoles.py` | 3.5 | Duck typing et typing.Protocol | Logger avec fake_file, Circle/Square | | `05_23_bonnes_pratiques.py` | 3.5 | Bonnes pratiques avancées | init_subclass, ABC, slots, MyMeta | diff --git a/03-programmation-orientee-objet/log.txt b/03-programmation-orientee-objet/log.txt new file mode 100644 index 0000000..ff11007 --- /dev/null +++ b/03-programmation-orientee-objet/log.txt @@ -0,0 +1,3 @@ +Début du programme +Traitement en cours... +Fin du programme diff --git a/04-gestion-donnees-et-fichiers/01-lecture-ecriture-fichiers.md b/04-gestion-donnees-et-fichiers/01-lecture-ecriture-fichiers.md index 64e95f8..6d4c86b 100644 --- a/04-gestion-donnees-et-fichiers/01-lecture-ecriture-fichiers.md +++ b/04-gestion-donnees-et-fichiers/01-lecture-ecriture-fichiers.md @@ -60,7 +60,7 @@ Pour les fichiers volumineux, il est préférable de lire ligne par ligne : fichier = open('mon_document.txt', 'r', encoding='utf-8') for ligne in fichier: - print(ligne.strip()) # strip() enlève les retours à la ligne + print(ligne.strip()) # strip() enlève le retour à la ligne (et les espaces) en début/fin fichier.close() ``` @@ -125,6 +125,22 @@ fichier.write("Et encore une autre ligne\n") fichier.close() ``` +### Mode 'x' - Création exclusive (échoue si le fichier existe) + +Le mode `'x'` crée un fichier **uniquement s'il n'existe pas encore**. Si le fichier existe déjà, il lève une `FileExistsError` au lieu de l'écraser — pratique pour éviter de détruire accidentellement des données : + +```python +# Créer un fichier seulement s'il n'existe pas +try: + with open('rapport.txt', 'x', encoding='utf-8') as fichier: + fichier.write("Premier rapport\n") + print("Fichier créé") +except FileExistsError: + print("Le fichier existe déjà : aucune donnée écrasée") +``` + +Contrairement au mode `'w'` (qui écrase sans prévenir), le mode `'x'` garantit que vous ne détruisez jamais un fichier existant. + ### Écrire plusieurs lignes avec writelines() ```python @@ -141,6 +157,8 @@ fichier.writelines(courses) fichier.close() ``` +> ⚠️ **`writelines()` n'ajoute aucun retour à la ligne**, contrairement à ce que son nom laisse penser. Il écrit les chaînes les unes à la suite des autres, sans séparateur : `writelines(["Pommes", "Pain"])` produit `PommesPain` sur une seule ligne. C'est pourquoi chaque élément de la liste se termine ici par `\n`. (Son symétrique `readlines()` **conserve** d'ailleurs les `\n` de chaque ligne lue : les deux méthodes sont cohérentes l'une avec l'autre.) + --- ## Le Gestionnaire de Contexte : `with` @@ -252,6 +270,46 @@ with open('gros_fichier.bin', 'rb') as fichier: --- +## Positionnement dans un Fichier : `seek()` et `tell()` + +Lorsqu'on lit ou écrit, Python maintient une **position courante** dans le fichier (le « curseur »). Deux méthodes permettent de la consulter et de la modifier : + +- `tell()` : retourne la position actuelle (en octets depuis le début) ; +- `seek(position)` : déplace le curseur à la position voulue. + +```python +with open('donnees.txt', 'w', encoding='utf-8') as f: + f.write("ABCDEFGHIJ") + +with open('donnees.txt', 'r', encoding='utf-8') as f: + print(f.tell()) # 0 (début du fichier) + print(f.read(3)) # ABC + print(f.tell()) # 3 (le curseur a avancé) + + f.seek(0) # revenir au début + print(f.read(2)) # AB + + f.seek(5) # sauter directement à la position 5 + print(f.read()) # FGHIJ +``` + +### Le mode `'r+'` : lire *et* écrire + +Le mode `'r+'` ouvre le fichier en lecture **et** écriture sans l'effacer (contrairement à `'w'`). Combiné à `seek()`, il permet de modifier une partie d'un fichier existant : + +```python +with open('donnees.txt', 'r+', encoding='utf-8') as f: + f.seek(0) + f.write("12345") # remplace les 5 premiers caractères + +with open('donnees.txt', 'r', encoding='utf-8') as f: + print(f.read()) # 12345FGHIJ +``` + +> ⚠️ En mode **texte**, n'utilisez `seek()` qu'avec `0` ou une position renvoyée par `tell()` : à cause de l'encodage, un caractère peut occuper plusieurs octets, donc « compter en caractères » n'est pas fiable. Pour un contrôle précis octet par octet, travaillez en mode binaire (`'rb+'`). + +--- + ## Vérifier l'Existence d'un Fichier Avant d'ouvrir un fichier, on peut vérifier s'il existe avec `pathlib` : @@ -272,7 +330,7 @@ else: print("Le fichier n'existe pas") ``` -> 💡 Vous verrez parfois l'approche plus ancienne avec `os.path.exists()` et `os.path.isfile()`. Les deux fonctionnent, mais `pathlib` est l'approche moderne recommandée (voir section 4.4). +> 💡 Vous verrez parfois l'approche plus ancienne avec `os.path.exists()` et `os.path.isfile()`. Les deux fonctionnent, mais `pathlib` est l'approche moderne recommandée (voir la [section 4.4](/04-gestion-donnees-et-fichiers/04-gestion-chemins-pathlib.md)). --- @@ -303,6 +361,8 @@ with open('fichier.txt', 'r') as f: contenu = f.read() ``` +> 💡 **Pourquoi est-ce important ?** Sans le paramètre `encoding`, Python utilise l'encodage **par défaut du système**, qui dépend de la plateforme : souvent UTF-8 sous Linux/macOS, mais parfois `cp1252` sous Windows. Un fichier UTF-8 contenant des accents, lu sans préciser l'encodage, peut alors lever une `UnicodeDecodeError` ou afficher des caractères incohérents (le fameux « mojibake », par ex. `é` au lieu de `é`). Préciser `encoding='utf-8'` garantit le **même comportement sur tous les systèmes**. + ### 3. Gérer les erreurs ```python @@ -367,6 +427,8 @@ with open('donnees.csv', 'r', encoding='utf-8') as fichier: print(valeurs) ``` +> ⚠️ Cette méthode « maison » ne convient qu'aux CSV **très simples**. Elle se casse dès qu'un champ contient lui-même une virgule : sur `"Paris, France",75000`, `split(',')` produit **trois** morceaux au lieu de deux. Pour lire des CSV réels de façon fiable (champs entre guillemets, virgules ou retours à la ligne internes…), utilisez le **module `csv`** présenté à la [section 4.2](/04-gestion-donnees-et-fichiers/02-formats-de-donnees.md). + ### Exemple 4 : Sauvegarder une liste en fichier ```python diff --git a/04-gestion-donnees-et-fichiers/02-formats-de-donnees.md b/04-gestion-donnees-et-fichiers/02-formats-de-donnees.md index 3a5b8d8..0b4d0dd 100644 --- a/04-gestion-donnees-et-fichiers/02-formats-de-donnees.md +++ b/04-gestion-donnees-et-fichiers/02-formats-de-donnees.md @@ -133,6 +133,24 @@ print(type(data)) # - `json.dump()` : écrit dans un **fichier** - `json.dumps()` : écrit dans une **chaîne de caractères** +### Correspondance des types Python ↔ JSON + +Lors de la conversion, Python traduit ses types vers leurs équivalents JSON (et inversement) : + +| Python | JSON | +|--------|------| +| `dict` | objet `{ }` | +| `list`, `tuple` | tableau `[ ]` | +| `str` | chaîne | +| `int`, `float` | nombre | +| `True` / `False` | `true` / `false` | +| `None` | `null` | + +> ⚠️ **Trois pièges classiques à connaître :** +> - **Les `tuple` deviennent des tableaux** : `json.dumps({"t": (1, 2)})` donne `{"t": [1, 2]}`. À la relecture, vous récupérez une **liste**, pas un tuple. +> - **Les clés de dictionnaire sont toujours converties en chaînes** : `json.dumps({1: "a"})` donne `{"1": "a"}`. Après `json.loads`, la clé `1` est devenue `"1"`. +> - **Tous les objets ne sont pas sérialisables** : un `datetime`, un `set` ou un objet de vos classes lèvent `TypeError: Object of type ... is not JSON serializable`. Solution simple : `json.dump(data, f, default=str)` (convertit en texte), ou un encodeur personnalisé pour un contrôle précis. + ### Exemple : Liste de personnes ```python @@ -243,6 +261,16 @@ with open('employes.csv', 'r', encoding='utf-8') as fichier: print() ``` +> ⚠️ **Toutes les valeurs lues d'un CSV sont des chaînes de caractères**, même les nombres : `ligne['age']` vaut `'30'` (type `str`), pas `30` (type `int`). Le module `csv` ne devine **jamais** les types. Pour faire des calculs, convertissez explicitement avec `int()`, `float()`, etc. : +> +> ```python +> # ❌ Erreur : on additionne des chaînes +> total = sum(ligne['salaire'] for ligne in csv.DictReader(f)) # TypeError +> +> # ✅ Correct : conversion explicite +> total = sum(int(ligne['salaire']) for ligne in csv.DictReader(f)) +> ``` + ### Écrire dans un fichier CSV #### Méthode 1 : Avec `csv.writer()` @@ -335,6 +363,8 @@ with open('informaticiens.csv', 'w', encoding='utf-8', newline='') as fichier: print(f"{len(informaticiens)} informaticiens exportés") ``` +> 💡 **Pourquoi `extrasaction='ignore'` ?** Les dictionnaires `informaticiens` contiennent une clé `'service'` qui ne figure **pas** dans `colonnes`. Par défaut, `DictWriter` lèverait une `ValueError` (« dict contains fields not in fieldnames ») en rencontrant cette clé en trop. `extrasaction='ignore'` lui demande d'**ignorer** les clés absentes de `fieldnames` — pratique pour n'exporter qu'une sélection de colonnes. + --- ## XML (eXtensible Markup Language) @@ -378,6 +408,8 @@ Python dispose d'un module intégré pour manipuler XML : import xml.etree.ElementTree as ET ``` +> ⚠️ **Sécurité : ne parsez pas de XML non fiable tel quel.** Comme l'indique la documentation officielle, `xml.etree.ElementTree` n'est **pas conçu pour résister à du XML malveillant**. Certains documents piégés (attaques par expansion d'entités, dites « billion laughs ») peuvent saturer la mémoire. Pour parser des données provenant d'une source non fiable (réseau, fichier externe), utilisez le paquet tiers [`defusedxml`](https://pypi.org/project/defusedxml/), conçu pour neutraliser ces attaques. + ### Lire un fichier XML ```python @@ -570,6 +602,41 @@ for item in racine.findall('.//item'): --- +## Autres Formats à Connaître : TOML et YAML + +JSON, CSV et XML ne sont pas les seuls formats que vous croiserez. Deux autres sont devenus incontournables, surtout pour la **configuration**. + +### TOML + +**TOML** est le format de configuration de l'écosystème Python moderne : c'est celui du fichier `pyproject.toml`. Il est lisible, **typé** (nombres, booléens, dates) et organisé en sections. Depuis **Python 3.11**, le module **`tomllib`** permet de le lire sans rien installer : + +```python +import tomllib + +# tomllib.load() exige le mode binaire ('rb') +with open('config.toml', 'rb') as f: + config = tomllib.load(f) + +print(config['serveur']['port']) # 8000 — un vrai int, pas une chaîne +``` + +> 💡 `tomllib` est en **lecture seule** (pas de fonction `dump()`). Pour *écrire* du TOML, utilisez un paquet tiers comme `tomli-w` ou `tomlkit`. + +### YAML + +**YAML** est très répandu pour la configuration (Docker, GitHub Actions, Ansible…). Plus concis que JSON et acceptant les commentaires, il n'est **pas** dans la bibliothèque standard : il faut installer `PyYAML` (`pip install pyyaml`). + +```python +import yaml + +with open('config.yaml', 'r', encoding='utf-8') as f: + config = yaml.safe_load(f) # ✅ safe_load, jamais load() ! +``` + +> ⚠️ **Sécurité** : utilisez toujours `yaml.safe_load()`, et non `yaml.load()`. Comme pickle, le chargement complet de YAML peut instancier des objets arbitraires et exécuter du code à partir d'un fichier malveillant. + +--- + ## Exemple Pratique : Conversion entre Formats ### CSV → JSON diff --git a/04-gestion-donnees-et-fichiers/03-serialisation-pickle.md b/04-gestion-donnees-et-fichiers/03-serialisation-pickle.md index 2f83947..022c6a5 100644 --- a/04-gestion-donnees-et-fichiers/03-serialisation-pickle.md +++ b/04-gestion-donnees-et-fichiers/03-serialisation-pickle.md @@ -250,6 +250,8 @@ print(f"Type : {type(personne_chargee)}") **Important :** La définition de la classe doit être disponible lors du chargement ! +> 💡 **Pourquoi ?** Pickle ne sauvegarde **pas le code** de la classe, seulement les **données** de l'instance (ses attributs) et une **référence** à la classe sous la forme `module.NomDeClasse`. Au chargement, il **réimporte** la classe via cette référence pour reconstruire l'objet — d'où l'échec si la classe est introuvable (non définie, renommée ou déplacée). C'est la même raison qui rend une `lambda` non picklable (pas de nom réimportable, voir *Limitations*) et qui cantonne pickle à **Python**. + --- ## Exemple Pratique : Système de Sauvegarde de Jeu @@ -452,13 +454,15 @@ print(f"Protocole le plus récent : {pickle.HIGHEST_PROTOCOL}") |---------|--------|------| | **Format** | Binaire | Texte | | **Lisible** | ❌ Non | ✅ Oui | -| **Types supportés** | ✅ Tous les types Python | ⚠️ Types limités | +| **Types supportés** | ✅ La plupart des types Python | ⚠️ Types limités | | **Objets personnalisés** | ✅ Oui | ❌ Non (sans conversion) | | **Sécurité** | ⚠️ Risques | ✅ Sûr | | **Portabilité** | ⚠️ Python uniquement | ✅ Universel | | **Taille fichier** | ✅ Compact | ⚠️ Plus volumineux | | **Vitesse** | ✅ Rapide | ⚠️ Plus lent | +> 💡 **« La plupart » et non « tous »** : `pickle` accepte les types natifs (`tuple`, `set`, `bytes`…) **et** vos propres classes — bien plus large que JSON. Il reste toutefois des exceptions : les objets liés à l'état d'exécution (fonctions `lambda`, fichiers ouverts, sockets réseau) ne peuvent **pas** être sérialisés, comme détaillé dans la section *Limitations de Pickle* ci-dessous. + ### Exemple Comparatif ```python @@ -473,7 +477,7 @@ donnees = { 'bytes': b'data' # Bytes } -# Pickle : fonctionne avec tous les types +# Pickle : gère ces types complexes sans conversion with open('donnees.pkl', 'wb') as f: pickle.dump(donnees, f) print("✅ Pickle : sauvegarde réussie") @@ -556,12 +560,12 @@ Certains objets ne peuvent pas être pickled : ```python import pickle -# ❌ Ne fonctionne pas -fichier_ouvert = open('test.txt', 'r') +# ❌ Ne fonctionne pas : un objet fichier ouvert n'est pas sérialisable +fichier_ouvert = open('test.txt', 'w') try: pickle.dumps(fichier_ouvert) except TypeError as e: - print(f"Erreur : {e}") + print(f"Erreur : {e}") # TypeError : cannot pickle ... TextIOWrapper (le libellé exact varie selon la version) finally: fichier_ouvert.close() ``` @@ -575,8 +579,9 @@ import pickle ma_fonction = lambda x: x * 2 try: pickle.dumps(ma_fonction) -except AttributeError as e: - print(f"Erreur : impossible de pickler une lambda") +except (pickle.PicklingError, AttributeError): + # PicklingError pour une lambda de module, AttributeError si elle est locale + print("Erreur : impossible de pickler une lambda") ``` ### 3. Objets avec des connexions réseau diff --git a/04-gestion-donnees-et-fichiers/04-gestion-chemins-pathlib.md b/04-gestion-donnees-et-fichiers/04-gestion-chemins-pathlib.md index 08c6c4b..c5c7c47 100644 --- a/04-gestion-donnees-et-fichiers/04-gestion-chemins-pathlib.md +++ b/04-gestion-donnees-et-fichiers/04-gestion-chemins-pathlib.md @@ -105,6 +105,8 @@ print(fichier) # Résultat : mes_documents/projets/python/script.py ``` +> ⚠️ **Piège : un argument absolu efface ce qui précède.** Si l'opérande de droite de `/` est un chemin **absolu**, toute la partie de gauche est **ignorée** : `Path('mes_documents') / '/etc'` donne `/etc`, et non `mes_documents/etc`. Pour construire un chemin à partir d'une base, ne joignez que des fragments **relatifs**. + ### Joindre avec `joinpath()` ```python @@ -187,6 +189,31 @@ print(f"📂 Dossier parent : {fichier.parent}") print(f"🗂️ Grand-parent : {fichier.parent.parent}") ``` +### Transformer un chemin : `with_suffix()`, `with_name()`, `with_stem()` + +Ces méthodes ne modifient pas l'objet d'origine : elles **retournent un nouveau** `Path` avec un composant changé. Très pratique pour dériver un chemin à partir d'un autre (changer d'extension, renommer…) : + +```python +from pathlib import Path + +fichier = Path('rapports/donnees.csv') + +# Changer l'extension +print(fichier.with_suffix('.json')) # rapports/donnees.json + +# Changer le nom complet (nom + extension) +print(fichier.with_name('resume.txt')) # rapports/resume.txt + +# Changer le nom sans toucher à l'extension (Python 3.9+) +print(fichier.with_stem('donnees_2024')) # rapports/donnees_2024.csv +``` + +> 💡 Cas d'usage fréquent : créer un fichier dérivé à côté d'un fichier existant, par exemple une sauvegarde ou une version convertie. +> ```python +> source = Path('document.txt') +> sauvegarde = source.with_suffix('.bak') # document.bak +> ``` + --- ## Chemins Absolus et Relatifs @@ -219,6 +246,17 @@ chemin_resolu = chemin.resolve() print(f"Répertoire courant : {chemin_resolu}") ``` +> 💡 **`absolute()` ou `resolve()` ?** Les deux renvoient un chemin absolu, mais `absolute()` se contente de **préfixer le répertoire courant** : un `..` présent dans le chemin est **conservé** tel quel. `resolve()` va plus loin — il **normalise** le chemin (supprime les `.` et `..`) **et** suit les liens symboliques, produisant le chemin **canonique** (en général préférable). Depuis Python 3.6, il n'exige pas que le chemin existe. + +```python +from pathlib import Path + +chemin = Path('dossier/sousdossier/../fichier.txt') + +print(chemin.absolute()) # .../dossier/sousdossier/../fichier.txt (le '..' reste) +print(chemin.resolve()) # .../dossier/fichier.txt (normalisé) +``` + ### Chemin Relatif Entre Deux Chemins ```python @@ -294,11 +332,11 @@ def analyser_chemin(chemin_str): print("✅ Le chemin existe") if chemin.is_file(): - print(f"📄 Type : Fichier") + print("📄 Type : Fichier") taille = chemin.stat().st_size print(f"📊 Taille : {taille} octets") elif chemin.is_dir(): - print(f"📁 Type : Dossier") + print("📁 Type : Dossier") nb_fichiers = len(list(chemin.iterdir())) print(f"📊 Nombre d'éléments : {nb_fichiers}") @@ -384,6 +422,8 @@ if fichier.exists(): print(f"Déplacé vers : {destination}") ``` +> 💡 **`rename()` ou `shutil.move()` pour déplacer ?** `Path.rename()` est rapide mais a deux limites : il **écrase silencieusement** un fichier de destination existant, et il **échoue** (`OSError`) si la source et la destination sont sur deux systèmes de fichiers différents (par exemple un disque interne et une clé USB). `shutil.move()` (employé plus loin dans l'exemple « Organiser des fichiers ») est plus robuste : il gère ce cas en copiant puis supprimant. Pour un simple renommage au même endroit, `rename()` suffit ; pour déplacer ailleurs de façon sûre, préférez `shutil.move()`. + ### Copier un Fichier ```python @@ -398,6 +438,10 @@ if source.exists(): print(f"Fichier copié : {source} → {destination}") ``` +> 💡 **Le module `shutil` complète `pathlib`** pour les opérations de **haut niveau** que `Path` ne propose pas : copier un fichier (`shutil.copy`, `shutil.copy2`), déplacer un fichier ou un dossier entier (`shutil.move`), et supprimer un dossier non vide (`shutil.rmtree`, vu plus haut). +> +> **`copy` vs `copy2`** : `shutil.copy` copie le **contenu** et les permissions, mais **pas** les dates ; `shutil.copy2` préserve **en plus les métadonnées** (notamment la date de dernière modification). Pour une **sauvegarde** fidèle, on préfère donc `copy2` — c'est exactement pourquoi l'exemple de backup plus bas l'utilise. + --- ## Lister le Contenu d'un Dossier @@ -567,9 +611,9 @@ def infos_fichier(chemin_str): # Type if chemin.is_file(): - print(f"Type : Fichier") + print("Type : Fichier") elif chemin.is_dir(): - print(f"Type : Dossier") + print("Type : Dossier") # Chemin print(f"Chemin complet : {chemin.absolute()}") @@ -732,7 +776,7 @@ def backup_fichiers(dossier_source, dossier_backup): # Créer le dossier de backup dossier_destination.mkdir(parents=True, exist_ok=True) - print(f"💾 Backup en cours...") + print("💾 Backup en cours...") print(f"Source : {source}") print(f"Destination : {dossier_destination}\n") @@ -794,6 +838,27 @@ cwd = Path.cwd() print(f"Répertoire courant : {cwd}") ``` +### Localiser un Fichier par Rapport au Script + +`Path.cwd()` renvoie le **répertoire de travail** — celui depuis lequel la commande `python` a été lancée, qui n'est **pas forcément** le dossier où se trouve votre script. Un simple `open('config.json')` cherche donc le fichier dans le répertoire courant, et lève une `FileNotFoundError` si le programme est lancé depuis ailleurs. + +Pour viser un fichier **toujours rangé au même endroit que le script**, partez de la variable spéciale `__file__` (le chemin du fichier `.py` en cours d'exécution) : + +```python +from pathlib import Path + +# Dossier contenant CE script, quelle que soit la façon de le lancer +dossier_script = Path(__file__).resolve().parent + +# Des fichiers livrés à côté du script +config = dossier_script / 'config.json' +donnees = dossier_script / 'data' / 'valeurs.csv' + +print(config) +``` + +> 💡 **Pourquoi `.resolve()` ?** Selon la manière dont le script est lancé, `__file__` peut être un chemin relatif. `Path(__file__).resolve().parent` donne un chemin **absolu et normalisé**, fiable où que vous soyez. C'est le réflexe à adopter pour toutes les **ressources** livrées avec un programme (configuration, données, gabarits), au lieu d'un chemin relatif au répertoire courant qui casse dès qu'on lance le script depuis un autre dossier. + --- ## Conversion avec l'Ancien Module `os` diff --git a/04-gestion-donnees-et-fichiers/README.md b/04-gestion-donnees-et-fichiers/README.md index 454a920..6fa1d2a 100644 --- a/04-gestion-donnees-et-fichiers/README.md +++ b/04-gestion-donnees-et-fichiers/README.md @@ -226,6 +226,8 @@ with open('notes.json', 'w') as f: | JSON | APIs, configuration | | CSV | Données tabulaires | | XML | Documents structurés | +| TOML | Configuration (ex. `pyproject.toml`) | +| YAML | Configuration (Docker, CI…) | | Pickle | Objets Python complexes | ### 3. Gestion des Ressources @@ -258,13 +260,15 @@ with open('texte.txt', 'r', encoding='utf-8') as f: ```python from pathlib import Path -# ✅ Portable (fonctionne partout) +# ✅ Idéal : Path choisit le bon séparateur selon l'OS chemin = Path('dossier') / 'sous_dossier' / 'fichier.txt' -# ❌ Pas portable (seulement Windows) +# ❌ Fragile : le backslash est le séparateur Windows ; sous Linux/macOS, +# « \ » est un caractère ordinaire, ce n'est donc pas un chemin valide chemin = 'dossier\\sous_dossier\\fichier.txt' -# ❌ Pas portable (seulement Unix) +# ⚠️ Les « / » fonctionnent sur tous les OS en Python (même Windows), +# mais coder le séparateur en dur reste moins robuste que Path chemin = 'dossier/sous_dossier/fichier.txt' ``` @@ -430,6 +434,7 @@ Au fil de ce chapitre, vous découvrirez des exemples concrets couvrant des cas | `json` | Format JSON | 4.2 | | `csv` | Format CSV | 4.2 | | `xml.etree.ElementTree` | Format XML | 4.2 | +| `tomllib` | Format TOML — lecture (Python 3.11+) | 4.2 | | `pickle` | Sérialisation | 4.3 | | `pathlib` | Gestion de chemins | 4.4 | diff --git a/04-gestion-donnees-et-fichiers/exemples/01_08_positionnement_seek_tell.py b/04-gestion-donnees-et-fichiers/exemples/01_08_positionnement_seek_tell.py new file mode 100644 index 0000000..5ae9d04 --- /dev/null +++ b/04-gestion-donnees-et-fichiers/exemples/01_08_positionnement_seek_tell.py @@ -0,0 +1,37 @@ +# ============================================================================ +# Section 4.1 : Positionnement dans un fichier (seek et tell) +# Description : tell() pour connaître la position, seek() pour la déplacer, +# mode 'r+' pour lire ET écrire +# Fichier source : 01-lecture-ecriture-fichiers.md +# ============================================================================ + +import os + +# Créer un fichier de test +with open('donnees.txt', 'w', encoding='utf-8') as f: + f.write("ABCDEFGHIJ") + +# --- tell() et seek() --- +print("=== tell() et seek() ===") +with open('donnees.txt', 'r', encoding='utf-8') as f: + print(f"Position initiale : {f.tell()}") # 0 + print(f"read(3) : {f.read(3)}") # ABC + print(f"Position : {f.tell()}") # 3 + + f.seek(0) # revenir au début + print(f"Après seek(0), read(2) : {f.read(2)}") # AB + + f.seek(5) # sauter à la position 5 + print(f"Après seek(5), read() : {f.read()}") # FGHIJ + +# --- Le mode 'r+' : lire ET écrire sans effacer --- +print("\n=== Mode 'r+' ===") +with open('donnees.txt', 'r+', encoding='utf-8') as f: + f.seek(0) + f.write("12345") # remplace les 5 premiers caractères + +with open('donnees.txt', 'r', encoding='utf-8') as f: + print(f"Après r+/write au début : {f.read()}") # 12345FGHIJ + +# Nettoyage +os.remove('donnees.txt') diff --git a/04-gestion-donnees-et-fichiers/exemples/01_09_mode_x_creation_exclusive.py b/04-gestion-donnees-et-fichiers/exemples/01_09_mode_x_creation_exclusive.py new file mode 100644 index 0000000..1fdbe00 --- /dev/null +++ b/04-gestion-donnees-et-fichiers/exemples/01_09_mode_x_creation_exclusive.py @@ -0,0 +1,35 @@ +# ============================================================================ +# Section 4.1 : Mode 'x' - Création exclusive +# Description : Créer un fichier seulement s'il n'existe pas ; FileExistsError +# si le fichier existe déjà (contrairement à 'w' qui écrase) +# Fichier source : 01-lecture-ecriture-fichiers.md +# ============================================================================ + +import os + +# Au cas où un ancien fichier traînerait +if os.path.exists('rapport.txt'): + os.remove('rapport.txt') + +# --- Première création avec 'x' : réussit (le fichier n'existe pas) --- +try: + with open('rapport.txt', 'x', encoding='utf-8') as fichier: + fichier.write("Premier rapport\n") + print("Fichier créé") +except FileExistsError: + print("Le fichier existe déjà : aucune donnée écrasée") + +# --- Deuxième tentative avec 'x' : échoue (le fichier existe maintenant) --- +try: + with open('rapport.txt', 'x', encoding='utf-8') as fichier: + fichier.write("Tentative d'écrasement") + print("Fichier créé") +except FileExistsError: + print("Le fichier existe déjà : aucune donnée écrasée") + +# Le contenu d'origine est intact : le mode 'x' n'a rien écrasé +with open('rapport.txt', 'r', encoding='utf-8') as f: + print("Contenu conservé :", f.read().strip()) + +# Nettoyage +os.remove('rapport.txt') diff --git a/04-gestion-donnees-et-fichiers/exemples/02_02_json_conversion.py b/04-gestion-donnees-et-fichiers/exemples/02_02_json_conversion.py index 17e0385..5dd0d07 100644 --- a/04-gestion-donnees-et-fichiers/exemples/02_02_json_conversion.py +++ b/04-gestion-donnees-et-fichiers/exemples/02_02_json_conversion.py @@ -1,10 +1,12 @@ # ============================================================================ # Section 4.2 : JSON - Conversion entre Python et JSON -# Description : json.dumps() et json.loads() pour convertir sans fichier +# Description : json.dumps() et json.loads() pour convertir sans fichier, +# et les pièges de conversion (tuple, clés, types non sérialisables) # Fichier source : 02-formats-de-donnees.md # ============================================================================ import json +from datetime import datetime # --- Python -> JSON (chaîne de caractères) --- print("=== dumps() : Python -> JSON ===") @@ -23,3 +25,21 @@ data = json.loads(json_string) print(data) print(type(data)) # + +# --- Pièges de conversion à connaître --- +print("\n=== Pièges de conversion ===") + +# 1. Les tuples deviennent des tableaux (relus comme des listes) +print("tuple ->", json.dumps({"t": (1, 2)})) # {"t": [1, 2]} + +# 2. Les clés de dictionnaire sont converties en chaînes +print("clé 1 ->", json.dumps({1: "a"})) # {"1": "a"} + +# 3. Tous les objets ne sont pas sérialisables +try: + json.dumps({"date": datetime(2026, 6, 1)}) +except TypeError as e: + print("datetime ->", e) # Object of type datetime is not JSON serializable + +# Solution simple : default=str (convertit en texte) +print("avec default=str ->", json.dumps({"date": datetime(2026, 6, 1)}, default=str)) diff --git a/04-gestion-donnees-et-fichiers/exemples/02_04_csv_lecture.py b/04-gestion-donnees-et-fichiers/exemples/02_04_csv_lecture.py index 172c99c..a1527d2 100644 --- a/04-gestion-donnees-et-fichiers/exemples/02_04_csv_lecture.py +++ b/04-gestion-donnees-et-fichiers/exemples/02_04_csv_lecture.py @@ -38,10 +38,28 @@ for ligne in lecteur: print(f"{ligne['prenom']} {ligne['nom']}") - print(f" Age: {ligne['age']} ans") + print(f" Âge: {ligne['age']} ans") print(f" Salaire: {ligne['salaire']} EUR") print(f" Service: {ligne['service']}") print() +# --- Pourquoi le module csv (et pas split(',')) ? --- +# Sur un champ contenant une virgule (entre guillemets), split(',') se trompe : +print("=== csv.reader vs split(',') ===") +with open('villes.csv', 'w', encoding='utf-8', newline='') as f: + f.write('ville,population\n') + f.write('"Lyon, France",515000\n') # virgule DANS le champ ville + +with open('villes.csv', 'r', encoding='utf-8') as f: + next(f) # ignorer l'en-tête + ligne = next(f) + print("split(',') :", ligne.strip().split(',')) # 3 morceaux -> FAUX + +with open('villes.csv', 'r', encoding='utf-8') as f: + lecteur = csv.reader(f) + next(lecteur) + print("csv.reader :", next(lecteur)) # 2 champs -> correct + # Nettoyage os.remove('employes.csv') +os.remove('villes.csv') diff --git a/04-gestion-donnees-et-fichiers/exemples/02_06_csv_delimiteurs_filtrage.py b/04-gestion-donnees-et-fichiers/exemples/02_06_csv_delimiteurs_filtrage.py index 26634ec..de08668 100644 --- a/04-gestion-donnees-et-fichiers/exemples/02_06_csv_delimiteurs_filtrage.py +++ b/04-gestion-donnees-et-fichiers/exemples/02_06_csv_delimiteurs_filtrage.py @@ -57,6 +57,12 @@ with open('informaticiens.csv', 'r', encoding='utf-8') as f: print(f.read()) +# --- Calcul de statistiques : convertir les chaînes en nombres --- +# Toutes les valeurs CSV sont des chaînes : il faut convertir avec int() / float() +print("=== Statistiques (conversion int) ===") +masse_salariale = sum(int(e['salaire']) for e in informaticiens) +print(f"Masse salariale Informatique : {masse_salariale} EUR") # 73000 + # Nettoyage os.remove('donnees_fr.csv') os.remove('employes.csv') diff --git a/04-gestion-donnees-et-fichiers/exemples/03_01_pickle_base.py b/04-gestion-donnees-et-fichiers/exemples/03_01_pickle_base.py index 458493f..53b55ec 100644 --- a/04-gestion-donnees-et-fichiers/exemples/03_01_pickle_base.py +++ b/04-gestion-donnees-et-fichiers/exemples/03_01_pickle_base.py @@ -41,7 +41,7 @@ with open('utilisateur.pkl', 'rb') as fichier: utilisateur = pickle.load(fichier) print(f"Nom : {utilisateur['nom']}") -print(f"Age : {utilisateur['age']} ans") +print(f"Âge : {utilisateur['age']} ans") print(f"Compétences : {', '.join(utilisateur['competences'])}") # --- Sauvegarder et charger plusieurs objets --- diff --git a/04-gestion-donnees-et-fichiers/exemples/03_05_pickle_cache.py b/04-gestion-donnees-et-fichiers/exemples/03_05_pickle_cache.py index 3ca6c3f..5d52a70 100644 --- a/04-gestion-donnees-et-fichiers/exemples/03_05_pickle_cache.py +++ b/04-gestion-donnees-et-fichiers/exemples/03_05_pickle_cache.py @@ -11,8 +11,6 @@ def calcul_long(n): """Simule un calcul long (sans sleep pour l'exécution rapide)""" print(f"Calcul en cours pour n={n}...") - # Calcul réel au lieu de sleep - resultat = sum(range(n * 100000)) return n ** 2 def calcul_avec_cache(n, fichier_cache='cache.pkl'): diff --git a/04-gestion-donnees-et-fichiers/exemples/03_07_pickle_vs_json.py b/04-gestion-donnees-et-fichiers/exemples/03_07_pickle_vs_json.py index fd61140..f56f8e2 100644 --- a/04-gestion-donnees-et-fichiers/exemples/03_07_pickle_vs_json.py +++ b/04-gestion-donnees-et-fichiers/exemples/03_07_pickle_vs_json.py @@ -1,6 +1,6 @@ # ============================================================================ # Section 4.3 : Pickle vs JSON - Comparaison -# Description : Pickle gère tous les types Python (tuples, sets, bytes), +# Description : Pickle gère les types complexes (tuples, sets, bytes), # JSON ne supporte que les types de base # Fichier source : 03-serialisation-pickle.md # ============================================================================ @@ -17,7 +17,7 @@ 'bytes': b'data' # Bytes } -# Pickle : fonctionne avec tous les types +# Pickle : gère ces types complexes sans conversion with open('donnees.pkl', 'wb') as f: pickle.dump(donnees, f) print("Pickle : sauvegarde réussie") diff --git a/04-gestion-donnees-et-fichiers/exemples/03_08_pickle_limitations.py b/04-gestion-donnees-et-fichiers/exemples/03_08_pickle_limitations.py index 21e7a49..4732e01 100644 --- a/04-gestion-donnees-et-fichiers/exemples/03_08_pickle_limitations.py +++ b/04-gestion-donnees-et-fichiers/exemples/03_08_pickle_limitations.py @@ -30,8 +30,8 @@ try: pickle.dumps(ma_fonction) print("Lambda sérialisée (succès inattendu)") -except (AttributeError, pickle.PicklingError) as e: - print(f"Erreur : impossible de pickler une lambda") +except (AttributeError, pickle.PicklingError): + print("Erreur : impossible de pickler une lambda") # --- Fonctions réutilisables --- print("\n=== Fonctions réutilisables ===") diff --git a/04-gestion-donnees-et-fichiers/exemples/04_04_verifications.py b/04-gestion-donnees-et-fichiers/exemples/04_04_verifications.py index 1040222..ed93b74 100644 --- a/04-gestion-donnees-et-fichiers/exemples/04_04_verifications.py +++ b/04-gestion-donnees-et-fichiers/exemples/04_04_verifications.py @@ -49,11 +49,11 @@ def analyser_chemin(chemin_str): print("Le chemin existe") if chemin.is_file(): - print(f"Type : Fichier") + print("Type : Fichier") taille = chemin.stat().st_size print(f"Taille : {taille} octets") elif chemin.is_dir(): - print(f"Type : Dossier") + print("Type : Dossier") nb_fichiers = len(list(chemin.iterdir())) print(f"Nombre d'éléments : {nb_fichiers}") diff --git a/04-gestion-donnees-et-fichiers/exemples/04_05_operations_dossiers.py b/04-gestion-donnees-et-fichiers/exemples/04_05_operations_dossiers.py index 408b476..bdd4286 100644 --- a/04-gestion-donnees-et-fichiers/exemples/04_05_operations_dossiers.py +++ b/04-gestion-donnees-et-fichiers/exemples/04_05_operations_dossiers.py @@ -56,4 +56,4 @@ print(f"Dossier vide supprimé : {nouveau_dossier}") shutil.rmtree('projets') # Dossier non vide -print(f"Dossier avec contenu supprimé : projets/") +print("Dossier avec contenu supprimé : projets/") diff --git a/04-gestion-donnees-et-fichiers/exemples/04_07_infos_fichiers.py b/04-gestion-donnees-et-fichiers/exemples/04_07_infos_fichiers.py index 9f4c688..26eb9a0 100644 --- a/04-gestion-donnees-et-fichiers/exemples/04_07_infos_fichiers.py +++ b/04-gestion-donnees-et-fichiers/exemples/04_07_infos_fichiers.py @@ -48,9 +48,9 @@ def infos_fichier(chemin_str): print(f"{'='*60}") if chemin.is_file(): - print(f"Type : Fichier") + print("Type : Fichier") elif chemin.is_dir(): - print(f"Type : Dossier") + print("Type : Dossier") print(f"Chemin complet : {chemin.absolute()}") print(f"Dossier parent : {chemin.parent}") diff --git a/04-gestion-donnees-et-fichiers/exemples/04_10_backup_fichiers.py b/04-gestion-donnees-et-fichiers/exemples/04_10_backup_fichiers.py index 0360bb1..fb7e66e 100644 --- a/04-gestion-donnees-et-fichiers/exemples/04_10_backup_fichiers.py +++ b/04-gestion-donnees-et-fichiers/exemples/04_10_backup_fichiers.py @@ -25,7 +25,7 @@ def backup_fichiers(dossier_source, dossier_backup): # Créer le dossier de backup dossier_destination.mkdir(parents=True, exist_ok=True) - print(f"Backup en cours...") + print("Backup en cours...") print(f"Source : {source}") print(f"Destination : {dossier_destination}\n") diff --git a/04-gestion-donnees-et-fichiers/exemples/04_11_transformer_chemin.py b/04-gestion-donnees-et-fichiers/exemples/04_11_transformer_chemin.py new file mode 100644 index 0000000..fe64fd8 --- /dev/null +++ b/04-gestion-donnees-et-fichiers/exemples/04_11_transformer_chemin.py @@ -0,0 +1,30 @@ +# ============================================================================ +# Section 4.4 : Transformer un chemin (with_suffix, with_name, with_stem) +# Description : Dériver un nouveau Path en changeant l'extension, le nom +# complet ou le nom sans extension (sans modifier l'original) +# Fichier source : 04-gestion-chemins-pathlib.md +# ============================================================================ + +from pathlib import Path + +fichier = Path('rapports/donnees.csv') + +# --- with_suffix : changer l'extension --- +print("=== with_suffix / with_name / with_stem ===") +print(f"Original : {fichier}") +print(f"with_suffix('.json') : {fichier.with_suffix('.json')}") # rapports/donnees.json + +# --- with_name : changer le nom complet (nom + extension) --- +print(f"with_name('resume.txt') : {fichier.with_name('resume.txt')}") # rapports/resume.txt + +# --- with_stem : changer le nom sans toucher à l'extension (Python 3.9+) --- +print(f"with_stem('donnees_2024') : {fichier.with_stem('donnees_2024')}") # rapports/donnees_2024.csv + +# L'objet d'origine n'est pas modifié +print(f"Original inchangé : {fichier}") + +# --- Cas d'usage : fichier de sauvegarde dérivé --- +print("\n=== Cas d'usage : sauvegarde ===") +source = Path('document.txt') +sauvegarde = source.with_suffix('.bak') +print(f"{source} -> {sauvegarde}") # document.txt -> document.bak diff --git a/04-gestion-donnees-et-fichiers/exemples/04_12_shutil_copy_vs_copy2.py b/04-gestion-donnees-et-fichiers/exemples/04_12_shutil_copy_vs_copy2.py new file mode 100644 index 0000000..2a9ab82 --- /dev/null +++ b/04-gestion-donnees-et-fichiers/exemples/04_12_shutil_copy_vs_copy2.py @@ -0,0 +1,34 @@ +# ============================================================================ +# Section 4.4 : shutil.copy vs shutil.copy2 (préservation des métadonnées) +# Description : copy ne préserve pas les dates ; copy2 préserve la date de +# dernière modification (utile pour une sauvegarde fidèle) +# Fichier source : 04-gestion-chemins-pathlib.md +# ============================================================================ + +import os +import shutil +import time +from pathlib import Path + +# Créer un fichier source avec une date de modification ANCIENNE (~28h avant) +source = Path('source.txt') +source.write_text("contenu important\n", encoding='utf-8') +ancienne_date = time.time() - 100000 +os.utime(source, (ancienne_date, ancienne_date)) + +# copy : copie le contenu et les permissions, mais PAS la date de modification +copie_simple = Path('copie_simple.txt') +shutil.copy(source, copie_simple) + +# copy2 : copie le contenu ET préserve la date de modification +copie_fidele = Path('copie_fidele.txt') +shutil.copy2(source, copie_fidele) + +date_source = round(source.stat().st_mtime) +print("copy préserve la date ?", round(copie_simple.stat().st_mtime) == date_source) # False +print("copy2 préserve la date ?", round(copie_fidele.stat().st_mtime) == date_source) # True + +# Nettoyage +source.unlink() +copie_simple.unlink() +copie_fidele.unlink() diff --git a/04-gestion-donnees-et-fichiers/exemples/04_13_absolute_vs_resolve.py b/04-gestion-donnees-et-fichiers/exemples/04_13_absolute_vs_resolve.py new file mode 100644 index 0000000..2d16d6a --- /dev/null +++ b/04-gestion-donnees-et-fichiers/exemples/04_13_absolute_vs_resolve.py @@ -0,0 +1,17 @@ +# ============================================================================ +# Section 4.4 : absolute() vs resolve() - normalisation des chemins +# Description : absolute() préfixe le répertoire courant mais garde les '..' ; +# resolve() normalise (supprime les '..') et suit les liens +# Fichier source : 04-gestion-chemins-pathlib.md +# ============================================================================ + +from pathlib import Path + +chemin = Path('dossier/sousdossier/../fichier.txt') + +absolu = chemin.absolute() # préfixe le répertoire courant, sans simplifier +resolu = chemin.resolve() # normalise : supprime les '..' (et suit les liens) + +print("absolute() garde le '..' ?", '..' in absolu.parts) # True +print("resolve() garde le '..' ?", '..' in resolu.parts) # False +print("Même nom de fichier final ?", absolu.name == resolu.name == 'fichier.txt') # True diff --git a/04-gestion-donnees-et-fichiers/exemples/04_14_chemin_relatif_au_script.py b/04-gestion-donnees-et-fichiers/exemples/04_14_chemin_relatif_au_script.py new file mode 100644 index 0000000..6c95c1b --- /dev/null +++ b/04-gestion-donnees-et-fichiers/exemples/04_14_chemin_relatif_au_script.py @@ -0,0 +1,27 @@ +# ============================================================================ +# Section 4.4 : Localiser un fichier par rapport au script (__file__) +# Description : Path(__file__).parent vise le dossier du SCRIPT, pas le +# répertoire de lancement (cwd) -> chemins robustes vers +# les ressources livrées avec le programme +# Fichier source : 04-gestion-chemins-pathlib.md +# ============================================================================ + +from pathlib import Path + +# __file__ est le chemin de CE fichier .py ; .resolve().parent donne le +# dossier qui le contient, sous forme absolue et normalisée. +script = Path(__file__).resolve() +dossier_script = script.parent + +print("Nom de ce script :", script.name) + +# Construire un chemin vers un fichier VOISIN du script (à côté de lui). +# Peu importe d'où on lance la commande python, ce chemin reste correct. +config = dossier_script / 'config.json' +print("Fichier visé :", config.name) +print("Situé à côté du script ?", config.parent == dossier_script) # True + +# Le répertoire de travail (cwd) est celui d'où on lance la commande : +# il peut différer du dossier du script. C'est pourquoi un chemin relatif +# nu comme open('config.json') n'est PAS fiable -> on part de __file__. +print("cwd identique au dossier du script ?", Path.cwd() == dossier_script) diff --git a/04-gestion-donnees-et-fichiers/exemples/README.md b/04-gestion-donnees-et-fichiers/exemples/README.md index d71ad16..fdef408 100644 --- a/04-gestion-donnees-et-fichiers/exemples/README.md +++ b/04-gestion-donnees-et-fichiers/exemples/README.md @@ -1,8 +1,25 @@ # Exemples - Chapitre 04 : Gestion des données et fichiers -35 fichiers d'exemples exécutables, répartis sur 4 fichiers source. +41 fichiers d'exemples exécutables, répartis sur 4 fichiers source. -## Fichier 01 : Lecture et écriture de fichiers (7 fichiers) +**Convention de nommage** : `SS_NN_description.py`, où `SS` est le numéro de section (01 à 04) et `NN` l'ordre de l'exemple. Exemple : `02_04_csv_lecture.py` = section 4.2, 4ᵉ exemple. + +## Prérequis + +- **Python 3.10+** (le cours utilise la syntaxe moderne ; `ET.indent` requiert 3.9+). +- **Aucune dépendance externe** : uniquement la bibliothèque standard (`json`, `csv`, `xml.etree.ElementTree`, `pickle`, `pathlib`, `datetime`, `shutil`, `os`, `time`). + +## Correspondance avec le cours + +Chaque exemple reprend le code de son fichier `.md` source (indiqué sous chaque tableau). Pour rester **exécutables et autonomes**, les `.py` adaptent légèrement le cours : + +- ils sont **auto-contenus** : chaque fichier crée ses propres fichiers de test, puis les **supprime** en fin d'exécution (`os.remove`, `shutil.rmtree`) — aucun résidu sur le disque ; +- les **symboles décoratifs** du cours (émojis 📰/✅, `€`…) sont rendus en **ASCII** dans les sorties (`[F]`/`[D]`, `[ok]`, `>>`, `EUR`) pour un affichage portable sur tous les terminaux ; +- le délai simulé du cours (`time.sleep`) est retiré pour une exécution immédiate. + +La **logique et les valeurs** restent identiques à celles du cours. + +## Fichier 01 : Lecture et écriture de fichiers (9 fichiers) | Fichier | Section | Description | Sortie attendue | |---------|---------|-------------|-----------------| @@ -13,6 +30,8 @@ | `01_05_fichiers_binaires.py` | 4.1 | Écriture, lecture, copie, lecture par morceaux de binaires | 6 octets, copie, 1024+1024+512 octets | | `01_06_verifier_existence.py` | 4.1 | pathlib.Path exists(), is_file(), stat() | Existe, fichier, 16 octets | | `01_07_exemples_pratiques.py` | 4.1 | Compter mots, log, CSV simple, sauvegarder/relire liste | 11 mots, 3 logs, 3 lignes CSV, 4 noms | +| `01_08_positionnement_seek_tell.py` | 4.1 | `tell()`, `seek()`, mode `'r+'` | Position 0→3, seek, `12345FGHIJ` | +| `01_09_mode_x_creation_exclusive.py` | 4.1 | Mode `'x'` (création exclusive) | "Fichier créé", puis FileExistsError, contenu conservé | **Fichier source** : `01-lecture-ecriture-fichiers.md` @@ -21,11 +40,11 @@ | Fichier | Section | Description | Sortie attendue | |---------|---------|-------------|-----------------| | `02_01_json_lire_ecrire.py` | 4.2 | json.load() et json.dump() | Dupont, 28, compétences, fichier JSON indenté | -| `02_02_json_conversion.py` | 4.2 | json.dumps() et json.loads() sans fichier | JSON string, dict Python | +| `02_02_json_conversion.py` | 4.2 | json.dumps()/loads() + pièges de conversion | JSON string, dict, tuple→liste, clé→str, datetime→erreur | | `02_03_json_liste_erreurs.py` | 4.2 | Liste de personnes, gestion erreurs JSON | 3 personnes, FileNotFoundError, JSONDecodeError | -| `02_04_csv_lecture.py` | 4.2 | csv.reader() et csv.DictReader() | 3 employés avec colonnes nommées | +| `02_04_csv_lecture.py` | 4.2 | csv.reader(), csv.DictReader(), csv vs split(',') | 3 employés ; split casse sur une virgule dans un champ | | `02_05_csv_ecriture.py` | 4.2 | csv.writer() et csv.DictWriter() | Fichiers CSV avec en-têtes | -| `02_06_csv_delimiteurs_filtrage.py` | 4.2 | Délimiteur point-virgule, filtrage par service | 2 informaticiens exportés | +| `02_06_csv_delimiteurs_filtrage.py` | 4.2 | Délimiteur `;`, filtrage, calcul (conversion int) | 2 informaticiens, masse salariale 73000 | | `02_07_xml_lecture.py` | 4.2 | Parser un fichier XML avec ElementTree | 2 livres avec ID, titre, auteur, prix | | `02_08_xml_ecriture.py` | 4.2 | Construire et sauvegarder un arbre XML | XML indenté avec 2 livres | | `02_09_xml_modification_xpath.py` | 4.2 | Modifier XML, recherche XPath, parser RSS | Prix modifié, 1 livre 2024, 2 articles RSS | @@ -48,12 +67,12 @@ **Fichier source** : `03-serialisation-pickle.md` -## Fichier 04 : Gestion des chemins avec pathlib (10 fichiers) +## Fichier 04 : Gestion des chemins avec pathlib (14 fichiers) | Fichier | Section | Description | Sortie attendue | |---------|---------|-------------|-----------------| | `04_01_path_creation.py` | 4.4 | Créer des Path, opérateur /, joinpath() | Chemins construits progressivement | -| `04_02_proprietes_chemin.py` | 4.4 | name, stem, suffix, suffixes, parent, parts | script.py, .py, ['.tar','.gz'], parents | +| `04_02_proprietes_chemin.py` | 4.4 | name, stem, suffix, suffixes, parent, parts | script.py, .py, ['.tar','.gz'], parties du chemin | | `04_03_chemins_absolus_relatifs.py` | 4.4 | absolute(), resolve(), relative_to(), home(), cwd() | Chemins absolus, relatifs, spéciaux | | `04_04_verifications.py` | 4.4 | exists(), is_file(), is_dir(), analyse complète | Fichier 15 octets, dossier 0 éléments | | `04_05_operations_dossiers.py` | 4.4 | mkdir(), rename(), copy(), unlink(), rmtree() | Création, renommage, copie, suppression | @@ -62,6 +81,10 @@ | `04_08_lecture_ecriture_pathlib.py` | 4.4 | read_text(), write_text(), read_bytes(), traitement | Texte lu, 5 octets binaires, comptage lignes/mots | | `04_09_organiser_fichiers.py` | 4.4 | Organiser des fichiers par extension | 6 fichiers classés en jpg/, pdf/, py/, etc. | | `04_10_backup_fichiers.py` | 4.4 | Backup horodaté d'un dossier complet | 3 fichiers copiés avec structure préservée | +| `04_11_transformer_chemin.py` | 4.4 | `with_suffix()`, `with_name()`, `with_stem()` | donnees.json, resume.txt, donnees_2024.csv | +| `04_12_shutil_copy_vs_copy2.py` | 4.4 | `shutil.copy` vs `copy2` (préservation des métadonnées) | copy ne préserve pas la date (False), copy2 si (True) | +| `04_13_absolute_vs_resolve.py` | 4.4 | `absolute()` (garde `..`) vs `resolve()` (normalise) | absolute True, resolve False, même fichier final | +| `04_14_chemin_relatif_au_script.py` | 4.4 | `Path(__file__).parent` : viser un fichier relatif au script (pas au cwd) | nom du script, `config.json` à côté (True) | **Fichier source** : `04-gestion-chemins-pathlib.md` diff --git a/05-programmation-fonctionnelle/01-lambda-et-fonctions-ordre-superieur.md b/05-programmation-fonctionnelle/01-lambda-et-fonctions-ordre-superieur.md index 8843b2a..0e5fbbf 100644 --- a/05-programmation-fonctionnelle/01-lambda-et-fonctions-ordre-superieur.md +++ b/05-programmation-fonctionnelle/01-lambda-et-fonctions-ordre-superieur.md @@ -51,6 +51,8 @@ print(resultat) # Affiche : 8 Les deux approches donnent le même résultat, mais la lambda est plus concise. +> 💡 **Bonne pratique (PEP 8)** : ici on **nomme** la lambda (`additionner = lambda ...`) uniquement pour comparer les deux syntaxes. En réalité, **dès que vous donnez un nom à une fonction, préférez `def`** : c'est la recommandation de PEP 8 (les linters signalent d'ailleurs un avertissement `E731`). Tout l'intérêt d'une lambda est d'être **anonyme et utilisée sur place** — par exemple passée en argument à `sorted()`, `map()` ou `filter()`, comme dans les sections suivantes. + ### Exemples de fonctions lambda #### Exemple 1 : Doubler un nombre @@ -153,7 +155,7 @@ print(multiplier_par_5(7)) # Affiche : 35 print(multiplier_par_10(7)) # Affiche : 70 ``` -Ici, `creer_multiplicateur` retourne une fonction lambda différente selon la valeur de `n`. +Ici, `creer_multiplicateur` retourne une fonction lambda différente selon la valeur de `n`. La fonction retournée « se souvient » de `n` même après la fin de `creer_multiplicateur` : ce mécanisme s'appelle une **closure**, détaillée en [5.5 Closures](/05-programmation-fonctionnelle/05-closures-et-prog-fonctionnelle.md). ### Exemple pratique : Filtrer une liste diff --git a/05-programmation-fonctionnelle/02-map-filter-reduce.md b/05-programmation-fonctionnelle/02-map-filter-reduce.md index 7100c8e..186bcb1 100644 --- a/05-programmation-fonctionnelle/02-map-filter-reduce.md +++ b/05-programmation-fonctionnelle/02-map-filter-reduce.md @@ -369,6 +369,8 @@ phrase = reduce(lambda acc, mot: acc + " " + mot, mots, "Langage:") print(phrase) # Langage: Python est génial ``` +> ⚠️ La valeur initiale n'est pas qu'une commodité : **sans elle, `reduce()` sur une séquence vide lève une erreur** (`TypeError: reduce() of empty iterable with no initial value`). Avec une valeur initiale, `reduce(f, [], initiale)` retourne simplement `initiale`. Fournissez-la donc dès que la séquence peut être vide. + #### Exemple 4 : Compter les occurrences ```python @@ -690,6 +692,14 @@ doubles = map(lambda x: x * 2, nombres) # liste_doubles = list(doubles) # Ceci prendrait du temps ``` +> ⚠️ **Un itérateur `map`/`filter` ne se parcourt qu'une seule fois.** Une fois épuisé, il est vide — c'est un piège classique : +> ```python +> doubles = map(lambda x: x * 2, [1, 2, 3]) +> print(list(doubles)) # [2, 4, 6] +> print(list(doubles)) # [] — l'itérateur est déjà épuisé ! +> ``` +> Si vous devez parcourir le résultat **plusieurs fois**, convertissez-le d'abord en liste avec `list()`. + ### Quand utiliser list() ? ```python @@ -764,6 +774,49 @@ print(tous_pairs) # True --- +## Le module `operator` + +Le module `operator` fournit les opérateurs de Python sous forme de **fonctions**. C'est une alternative plus claire (et légèrement plus rapide) aux petites lambdas comme `lambda a, b: a + b`. + +```python +import operator +from functools import reduce + +# Au lieu de lambda acc, x: acc + x +somme = reduce(operator.add, [1, 2, 3, 4, 5]) # 15 + +# Au lieu de lambda acc, x: acc * x +produit = reduce(operator.mul, [2, 3, 4, 5]) # 120 +``` + +### itemgetter et attrgetter + +`operator.itemgetter` et `operator.attrgetter` remplacent élégamment les lambdas d'accès, par exemple comme clé de tri : + +```python +from operator import itemgetter + +personnes = [ + {"nom": "Alice", "age": 30}, + {"nom": "Bob", "age": 25}, + {"nom": "Charlie", "age": 35}, +] + +# Au lieu de key=lambda p: p["age"] +par_age = sorted(personnes, key=itemgetter("age")) +print([p["nom"] for p in par_age]) # ['Bob', 'Alice', 'Charlie'] + +# Extraire une "colonne" +ages = list(map(itemgetter("age"), personnes)) +print(ages) # [30, 25, 35] +``` + +`operator.attrgetter("age")` fait de même pour les **attributs** d'objets (ex. `key=attrgetter("age")` pour trier des instances de classe). + +> 💡 Pour le cas particulier du **produit**, Python fournit une fonction native depuis la 3.8 : `math.prod([2, 3, 4, 5])` donne `120`, sans avoir recours à `reduce`. + +--- + ## Bonnes pratiques ### 1. Privilégiez la lisibilité diff --git a/05-programmation-fonctionnelle/03-decorateurs-avances.md b/05-programmation-fonctionnelle/03-decorateurs-avances.md index 4fdef97..a63ddaf 100644 --- a/05-programmation-fonctionnelle/03-decorateurs-avances.md +++ b/05-programmation-fonctionnelle/03-decorateurs-avances.md @@ -14,6 +14,8 @@ Dans ce chapitre, nous allons explorer les décorateurs en profondeur, des conce Un **décorateur** est une fonction qui prend une autre fonction en paramètre, lui ajoute des fonctionnalités, et retourne la fonction modifiée. +> 📚 Les bases des décorateurs (syntaxe `@`, `@property`, `@staticmethod`/`@classmethod`) ont été introduites au chapitre [3.4 Propriétés et décorateurs](/03-programmation-orientee-objet/04-proprietes-et-decorateurs.md). Ce chapitre-ci approfondit le sujet : décorateurs **paramétrés**, décorateurs **de classe**, décorateurs **implémentés comme des classes**, et cas d'usage avancés. + ### Exemple simple sans décorateur ```python @@ -373,7 +375,7 @@ def retry(nombre_essais=3, delai=1): try: print(f"🔄 Tentative {tentative}/{nombre_essais}") resultat = fonction(*args, **kwargs) - print(f"✅ Succès !") + print("✅ Succès !") return resultat except Exception as e: print(f"❌ Erreur : {e}") @@ -455,6 +457,8 @@ resultat = calculer_factorielle(5) # ✅ fonction_modifiee a retourné 120 ``` +> 💡 **Pourquoi `fonction_modifiee` et non `calculer_factorielle` ?** `logger` est appliqué *au-dessus* de `mesurer_temps` : il décore donc le **wrapper** renvoyé par `mesurer_temps` (interne, nommé `fonction_modifiee`), et non la fonction d'origine — d'où ce nom dans ses messages. `mesurer_temps`, lui, décore directement `calculer_factorielle` et affiche le bon nom. C'est exactement le problème de perte d'identité que résout `functools.wraps`, présenté juste après. + --- ## Le module functools.wraps @@ -626,6 +630,8 @@ print(config1 is config2) # Affiche : True On peut aussi créer des décorateurs sous forme de classes : +> 📚 Le mécanisme clé ici est la méthode spéciale **`__call__`** : elle rend une **instance** de classe *appelable* comme une fonction — écrire `instance(...)` exécute en réalité `instance.__call__(...)`. C'est ce qui permet à un objet de se comporter comme un décorateur. Cette méthode est détaillée en [3.3 Méthodes spéciales](/03-programmation-orientee-objet/03-methodes-speciales.md). + ### Exemple basique ```python @@ -719,6 +725,8 @@ print(personne1) # {'nom': 'Alice', 'age': 30} # TypeError: age doit être de type int, pas str ``` +> ⚠️ **Limite de cet exemple : seuls les arguments passés par leur nom sont vérifiés.** Le wrapper n'inspecte que `kwargs` ; appelé en positionnel — `creer_personne("Bob", "trente")` — la validation est **contournée silencieusement**. Pour vérifier *tous* les arguments quelle que soit la manière de les passer, on relie positions et noms avec `inspect.signature(fonction).bind(*args, **kwargs)`, qui range chaque valeur sous son nom de paramètre. + ### 2. Rate limiting (limitation du taux d'appel) ```python diff --git a/05-programmation-fonctionnelle/04-generateurs.md b/05-programmation-fonctionnelle/04-generateurs.md index c5ae954..ea4be2f 100644 --- a/05-programmation-fonctionnelle/04-generateurs.md +++ b/05-programmation-fonctionnelle/04-generateurs.md @@ -86,6 +86,30 @@ print(next(gen)) # Lève : StopIteration ``` +### La valeur par défaut de `next()` + +Appeler `next()` sur un générateur épuisé lève `StopIteration`. Pour éviter cette erreur, on peut fournir une **valeur par défaut**, retournée à la place de l'exception : + +```python +gen = (x for x in [10, 20]) + +print(next(gen, None)) # 10 +print(next(gen, None)) # 20 +print(next(gen, None)) # None — épuisé, mais aucune erreur +``` + +C'est l'idiome idéal pour récupérer le **premier élément** correspondant à une condition, sans construire de liste intermédiaire : + +```python +nombres = [1, 3, 5, 8, 9, 10] + +# Premier nombre pair (ou None s'il n'y en a aucun) +premier_pair = next((x for x in nombres if x % 2 == 0), None) +print(premier_pair) # 8 +``` + +Comme l'expression génératrice est paresseuse, la recherche **s'arrête dès le premier élément trouvé** — bien plus efficace que `[x for x in nombres if x % 2 == 0][0]`, qui parcourt et stocke toute la liste (et lève `IndexError` si aucun élément ne correspond). + --- ## Créer des générateurs simples @@ -142,19 +166,23 @@ for nombre in fibonacci(10): **Avec une liste (tout en mémoire) :** ```python +import sys + def creer_grands_nombres(): """Crée une liste de 1 million de nombres.""" return [i for i in range(1000000)] # Crée une liste de 1 million d'éléments en mémoire liste = creer_grands_nombres() -print(f"Taille en mémoire : ~{liste.__sizeof__()} bytes") -# Taille en mémoire : ~8000000 bytes (environ 8 MB) +print(f"Taille de la liste : {sys.getsizeof(liste) / 1_000_000:.1f} Mo") +# environ 8 Mo : le million de valeurs est réellement stocké ``` **Avec un générateur (valeurs à la demande) :** ```python +import sys + def generer_grands_nombres(): """Génère 1 million de nombres à la demande.""" for i in range(1000000): @@ -162,8 +190,9 @@ def generer_grands_nombres(): # Ne crée qu'un objet générateur, pas les valeurs gen = generer_grands_nombres() -print(f"Taille en mémoire : ~{gen.__sizeof__()} bytes") -# Taille en mémoire : ~200 bytes +print(f"Taille du générateur : {sys.getsizeof(gen)} octets") +# ~100 à 200 octets selon la version : une taille fixe et minuscule, +# quelle que soit la quantité générée ``` ### 2. Évaluation paresseuse (lazy evaluation) @@ -590,10 +619,28 @@ gen = generateur_resilient() next(gen) # Démarrer gen.send(10) # Reçu : 10 -gen.throw(ValueError, "Une erreur") # Erreur ValueError capturée ! +gen.throw(ValueError("Une erreur")) # Erreur ValueError capturée ! gen.send(20) # Reçu : 20 ``` +### `return` dans un générateur + +Un générateur **peut** contenir `return` (l'introduction le contraste avec `yield`, mais les deux coexistent) : +- **`return` seul** arrête le générateur prématurément, comme une fin de fonction ; +- **`return valeur`** transmet une valeur finale, récupérable via `yield from`. + +```python +def generer_jusqu_a_negatif(nombres): + for n in nombres: + if n < 0: + return # arrête dès qu'on rencontre un nombre négatif + yield n + +print(list(generer_jusqu_a_negatif([1, 2, 3, -1, 4]))) # [1, 2, 3] +``` + +> 💡 La valeur d'un `return valeur` **n'apparaît pas** dans l'itération : elle est rangée dans l'exception `StopIteration` et n'est récupérée que par `yield from` (`resultat = yield from sous_generateur()`). + --- ## yield from - Délégation de générateurs @@ -781,6 +828,9 @@ nombres = [1, 4, 6, 4, 1] for x in itertools.dropwhile(lambda x: x < 5, nombres): print(x, end=" ") # 6 4 1 print() + +# accumulate() - Totaux cumulés (comme reduce, mais en gardant chaque étape) +print(list(itertools.accumulate([1, 2, 3, 4, 5]))) # [1, 3, 6, 10, 15] ``` --- @@ -854,13 +904,13 @@ def lire_fichier_securise_v2(nom_fichier): ```python # ❌ Inutile de convertir en liste si on itère une fois -nombres = (x ** 2 for x in range(1000)) -liste_nombres = list(nombres) # Consomme la mémoire +nombres = (x ** 2 for x in range(5)) +liste_nombres = list(nombres) # Matérialise tout en mémoire for n in liste_nombres: print(n) -# ✅ Mieux : itérer directement -nombres = (x ** 2 for x in range(1000)) +# ✅ Mieux : itérer directement (rien n'est stocké) +nombres = (x ** 2 for x in range(5)) for n in nombres: print(n) ``` diff --git a/05-programmation-fonctionnelle/05-closures-et-prog-fonctionnelle.md b/05-programmation-fonctionnelle/05-closures-et-prog-fonctionnelle.md index 6f049e8..9a62d7d 100644 --- a/05-programmation-fonctionnelle/05-closures-et-prog-fonctionnelle.md +++ b/05-programmation-fonctionnelle/05-closures-et-prog-fonctionnelle.md @@ -177,6 +177,37 @@ exemple_avec_nonlocal() --- +## ⚠️ Piège classique : la liaison tardive (*late binding*) + +Une closure capture la **variable**, pas sa **valeur** au moment de sa création. Cela produit un résultat surprenant quand on crée plusieurs closures dans une boucle : + +```python +# ❌ Piège : toutes les fonctions partagent la même variable i +fonctions = [lambda: i for i in range(3)] +print([f() for f in fonctions]) # [2, 2, 2] — et non [0, 1, 2] ! +``` + +Au moment où les lambdas sont **appelées**, la boucle est déjà terminée et `i` vaut `2` pour toutes. + +**Solution 1 — figer la valeur avec un argument par défaut :** + +```python +fonctions = [lambda i=i: i for i in range(3)] +print([f() for f in fonctions]) # [0, 1, 2] ✅ +``` + +**Solution 2 — une fabrique de fonctions (chaque appel crée une nouvelle portée) :** + +```python +def faire(i): + return lambda: i + +fonctions = [faire(i) for i in range(3)] +print([f() for f in fonctions]) # [0, 1, 2] ✅ +``` + +--- + ## Cas d'usage pratiques des closures ### 1. Fabrique de fonctions @@ -507,13 +538,13 @@ dictionnaire['b'] = 2 # Modifie le dictionnaire original # ❌ Style mutable (modifie en place) def augmenter_prix_mutable(produits, pourcentage): for produit in produits: - produit['prix'] *= (1 + pourcentage / 100) + produit['prix'] = round(produit['prix'] * (1 + pourcentage / 100), 2) return produits # ✅ Style immuable (crée de nouveaux objets) def augmenter_prix_immuable(produits, pourcentage): return [ - {**produit, 'prix': produit['prix'] * (1 + pourcentage / 100)} + {**produit, 'prix': round(produit['prix'] * (1 + pourcentage / 100), 2)} for produit in produits ] @@ -529,6 +560,8 @@ print("Originaux :", produits_originaux) # Non modifiés print("Nouveaux :", nouveaux_produits) # Prix augmentés de 10% ``` +> 💡 **L'idiome `{**ancien, 'clé': valeur}`** crée un **nouveau** dictionnaire : il copie toutes les paires de `ancien` (le `**` les *déballe*), puis ajoute ou remplace une clé — **sans toucher** à l'original. C'est l'outil de base pour manipuler des dictionnaires de façon immuable. La syntaxe de déballage `**` est présentée au chapitre [2.1 Listes, tuples, dictionnaires et sets](/02-structures-de-donnees/01-listes-tuples-dicts-sets.md). + ### Créer des copies ```python @@ -683,6 +716,8 @@ print(creer_url_https("example.com", "api/users")) # https://example.com/api/us print(creer_url_site("products")) # https://example.com/products ``` +> 📝 **Curryfication ou application partielle ?** À proprement parler, `curryfier` ci-dessus réalise une **application partielle** : on fixe certains arguments, puis on appelle la fonction avec les arguments *restants groupés*. Ce n'est pas tout à fait la *curryfication* au sens strict, qui transformerait `f(a, b, c)` en `f(a)(b)(c)` — un seul argument à la fois. Les deux notions sont proches et souvent confondues. En pratique, Python fournit directement l'outil adapté à l'application partielle : **`functools.partial`**, présenté ci-dessous (inutile de réécrire `curryfier`). + --- ## Application partielle avec functools.partial @@ -1052,7 +1087,7 @@ Dans ce chapitre, nous avons exploré les closures et la programmation fonctionn **Techniques** : - map(), filter(), reduce() - Compréhensions de listes -- functools.partial pour curryfication +- functools.partial pour l'application partielle - Composition de fonctions - Récursion diff --git a/05-programmation-fonctionnelle/README.md b/05-programmation-fonctionnelle/README.md index 7f9cd10..3afc405 100644 --- a/05-programmation-fonctionnelle/README.md +++ b/05-programmation-fonctionnelle/README.md @@ -285,13 +285,14 @@ Contrairement à des langages comme Haskell ou Elixir, Python est **multi-paradi Python fournit de nombreux outils pour la programmation fonctionnelle : -**Fonctions natives** : +**Fonctions natives (*built-in*)** : - `map()` : applique une fonction à chaque élément - `filter()` : filtre les éléments selon une condition -- `reduce()` : réduit une séquence à une valeur unique - `zip()` : combine plusieurs séquences - `enumerate()` : itère avec des indices +> ℹ️ `reduce()` (réduire une séquence à une valeur unique) était native en Python 2, mais se trouve désormais dans le module **`functools`** (`from functools import reduce`) — voir la section 5.2. + **Fonctionnalités du langage** : - Fonctions lambda (anonymes) - Compréhensions de listes diff --git a/05-programmation-fonctionnelle/exemples/02_03_reduce_base.py b/05-programmation-fonctionnelle/exemples/02_03_reduce_base.py index 6fc4ebf..aea4b3e 100644 --- a/05-programmation-fonctionnelle/exemples/02_03_reduce_base.py +++ b/05-programmation-fonctionnelle/exemples/02_03_reduce_base.py @@ -93,3 +93,12 @@ def factorielle(n): print(f"5! = {factorielle(5)}") # 5! = 120 print(f"7! = {factorielle(7)}") # 7! = 5040 + +# --- Piège : reduce() sur une séquence vide --- +print("\n=== Séquence vide ===") +try: + reduce(lambda acc, x: acc + x, []) +except TypeError as e: + print(f"Erreur : {e}") # reduce() of empty iterable with no initial value +# Avec une valeur initiale, aucun problème : +print(reduce(lambda acc, x: acc + x, [], 0)) # 0 diff --git a/05-programmation-fonctionnelle/exemples/02_08_module_operator.py b/05-programmation-fonctionnelle/exemples/02_08_module_operator.py new file mode 100644 index 0000000..52208e1 --- /dev/null +++ b/05-programmation-fonctionnelle/exemples/02_08_module_operator.py @@ -0,0 +1,39 @@ +# ============================================================================ +# Section 5.2 : Le module operator +# Description : operator.add/mul (alternative aux lambdas), itemgetter et +# attrgetter, math.prod (produit natif, Python 3.8+) +# Fichier source : 02-map-filter-reduce.md +# ============================================================================ + +import operator +import math +from functools import reduce +from operator import itemgetter, attrgetter +from collections import namedtuple + +# --- operator.add / operator.mul au lieu de lambda --- +print("=== operator avec reduce ===") +print(reduce(operator.add, [1, 2, 3, 4, 5])) # 15 +print(reduce(operator.mul, [2, 3, 4, 5])) # 120 + +# --- itemgetter : clé de tri et extraction de "colonne" --- +print("\n=== itemgetter ===") +personnes = [ + {"nom": "Alice", "age": 30}, + {"nom": "Bob", "age": 25}, + {"nom": "Charlie", "age": 35}, +] +par_age = sorted(personnes, key=itemgetter("age")) +print([p["nom"] for p in par_age]) # ['Bob', 'Alice', 'Charlie'] +print(list(map(itemgetter("age"), personnes))) # [30, 25, 35] + +# --- attrgetter : pour les attributs d'objets --- +print("\n=== attrgetter ===") +Personne = namedtuple("Personne", ["nom", "age"]) +gens = [Personne("Alice", 30), Personne("Bob", 25)] +print([p.nom for p in sorted(gens, key=attrgetter("age"))]) # ['Bob', 'Alice'] + +# --- math.prod : produit natif (Python 3.8+) --- +print("\n=== math.prod ===") +print(math.prod([2, 3, 4, 5])) # 120 +print(math.prod([])) # 1 (élément neutre du produit) diff --git a/05-programmation-fonctionnelle/exemples/03_05_retry_empiler.py b/05-programmation-fonctionnelle/exemples/03_05_retry_empiler.py index 7c8fd2a..ccbd326 100644 --- a/05-programmation-fonctionnelle/exemples/03_05_retry_empiler.py +++ b/05-programmation-fonctionnelle/exemples/03_05_retry_empiler.py @@ -16,12 +16,12 @@ def fonction_modifiee(*args, **kwargs): try: print(f"[retry] Tentative {tentative}/{nombre_essais}") resultat = fonction(*args, **kwargs) - print(f"[ok] Succès !") + print("[ok] Succès !") return resultat except Exception as e: print(f"[erreur] Erreur : {e}") if tentative < nombre_essais: - print(f"[attente] Nouvel essai...") + print("[attente] Nouvel essai...") else: print(f"[echec] Échec après {nombre_essais} tentatives") raise diff --git a/05-programmation-fonctionnelle/exemples/04_03_avantages_generateurs.py b/05-programmation-fonctionnelle/exemples/04_03_avantages_generateurs.py index 6528309..f536da5 100644 --- a/05-programmation-fonctionnelle/exemples/04_03_avantages_generateurs.py +++ b/05-programmation-fonctionnelle/exemples/04_03_avantages_generateurs.py @@ -5,6 +5,8 @@ # Fichier source : 04-generateurs.md # ============================================================================ +import sys + # --- Économie de mémoire --- print("=== Économie de mémoire ===") @@ -14,7 +16,7 @@ def creer_grands_nombres(): return [i for i in range(1000000)] liste = creer_grands_nombres() -print(f"Liste - Taille en mémoire : ~{liste.__sizeof__()} bytes") +print(f"Liste - taille : {sys.getsizeof(liste) / 1_000_000:.1f} Mo") # Avec un générateur (valeurs à la demande) def generer_grands_nombres(): @@ -23,7 +25,7 @@ def generer_grands_nombres(): yield i gen = generer_grands_nombres() -print(f"Générateur - Taille en mémoire : ~{gen.__sizeof__()} bytes") +print(f"Générateur - taille : {sys.getsizeof(gen)} octets") # --- Évaluation paresseuse --- print("\n=== Évaluation paresseuse ===") diff --git a/05-programmation-fonctionnelle/exemples/04_08_methodes_avancees.py b/05-programmation-fonctionnelle/exemples/04_08_methodes_avancees.py index fbc9215..5d08e60 100644 --- a/05-programmation-fonctionnelle/exemples/04_08_methodes_avancees.py +++ b/05-programmation-fonctionnelle/exemples/04_08_methodes_avancees.py @@ -63,3 +63,27 @@ def generateur_resilient(): gen.send(10) # Reçu : 10 gen.throw(ValueError("Une erreur")) # Erreur ValueError capturée ! gen.send(20) # Reçu : 20 + +# --- return dans un générateur --- +print("\n=== return dans un générateur ===") + +# return seul : arrête le générateur prématurément +def generer_jusqu_a_negatif(nombres): + for n in nombres: + if n < 0: + return # stoppe dès qu'on rencontre un négatif + yield n + +print(list(generer_jusqu_a_negatif([1, 2, 3, -1, 4]))) # [1, 2, 3] + +# return valeur : récupéré par yield from +def sous_generateur(): + yield "a" + yield "b" + return "FINI" + +def delegue(): + resultat = yield from sous_generateur() + print(f"Valeur de return : {resultat}") + +list(delegue()) # affiche : Valeur de return : FINI diff --git a/05-programmation-fonctionnelle/exemples/04_11_itertools.py b/05-programmation-fonctionnelle/exemples/04_11_itertools.py index 27fe3f7..89b1103 100644 --- a/05-programmation-fonctionnelle/exemples/04_11_itertools.py +++ b/05-programmation-fonctionnelle/exemples/04_11_itertools.py @@ -62,3 +62,7 @@ for x in itertools.dropwhile(lambda x: x < 5, nombres): print(x, end=" ") # 6 4 1 print() + +# --- accumulate() : totaux cumulés (comme reduce, mais en gardant chaque étape) --- +print("\n=== accumulate() ===") +print(list(itertools.accumulate([1, 2, 3, 4, 5]))) # [1, 3, 6, 10, 15] diff --git a/05-programmation-fonctionnelle/exemples/04_12_next_avec_defaut.py b/05-programmation-fonctionnelle/exemples/04_12_next_avec_defaut.py new file mode 100644 index 0000000..40416a7 --- /dev/null +++ b/05-programmation-fonctionnelle/exemples/04_12_next_avec_defaut.py @@ -0,0 +1,28 @@ +# ============================================================================ +# Section 5.4 : La valeur par défaut de next() +# Description : next(gen, défaut) évite StopIteration ; idiome du "premier +# élément correspondant" avec une expression génératrice +# Fichier source : 04-generateurs.md +# ============================================================================ + +# --- next() avec valeur par défaut : pas de StopIteration --- +print("=== next(gen, défaut) ===") +gen = (x for x in [10, 20]) +print(next(gen, None)) # 10 +print(next(gen, None)) # 20 +print(next(gen, None)) # None -- épuisé, mais aucune erreur + +# --- Idiome : premier élément correspondant à une condition --- +print("\n=== Premier élément correspondant ===") +nombres = [1, 3, 5, 8, 9, 10] + +# Premier nombre pair (ou None s'il n'y en a aucun) +premier_pair = next((x for x in nombres if x % 2 == 0), None) +print("Premier pair :", premier_pair) # 8 + +# Aucun élément ne correspond -> on récupère la valeur par défaut +aucun = next((x for x in nombres if x > 100), None) +print("Aucun match :", aucun) # None + +# La recherche est paresseuse : elle s'arrête dès le premier élément trouvé, +# contrairement à [x for x in nombres if ...][0] qui parcourt toute la liste. diff --git a/05-programmation-fonctionnelle/exemples/05_05_fonctions_pures_immutabilite.py b/05-programmation-fonctionnelle/exemples/05_05_fonctions_pures_immutabilite.py index e8a7b74..e721760 100644 --- a/05-programmation-fonctionnelle/exemples/05_05_fonctions_pures_immutabilite.py +++ b/05-programmation-fonctionnelle/exemples/05_05_fonctions_pures_immutabilite.py @@ -41,7 +41,7 @@ def ajouter_pure(liste, element): def augmenter_prix_immuable(produits, pourcentage): return [ - {**produit, 'prix': produit['prix'] * (1 + pourcentage / 100)} + {**produit, 'prix': round(produit['prix'] * (1 + pourcentage / 100), 2)} for produit in produits ] diff --git a/05-programmation-fonctionnelle/exemples/05_11_late_binding.py b/05-programmation-fonctionnelle/exemples/05_11_late_binding.py new file mode 100644 index 0000000..9416514 --- /dev/null +++ b/05-programmation-fonctionnelle/exemples/05_11_late_binding.py @@ -0,0 +1,25 @@ +# ============================================================================ +# Section 5.5 : Piège de la liaison tardive (late binding) +# Description : Une closure capture la variable, pas sa valeur ; conséquence +# dans une boucle, et les deux solutions classiques +# Fichier source : 05-closures-et-prog-fonctionnelle.md +# ============================================================================ + +# --- Le piège : toutes les closures partagent la même variable i --- +print("=== Piège (liaison tardive) ===") +fonctions = [lambda: i for i in range(3)] +print([f() for f in fonctions]) # [2, 2, 2] -- et non [0, 1, 2] ! +# Au moment de l'appel, la boucle est finie et i vaut 2 pour toutes. + +# --- Solution 1 : figer la valeur avec un argument par défaut --- +print("\n=== Solution 1 : argument par défaut ===") +fonctions = [lambda i=i: i for i in range(3)] +print([f() for f in fonctions]) # [0, 1, 2] + +# --- Solution 2 : une fabrique (chaque appel crée une nouvelle portée) --- +print("\n=== Solution 2 : fabrique de fonctions ===") +def faire(i): + return lambda: i + +fonctions = [faire(i) for i in range(3)] +print([f() for f in fonctions]) # [0, 1, 2] diff --git a/05-programmation-fonctionnelle/exemples/README.md b/05-programmation-fonctionnelle/exemples/README.md index 8306c28..1314555 100644 --- a/05-programmation-fonctionnelle/exemples/README.md +++ b/05-programmation-fonctionnelle/exemples/README.md @@ -1,6 +1,22 @@ # Exemples - Chapitre 05 : Programmation fonctionnelle -40 fichiers d'exemples exécutables, répartis sur 5 fichiers source. +43 fichiers d'exemples exécutables, répartis sur 5 fichiers source. + +**Convention de nommage** : `SS_NN_description.py`, où `SS` est le numéro de section (01 à 05) et `NN` l'ordre de l'exemple. Exemple : `03_06_functools_wraps.py` = section 5.3, 6ᵉ exemple. + +## Prérequis + +- **Python 3.10+** (le cours utilise la syntaxe moderne ; `functools.cache` requiert 3.9+). +- **Aucune dépendance externe** : uniquement la bibliothèque standard (`functools`, `itertools`, `collections`, `copy`, `time`). + +## Correspondance avec le cours + +Chaque exemple reprend le code de son fichier `.md` source (indiqué sous chaque tableau). Pour rester **exécutables et lisibles dans un terminal**, les `.py` adaptent légèrement le cours : + +- les **symboles décoratifs** du cours (émojis ⏱️/📝/✅, `€`, flèches `→`…) sont rendus en **ASCII** dans les sorties (texte simple, `EUR`, `->`, `n.`) pour un affichage portable partout ; +- les calculs volontairement longs (`time.sleep`) sont parfois remplacés par un calcul rapide équivalent. + +La **logique et les valeurs** restent identiques à celles du cours. ## Fichier 01 : Lambda et fonctions d'ordre supérieur (3 fichiers) @@ -12,17 +28,18 @@ **Fichier source** : `01-lambda-et-fonctions-ordre-superieur.md` -## Fichier 02 : map(), filter(), reduce() (7 fichiers) +## Fichier 02 : map(), filter(), reduce() (8 fichiers) | Fichier | Section | Description | Sortie attendue | |---------|---------|-------------|-----------------| | `02_01_map_base.py` | 5.2 | Doubler (boucle vs map), températures, majuscules, longueurs, formatter, plusieurs itérables | [2,4,6,8,10], Fahrenheit [32..104], PYTHON/JAVASCRIPT, [7,20,6,13], Alice/Bob/Charlie | | `02_02_filter_base.py` | 5.2 | Filtrer pairs, positifs, chaînes longues, dictionnaires, non vides, adultes | [2,4,6,8,10], [3,8,7], éléphant/oiseau/papillon, Pomme/Banane, Alice/Charlie | -| `02_03_reduce_base.py` | 5.2 | Somme, produit, maximum, concaténer, occurrences, aplatir, factorielle | 15, 120, 89, Python est génial, {pomme:3...}, [1..8], 5!=120 | +| `02_03_reduce_base.py` | 5.2 | Somme, produit, max, concat, occurrences, aplatir, factorielle, séquence vide | 15, 120, 89, {pomme:3...}, 5!=120, `reduce([])`→erreur | | `02_04_combiner_map_filter_reduce.py` | 5.2 | Somme carrés des pairs, prix total promo, moyenne notes > 10 | 220, 1175EUR, 14.33 | | `02_05_comprehensions_vs_fonctionnel.py` | 5.2 | map() vs compréhension, filter() vs compréhension, combiné | [2,4,6,8,10], [4,16,36,64,100] | | `02_06_cas_usage_avances.py` | 5.2 | Pipeline de ventes, étudiants avec mentions, analyse de texte | Laptop 1600EUR, Alice/Charlie >=15, 11 mots | | `02_07_alternatives_bonnes_pratiques.py` | 5.2 | sum() vs reduce(), max() vs reduce(), all(), lisibilité, fonctions nommées | 15, 89, True, [4,8,12,16,20], prix TTC | +| `02_08_module_operator.py` | 5.2 | `operator.add`/`mul`, `itemgetter`/`attrgetter`, `math.prod` | 15, 120, ['Bob','Alice','Charlie'], [30,25,35], prod 120 | **Fichier source** : `02-map-filter-reduce.md` @@ -42,25 +59,26 @@ **Fichier source** : `03-decorateurs-avances.md` -## Fichier 04 : Générateurs et expressions génératrices (11 fichiers) +## Fichier 04 : Générateurs et expressions génératrices (12 fichiers) | Fichier | Section | Description | Sortie attendue | |---------|---------|-------------|-----------------| | `04_01_generateur_base.py` | 5.4 | Fonction normale vs générateur, yield, next(), StopIteration | [0,1,2,3,4], generator object, 1/2/3 | | `04_02_generateurs_simples.py` | 5.4 | Carrés, nombres pairs, Fibonacci | 0 1 4 9 16, [2,4,6,8,10], 0 1 1 2 3 5 8 13 21 34 | -| `04_03_avantages_generateurs.py` | 5.4 | Économie mémoire, évaluation paresseuse, séquence infinie | ~8MB vs ~176 bytes, traitement à la demande, 10..19 | +| `04_03_avantages_generateurs.py` | 5.4 | Économie mémoire, évaluation paresseuse, séquence infinie | 8.4 Mo vs ~100-200 octets, traitement à la demande, 10..19 | | `04_04_expressions_generatrices.py` | 5.4 | Syntaxe compacte, comparaison, filtrage, somme, chaîner | [0,1,4,9,16], [4,8,12,16,20], [64,100,144,196,256,324] | | `04_05_fonctions_natives.py` | 5.4 | map(), filter(), zip(), enumerate(), reversed() | Doubles, pairs, Alice/Bob/Charlie, 1.pomme, 5 4 3 2 1 | | `04_06_cas_usage_pratiques.py` | 5.4 | Lecture fichier, pagination, pipeline, données de test | Lignes erreur, 5 pages, [20,40,60,80,100], 5 utilisateurs | | `04_07_generateurs_infinis.py` | 5.4 | Compteur infini avec pas, cycle, répétition | 10 12 14 16 18, rouge vert bleu..., Python x5 | -| `04_08_methodes_avancees.py` | 5.4 | send(), close(), throw() | Total 0/10/15/18, fermé, ValueError capturée | +| `04_08_methodes_avancees.py` | 5.4 | send(), close(), throw(), `return` dans un générateur | Total 0/10/15/18, fermé, capturée, [1,2,3] + return via yield from | | `04_09_yield_from.py` | 5.4 | Combiner générateurs, aplatir listes, parcourir arbre | [1,2,3,4], [1..9], [1,2,4,5,3] | -| `04_10_comparaison_performance.py` | 5.4 | Mémoire liste vs générateur, épuisement, erreurs courantes | 8 MB vs 0.19 KB, [] après épuisement, TypeError | -| `04_11_itertools.py` | 5.4 | count(), cycle(), repeat(), chain(), islice(), takewhile(), dropwhile() | 10 12 14 16 18, R G B..., 5 6 7 8 9, 1 4, 6 4 1 | +| `04_10_comparaison_performance.py` | 5.4 | Mémoire liste vs générateur, épuisement, erreurs courantes | 8 MB vs ~0.1-0.2 KB, [] après épuisement, TypeError | +| `04_11_itertools.py` | 5.4 | count(), cycle(), repeat(), chain(), islice(), takewhile(), dropwhile(), accumulate() | 10 12 14 16 18, R G B..., 1 4, 6 4 1, [1,3,6,10,15] | +| `04_12_next_avec_defaut.py` | 5.4 | `next(gen, defaut)` (évite StopIteration) ; premier élément correspondant | 10, 20, None ; premier pair 8 ; aucun match None | **Fichier source** : `04-generateurs.md` -## Fichier 05 : Closures et programmation fonctionnelle (10 fichiers) +## Fichier 05 : Closures et programmation fonctionnelle (11 fichiers) | Fichier | Section | Description | Sortie attendue | |---------|---------|-------------|-----------------| @@ -74,6 +92,7 @@ | `05_08_recursion.py` | 5.5 | Factorielle, Fibonacci, somme récursive, tail recursion | 120, [0,1,1,2,3,5,8,13,21,34], 15, 120 | | `05_09_techniques_fonctionnelles.py` | 5.5 | namedtuple, prédicats, testabilité, traitement lisible | Point(3,4), [2,4], 120 TTC, ['java','python','ruby'] | | `05_10_exemple_complet.py` | 5.5 | Pipeline étudiants : moyenne, filtrer admis, extraire noms | Alice/Bob/Charlie admis, David refusé | +| `05_11_late_binding.py` | 5.5 | Piège de la liaison tardive et ses 2 solutions | `[lambda: i …]`→[2,2,2] ; corrigé→[0,1,2] | **Fichier source** : `05-closures-et-prog-fonctionnelle.md` @@ -86,7 +105,7 @@ python3 01_01_lambda_base.py python3 05_10_exemple_complet.py ``` -Tous les fichiers créent et nettoient leurs fichiers temporaires automatiquement. +La plupart des exemples ne réalisent que des calculs en mémoire. Le seul qui crée un fichier temporaire (`04_06`, pour illustrer la lecture de fichier) le supprime automatiquement après usage. Pour exécuter tous les fichiers et vérifier qu'il n'y a pas d'erreur : diff --git a/06-modules-et-packages/01-importation-et-creation-modules.md b/06-modules-et-packages/01-importation-et-creation-modules.md index 47d9ed6..790b417 100644 --- a/06-modules-et-packages/01-importation-et-creation-modules.md +++ b/06-modules-et-packages/01-importation-et-creation-modules.md @@ -127,6 +127,33 @@ print(email.group()) --- +## Explorer un module : la fonction `dir()` + +La fonction intégrée `dir()` permet de **découvrir les noms définis dans un module** (fonctions, classes, variables). Elle renvoie une liste triée de chaînes : + +```python +import math + +print(dir(math)) +# ['__doc__', '__loader__', '__name__', '__package__', ..., 'cos', 'pi', 'sqrt', 'tan', ...] +``` + +C'est très pratique pour explorer un module que vous connaissez mal, directement depuis l'interpréteur. Appelée **sans argument**, `dir()` liste les noms définis dans le contexte courant : + +```python +import math +a = 5 + +print(dir()) +# [..., 'a', 'math'] (vos variables et imports du moment) +``` + +> 📝 `dir()` ne liste pas les fonctions intégrées comme `print()` ou `len()` : celles-ci sont définies dans le module standard `builtins` (essayez `import builtins; dir(builtins)`). + +> 📝 **Complément : `help()`** — là où `dir()` liste les *noms* d'un module, la fonction intégrée `help()` en affiche la **documentation** (issue des docstrings) : `help(math.sqrt)` montre la description et la signature d'une fonction, `help(math)` celle du module entier. Appelée sans argument, `help()` ouvre un mode interactif (tapez `q` pour quitter). C'est l'outil idéal pour explorer un module inconnu directement depuis l'interpréteur. + +--- + ## Création de vos propres modules ### Module simple @@ -270,6 +297,29 @@ print(resultat) # Affiche : 100 --- +## Le dossier `__pycache__` + +Lorsque vous importez un module pour la première fois, Python compile son code en **bytecode** et le met en cache dans un dossier `__pycache__/`, sous un nom comme `operations.cpython-312.pyc` (le suffixe correspond à la version de Python). Au prochain import, si le fichier `.py` n'a pas changé, Python réutilise ce `.pyc` : le démarrage est plus rapide. + +``` +mon_projet/ + operations.py + __pycache__/ + operations.cpython-312.pyc +``` + +Ce dossier est **généré automatiquement** : vous n'avez jamais à le modifier ni à le partager. On l'ignore donc dans Git (voir la section 6.4) : + +``` +# .gitignore +__pycache__/ +*.pyc +``` + +> 📝 Le bytecode est créé lors de l'**import** d'un module, pas lors de l'exécution directe d'un script : lancer `python operations.py` ne crée pas de `.pyc` pour `operations.py` lui-même. + +--- + ## Organisation des modules ### Structure recommandée d'un module @@ -393,6 +443,8 @@ sys.path.append('/chemin/vers/mes/modules') import mon_module ``` +> 📝 **L'erreur `ModuleNotFoundError` ?** Si Python affiche `ModuleNotFoundError: No module named 'mon_module'`, c'est que le module est introuvable dans `sys.path`. Les causes les plus fréquentes sont une **faute de frappe** dans le nom, ou un terminal lancé depuis un **autre dossier** que celui du module. Vérifiez d'abord le nom et le répertoire courant ; n'ajoutez un chemin à `sys.path` qu'en dernier recours. + --- ## Bonnes pratiques pour les modules @@ -460,7 +512,23 @@ def fonction_b(): return fonction_a() ``` -Solution : Restructurer le code ou importer localement dans les fonctions. +**Pourquoi cela échoue ?** En important `module_a`, Python commence à l'exécuter et l'enregistre aussitôt comme « en cours d'initialisation ». Dès sa première ligne, `module_a` importe `module_b`, qui fait à son tour `from module_a import fonction_a`. Mais `module_a` n'a pas fini de se charger — `fonction_a` n'existe pas encore. Python lève alors : + +``` +ImportError: cannot import name 'fonction_a' from partially initialized module 'module_a' (most likely due to a circular import) +``` + +**Deux solutions :** + +- **Restructurer** : extraire le code partagé dans un troisième module dont `module_a` et `module_b` dépendent tous les deux (sans se dépendre l'un l'autre). +- **Importer localement** : placer l'import *à l'intérieur* de la fonction qui s'en sert. Il est alors **différé** jusqu'à l'appel, quand les deux modules sont entièrement chargés : + +```python +# module_b.py +def fonction_b(): + from module_a import fonction_a # importé à l'appel, plus au chargement + return fonction_a() +``` ### 4. Utiliser des imports absolus @@ -497,7 +565,7 @@ from mon_projet.utils import helpers ## Recharger un module -Pendant le développement, si vous modifiez un module déjà importé, vous devez le recharger : +Pendant le développement, si vous modifiez un module déjà importé, vous devez le recharger. **Pourquoi ?** Pour des raisons d'efficacité, Python n'importe chaque module qu'**une seule fois par session** : il conserve les modules déjà chargés dans `sys.modules` et réutilise cette version en cache lors des imports suivants. Un nouvel `import` ne relit donc pas le fichier modifié — il faut forcer le rechargement : ```python import mon_module diff --git a/06-modules-et-packages/02-structure-des-packages.md b/06-modules-et-packages/02-structure-des-packages.md index 65488f5..8c1bc4c 100644 --- a/06-modules-et-packages/02-structure-des-packages.md +++ b/06-modules-et-packages/02-structure-des-packages.md @@ -334,8 +334,8 @@ __all__ = [ __version__ = "1.0.0" # Faciliter l'accès aux sous-packages -from . import texte -from . import fichiers +from . import texte +# from . import fichiers # à ajouter de la même façon, une fois le sous-package fichiers/ créé ``` ### Utilisation des sous-packages @@ -375,9 +375,11 @@ print(resultat) # "trop d'espaces" ### Syntaxe des imports relatifs -- `.` : Répertoire courant -- `..` : Répertoire parent -- `...` : Deux niveaux au-dessus +- `.` : le **package courant** (celui qui contient le module) +- `..` : le **package parent** (un niveau au-dessus) +- `...` : le **package grand-parent** (deux niveaux au-dessus) + +> 📝 Les points désignent la hiérarchie de **packages**, pas les dossiers du système de fichiers. Un import relatif n'a donc de sens qu'à l'intérieur d'un package — d'où le piège décrit plus bas lorsqu'on exécute un module directement. ### Exemple pratique @@ -468,6 +470,29 @@ def lancer_application(): - Pour importer des packages externes - Si cela rend le code moins lisible +### Le piège classique : exécuter directement un module à imports relatifs + +C'est l'erreur la plus fréquente avec les packages. Si vous lancez **directement** un fichier qui contient un import relatif : + +```bash +python application/core/moteur.py +``` + +Python lève l'erreur : +``` +ImportError: attempted relative import with no known parent package +``` + +**Pourquoi ?** Lancé directement, `moteur.py` est traité comme un script isolé : Python ignore qu'il fait partie du package `application`, donc `from .config import ...` n'a aucun parent vers lequel pointer. + +**La solution** est d'exécuter le module *en tant que membre du package*, avec l'option `-m`, depuis la racine du projet (le dossier qui contient `application/`) : + +```bash +python -m application.core.moteur +``` + +Notez la syntaxe : des **points** (et non des barres obliques `/`) et **pas** d'extension `.py`. C'est aussi pourquoi on réserve les imports relatifs aux modules *internes* d'un package, et on prévoit un point d'entrée (un `main.py` à la racine, ou un `__main__.py`) qui, lui, importe le package normalement. + --- ## Structure de projet recommandée @@ -588,6 +613,8 @@ mon_projet/ sample_data.json ``` +> 📝 **`setup.py` ou `pyproject.toml` ?** Les arborescences ci-dessus montrent un `setup.py`, l'approche historique. Aujourd'hui, le standard est de décrire le projet dans un **`pyproject.toml`** (voir sections 6.3 et 6.5) ; pour un nouveau projet, créez plutôt un `pyproject.toml`. La structure des dossiers, elle, reste identique. + --- ## Fichier `__main__.py` @@ -861,6 +888,8 @@ bibliotheque/ database.py ``` +> 📝 Pour que cet exemple soit **reproductible tel quel**, nous fournissons ci-dessous tous les fichiers des sous-packages `models/` et `services/`. Les dossiers `utils/` et `data/` de la structure ci-dessus suivent exactement le même principe ; ils ne sont pas nécessaires pour exécuter l'exemple (le `__init__.py` n'importe que depuis `models` et `services`). + **Fichier : `bibliotheque/__init__.py`** ```python """ @@ -909,6 +938,45 @@ class Livre: return f"{self.titre} par {self.auteur} ({statut})" ``` +**Fichier : `bibliotheque/models/auteur.py`** +```python +"""Modèle de données pour les auteurs.""" + +class Auteur: + """Représente un auteur.""" + + def __init__(self, nom, nationalite=None): + self.nom = nom + self.nationalite = nationalite + + def __str__(self): + return self.nom +``` + +**Fichier : `bibliotheque/models/emprunt.py`** +```python +"""Modèle de données pour les emprunts.""" + +from datetime import date + +class Emprunt: + """Représente l'emprunt d'un livre par une personne.""" + + def __init__(self, livre, emprunteur): + self.livre = livre + self.emprunteur = emprunteur + self.date_emprunt = date.today() +``` + +**Fichier : `bibliotheque/models/__init__.py`** (rend les modèles accessibles via `bibliotheque.models`) +```python +"""Sous-package des modèles de données.""" + +from .livre import Livre +from .auteur import Auteur +from .emprunt import Emprunt +``` + **Fichier : `bibliotheque/services/gestion_livres.py`** ```python """Services de gestion des livres.""" @@ -936,17 +1004,44 @@ def lister_livres(): return _catalogue.copy() ``` +**Fichier : `bibliotheque/services/gestion_emprunts.py`** +```python +"""Services de gestion des emprunts.""" + +def emprunter_livre(livre): + """Marque un livre comme emprunté.""" + livre.disponible = False + return livre + +def retourner_livre(livre): + """Marque un livre comme disponible.""" + livre.disponible = True + return livre +``` + +**Fichier : `bibliotheque/services/__init__.py`** (rassemble les services du package) +```python +"""Sous-package des services métier.""" + +from .gestion_livres import ajouter_livre, rechercher_livre, lister_livres +from .gestion_emprunts import emprunter_livre, retourner_livre +``` + **Utilisation :** ```python -from bibliotheque import ajouter_livre, rechercher_livre +from bibliotheque import ajouter_livre, rechercher_livre, emprunter_livre # Ajouter des livres -livre1 = ajouter_livre("Python pour débutants", "John Doe", "123-456") -livre2 = ajouter_livre("JavaScript avancé", "Jane Smith", "789-012") +livre1 = ajouter_livre("Python pour débutants", "John Doe", "123-456") +livre2 = ajouter_livre("JavaScript avancé", "Jane Smith", "789-012") # Rechercher un livre -livre = rechercher_livre("Python") -print(livre) # Python pour débutants par John Doe (disponible) +livre = rechercher_livre("Python") +print(livre) # Python pour débutants par John Doe (disponible) + +# Emprunter ce livre, puis le rechercher de nouveau +emprunter_livre(livre) +print(rechercher_livre("Python")) # Python pour débutants par John Doe (emprunté) ``` --- diff --git a/06-modules-et-packages/03-gestion-dependances-pip.md b/06-modules-et-packages/03-gestion-dependances-pip.md index 47f7bcd..a8fb0b2 100644 --- a/06-modules-et-packages/03-gestion-dependances-pip.md +++ b/06-modules-et-packages/03-gestion-dependances-pip.md @@ -39,6 +39,8 @@ Résultat attendu : pip 24.0 from /usr/lib/python3.11/site-packages/pip (python 3.11) ``` +> 💡 **`pip` ou `python -m pip` ?** Les deux installent des packages, mais `python -m pip` est **plus sûr** : il utilise le pip du Python que vous venez d'invoquer (`python`). Sur une machine où plusieurs versions de Python cohabitent, la commande `pip` seule peut pointer vers un *autre* interpréteur (selon le `PATH`) et installer le package au mauvais endroit. Plus généralement, la forme **`python -m `** — qu'on retrouve dans `python -m venv`, `python -m ensurepip`, etc. — exécute un module installé **comme un script, avec cet interpréteur précis**. En cas de doute, préférez `python -m pip`. + Si pip n'est pas installé, vous pouvez l'installer avec : ```bash python -m ensurepip --upgrade @@ -91,6 +93,21 @@ Opérateurs de version disponibles : - `>` : Strictement supérieur - `<` : Strictement inférieur - `!=` : Exclure une version +- `~=` : Version compatible (par exemple : `~=2.28.0` autorise les correctifs `2.28.1`, `2.28.5`… mais **pas** `2.29.0`) + +### Installer un package avec ses « extras » + +Certains packages proposent des **dépendances optionnelles** regroupées sous un nom, appelées *extras*. On les active avec des crochets juste après le nom du package : + +```bash +# Installe uvicorn AVEC son groupe optionnel "standard" +pip install "uvicorn[standard]" + +# fastapi avec toutes ses dépendances optionnelles +pip install "fastapi[all]" +``` + +> 💡 Entourez le nom de **guillemets** (`"uvicorn[standard]"`) : sans eux, certains shells (comme zsh) interprètent les crochets `[]` comme un motif de noms de fichiers et la commande échoue. Vous retrouverez cette même notion d'extras avec Poetry (`uvicorn = {extras = ["standard"], ...}`) en section 6.5. ### Utilisation après installation @@ -690,11 +707,29 @@ ERROR: Could not install packages due to an OSError: [Errno 13] Permission denie **Solution :** N'utilisez JAMAIS `sudo pip install`. Utilisez un environnement virtuel à la place. -> 📝 **PEP 668 :** Sur les distributions Linux récentes (Ubuntu 23.04+, Fedora 38+, etc.), `pip install` en dehors d'un environnement virtuel est **bloqué** par défaut avec l'erreur `externally-managed-environment`. C'est un comportement voulu pour protéger les packages système. La solution est toujours d'utiliser un environnement virtuel (voir section 6.4). +> 📝 **PEP 668 :** Sur les distributions Linux récentes (Ubuntu 23.04+, Fedora 38+, etc.) et avec le Python d'Homebrew sur macOS, `pip install` en dehors d'un environnement virtuel est **bloqué** par défaut avec l'erreur `externally-managed-environment`. C'est un comportement voulu pour protéger les packages gérés par le système. La solution est toujours d'utiliser un environnement virtuel (voir section 6.4). +> +> ⚠️ Attention : `pip install --user` est **lui aussi** bloqué par la PEP 668 ; ce n'est donc *pas* un contournement. + +**Pour installer une application en ligne de commande** (par exemple `black`, `ruff`, ou même `poetry`) de façon globale tout en restant isolée, l'outil moderne est **pipx** : + +```bash +# Installer pipx (une seule fois) -- via le gestionnaire système sur les distributions récentes +sudo apt install pipx # Debian/Ubuntu +# ou : brew install pipx # macOS +# (sur un système non « externally-managed » : python -m pip install --user pipx) +pipx ensurepath + +# Installer une application dans son propre environnement isolé +pipx install black +pipx install ruff +``` + +`pipx` place chaque application dans un environnement virtuel dédié, puis expose sa commande dans votre `PATH` : vous obtenez l'outil globalement, sans polluer le Python système ni vos projets. C'est la méthode recommandée pour les outils, là où **venv** reste la méthode pour les dépendances *d'un projet*. -Si vous devez absolument installer globalement : +En dernier recours seulement, pour forcer une installation dans le Python système (rarement une bonne idée) : ```bash -pip install --user package_name +pip install --break-system-packages package_name ``` ### Problème 2 : Package introuvable diff --git a/06-modules-et-packages/04-environnements-virtuels.md b/06-modules-et-packages/04-environnements-virtuels.md index 12410c7..77e497d 100644 --- a/06-modules-et-packages/04-environnements-virtuels.md +++ b/06-modules-et-packages/04-environnements-virtuels.md @@ -171,6 +171,8 @@ Résultat attendu : /home/user/mon_projet/venv/bin/python ``` +> 📝 **Que fait l'activation ?** `source ... activate` ne « lance » aucun programme : il **modifie votre session shell**. Concrètement, il place `venv/bin` (ou `venv\Scripts` sous Windows) **au début de votre `PATH`** et définit la variable `VIRTUAL_ENV`. Comme le shell cherche les commandes dans l'ordre du `PATH`, taper `python` ou `pip` trouve désormais d'abord ceux du venv. `deactivate` se contente de restaurer l'ancien `PATH`. C'est aussi pourquoi l'activation ne vaut **que pour le terminal courant** : un autre terminal n'est pas affecté. + --- ## Utiliser l'environnement virtuel @@ -200,6 +202,8 @@ pip 23.2.1 setuptools 68.0.0 ``` +> 📝 **Python 3.12+ :** Depuis Python 3.12, `venv` n'installe plus `setuptools` par défaut — un environnement fraîchement créé ne contient plus que `pip`. Sur Python 3.10 et 3.11, `setuptools` est encore présent, comme dans l'exemple ci-dessus. Si un outil en a besoin sous 3.12+, installez-le explicitement : `pip install setuptools`. + Après installation de packages : ``` Package Version @@ -486,6 +490,11 @@ Noms recommandés : (venv) $ pip install --upgrade pip ``` +> 💡 **Astuce :** vous pouvez aussi demander la mise à jour de pip dès la création de l'environnement, en une seule commande, avec l'option `--upgrade-deps` (Python 3.9+) : +> ```bash +> python -m venv --upgrade-deps venv +> ``` + ### 5. Maintenir requirements.txt à jour Après chaque installation de package : @@ -748,9 +757,9 @@ pip install -r requirements.txt ### Problème 7 : Prompt ne montre pas (venv) -**Solution :** +**Solution :** le préfixe `(venv)` est masqué dès que la variable `VIRTUAL_ENV_DISABLE_PROMPT` est **définie** — à *n'importe quelle* valeur, y compris `0`. Il faut donc la **supprimer** (et non la mettre à `0`), puis réactiver : ```bash -export VIRTUAL_ENV_DISABLE_PROMPT=0 +unset VIRTUAL_ENV_DISABLE_PROMPT source venv/bin/activate ``` diff --git a/06-modules-et-packages/05-outils-modernes-poetry-pipenv.md b/06-modules-et-packages/05-outils-modernes-poetry-pipenv.md index 61c7370..cd4268a 100644 --- a/06-modules-et-packages/05-outils-modernes-poetry-pipenv.md +++ b/06-modules-et-packages/05-outils-modernes-poetry-pipenv.md @@ -15,6 +15,8 @@ Les outils modernes comme **Poetry** et **Pipenv** ont été créés pour simpli **Analogie :** Si pip + venv sont comme utiliser des outils séparés (tournevis, marteau, clés), Poetry et Pipenv sont comme des couteaux suisses qui intègrent tout en un seul outil. +> 📝 **Et uv ?** Depuis 2024, un nouvel outil écrit en Rust, **uv** (par Astral), bouscule cet écosystème en étant nettement plus rapide. Nous le présentons en détail plus loin dans ce chapitre ; en 2026, c'est souvent lui que l'on conseille pour démarrer un nouveau projet. + --- ## Pourquoi des outils modernes ? @@ -76,26 +78,32 @@ deactivate ### Qu'est-ce que Pipenv ? -**Pipenv** est un outil qui combine pip et virtualenv en une seule interface cohérente. Il a été créé par Kenneth Reitz (créateur de requests) et est recommandé officiellement par Python.org. +**Pipenv** est un outil qui combine pip et virtualenv en une seule interface cohérente. Il a été créé par Kenneth Reitz (créateur de requests) et est aujourd'hui maintenu par la **PyPA** (Python Packaging Authority), l'organisation qui chapeaute aussi pip. + +> 📝 **En 2026 :** Pipenv reste maintenu et stable, mais il a nettement perdu en popularité face à **Poetry** et surtout **uv** (voir plus loin dans ce chapitre). Il n'est plus *l'*outil unanimement recommandé qu'il a pu être vers 2018-2019. Nous le présentons ici car vous le rencontrerez encore dans de nombreux projets existants. **Philosophie :** "Pipenv vise à apporter le meilleur de tous les mondes de packaging à Python." ### Installation de Pipenv +Pipenv étant un **outil en ligne de commande**, la méthode recommandée est `pipx` (vu en section 6.3), qui l'installe dans son propre environnement isolé : + ```bash -# Installation globale -pip install --user pipenv +# Méthode recommandée : pipx +pipx install pipenv -# Ou avec pip système -pip install pipenv +# Ou via le gestionnaire système (Debian/Ubuntu) +sudo apt install pipenv # Vérification pipenv --version ``` +> ⚠️ Évitez `pip install pipenv` (et `pip install --user pipenv`) : sur les systèmes récents, ces commandes sont **bloquées par la PEP 668** (`externally-managed-environment`, cf. section 6.3). `pipx` est la bonne façon d'installer un outil global. + Résultat attendu : ``` -pipenv, version 2023.10.20 +pipenv, version 2026.6.1 ``` ### Les fichiers de Pipenv @@ -385,9 +393,16 @@ poetry --version Résultat : ``` -Poetry (version 1.7.0) +Poetry (version 2.3.2) ``` +> ⚠️ **Poetry 2.0 (janvier 2025) — changements importants :** Poetry est passé à la version 2.x, qui introduit plusieurs changements par rapport aux anciens tutoriels que vous trouverez sur le web : +> - `poetry shell` a été **retiré** du cœur de Poetry → utilisez `poetry env activate` (voir plus bas) ou installez le plugin `poetry-plugin-shell` ; +> - `poetry export` n'est **plus inclus par défaut** → il faut installer le plugin `poetry-plugin-export` ; +> - Poetry supporte désormais la table standard **`[project]`** (PEP 621) en plus de `[tool.poetry]`. +> +> Ces points sont détaillés dans les sections concernées ci-dessous. + **Configuration PATH :** Ajoutez Poetry à votre PATH si nécessaire : ```bash @@ -464,6 +479,25 @@ requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" ``` +> 📝 **Format `[project]` (PEP 621) — Poetry 2.0+ :** Depuis la version 2.0, Poetry comprend aussi la table standard `[project]` définie par la [PEP 621](https://peps.python.org/pep-0621/) (la même que celle utilisée par les autres outils modernes). Les métadonnées peuvent alors s'écrire de façon portable : +> +> ```toml +> [project] +> name = "mon-projet" +> version = "0.1.0" +> description = "" +> authors = [{name = "Votre Nom", email = "email@example.com"}] +> readme = "README.md" +> requires-python = ">=3.11" +> dependencies = ["requests>=2.31.0,<3.0.0"] +> +> [build-system] +> requires = ["poetry-core>=2.0"] +> build-backend = "poetry.core.masonry.api" +> ``` +> +> Le format historique `[tool.poetry]` montré ci-dessus **fonctionne toujours**, et la section `[tool.poetry]` reste utilisée pour les fonctionnalités spécifiques à Poetry (groupes de dépendances, sources personnalisées, etc.). Pour un nouveau projet, le format `[project]` est toutefois à privilégier : il facilite le passage d'un outil à l'autre (setuptools, Hatch, uv…). + ### Installer des dépendances ```bash @@ -543,20 +577,28 @@ poetry run python mon_script.py ### Activer l'environnement virtuel +Depuis Poetry 2.0, la commande recommandée est `poetry env activate`. Attention : elle ne fait qu'**afficher** la commande d'activation adaptée à votre shell ; pour activer réellement l'environnement, exécutez cette commande (ou enveloppez-la dans `eval`) : + ```bash -# Ouvrir un shell dans l'environnement -poetry shell +# Afficher la commande d'activation +poetry env activate + +# Activer directement (Linux/macOS, bash/zsh) +eval $(poetry env activate) ``` -Vous entrez dans l'environnement : +Une fois activé, vous voyez l'environnement dans votre invite : ```bash (mon-projet-py3.11) user@computer:~/mon_projet$ ``` -Pour sortir : -```bash -exit -``` +Pour sortir, utilisez `deactivate`. + +> 📝 **Et `poetry shell` ?** L'ancienne commande `poetry shell` (qui ouvrait un sous-shell) a été **retirée du cœur de Poetry en 2.0**. Pour la retrouver, installez le plugin dédié : +> ```bash +> poetry self add poetry-plugin-shell +> ``` +> Le plus souvent, `poetry run ` (vu juste au-dessus) suffit et évite d'avoir à activer l'environnement. ### Afficher les dépendances @@ -656,6 +698,11 @@ poetry add flask@2.3.3 requests@2.31.0 pandas@2.0.3 ### Exporter vers requirements.txt +> 📝 **Poetry 2.0+ :** la commande `poetry export` n'est plus incluse par défaut. Installez d'abord le plugin (une seule fois) : +> ```bash +> poetry self add poetry-plugin-export +> ``` + ```bash # Exporter vers requirements.txt poetry export -f requirements.txt --output requirements.txt @@ -742,22 +789,92 @@ poetry publish --- -## Comparaison : pip/venv vs Pipenv vs Poetry - -| Fonctionnalité | pip + venv | Pipenv | Poetry | -|----------------|-----------|--------|--------| -| **Installation** | Intégré à Python | `pip install pipenv` | Installation séparée | -| **Fichier de config** | requirements.txt | Pipfile | pyproject.toml | -| **Lock file** | ❌ Non | ✅ Pipfile.lock | ✅ poetry.lock | -| **Gestion venv** | Manuelle | ✅ Automatique | ✅ Automatique | -| **Résolution dépendances** | ⚠️ Basique | ✅ Avancée | ✅ Très avancée | -| **Dev vs Prod** | Fichiers séparés | ✅ Section [dev-packages] | ✅ Groupes | -| **Graphe dépendances** | ❌ Non (pipdeptree) | ✅ `pipenv graph` | ✅ `poetry show --tree` | -| **Build packages** | setuptools | ⚠️ Limité | ✅ Intégré | -| **Publication PyPI** | twine | ⚠️ Non | ✅ `poetry publish` | -| **Performance** | ✅ Rapide | ⚠️ Peut être lent | ✅ Rapide | -| **Courbe apprentissage** | ⚠️ Plusieurs outils | ⚠️ Moyenne | ⚠️ Moyenne | -| **Standard** | ✅ Officiel | ⚠️ Recommandé | ⚠️ Populaire | +## uv — l'outil ultra-rapide (Astral) + +**uv** est un gestionnaire de packages et de projets Python écrit en Rust par Astral (l'éditeur du linter Ruff). Apparu en 2024, il s'est imposé très vite : il vise à remplacer à lui seul `pip`, `venv`, `pipx`, `pip-tools` et une bonne partie de Poetry/Pipenv, le tout **10 à 100 fois plus rapidement**. + +> 📝 **Pourquoi uv est devenu incontournable en 2026 :** uv a dépassé Poetry en nombre de téléchargements mensuels sur PyPI et est devenu l'installeur par défaut dans de nombreuses chaînes d'intégration continue. Pour un **nouveau projet**, c'est aujourd'hui le choix le plus souvent recommandé. Il reste plus récent que Poetry/Pipenv, qui gardent un écosystème et une documentation très matures. + +### Installation + +```bash +# macOS / Linux (installeur autonome) +curl -LsSf https://astral.sh/uv/install.sh | sh + +# Windows (PowerShell) +powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" + +# Ou via un gestionnaire que vous avez déjà +pipx install uv # recommandé si vous avez déjà pipx +brew install uv # macOS (Homebrew) +pip install uv # dans un environnement Python +``` + +Vérification : +```bash +uv --version +``` + +### Les fichiers de uv + +Comme Poetry 2.0, uv s'appuie sur les standards : + +**1. pyproject.toml** : configuration du projet, avec la table `[project]` (PEP 621) +**2. uv.lock** : fichier de verrouillage multiplateforme (à committer dans Git) +**3. .venv/** : environnement virtuel créé et géré automatiquement (à ignorer dans Git) + +### Commandes essentielles + +```bash +# Créer un nouveau projet +uv init mon_projet +cd mon_projet + +# Ajouter une dépendance (crée .venv et met à jour pyproject.toml + uv.lock) +uv add requests +uv add 'fastapi>=0.110' + +# Ajouter une dépendance de développement +uv add --dev pytest ruff mypy + +# Supprimer une dépendance +uv remove requests + +# Installer / synchroniser l'environnement depuis le lock +uv sync + +# Exécuter une commande dans l'environnement du projet (sans activation manuelle) +uv run python app.py +uv run pytest +``` + +> 💡 uv n'oblige jamais à activer un environnement à la main : `uv run` s'en charge. uv sait aussi installer Python lui-même (`uv python install 3.12`) et offre une interface compatible pip (`uv pip install ...`) pour servir simplement de remplaçant rapide à pip. + +### Migrer un requirements.txt vers uv + +```bash +uv init +uv add -r requirements.txt +``` + +--- + +## Comparaison : pip/venv vs Pipenv vs Poetry vs uv + +| Fonctionnalité | pip + venv | Pipenv | Poetry | uv | +|----------------|-----------|--------|--------|----| +| **Installation** | Intégré à Python | `pipx install pipenv` | Installation séparée | Installation séparée | +| **Fichier de config** | requirements.txt | Pipfile | pyproject.toml | pyproject.toml | +| **Lock file** | ❌ Non | ✅ Pipfile.lock | ✅ poetry.lock | ✅ uv.lock | +| **Gestion venv** | Manuelle | ✅ Automatique | ✅ Automatique | ✅ Automatique | +| **Résolution dépendances** | ⚠️ Basique | ✅ Avancée | ✅ Très avancée | ✅ Très avancée | +| **Dev vs Prod** | Fichiers séparés | ✅ Section [dev-packages] | ✅ Groupes | ✅ Groupes | +| **Graphe dépendances** | ❌ Non (pipdeptree) | ✅ `pipenv graph` | ✅ `poetry show --tree` | ✅ `uv tree` | +| **Build packages** | setuptools | ⚠️ Limité | ✅ Intégré | ✅ `uv build` | +| **Publication PyPI** | twine | ⚠️ Non | ✅ `poetry publish` | ✅ `uv publish` | +| **Performance** | ✅ Rapide | ⚠️ Peut être lent | ✅ Rapide | ✅✅ Ultra-rapide (Rust) | +| **Courbe apprentissage** | ⚠️ Plusieurs outils | ⚠️ Moyenne | ⚠️ Moyenne | ✅ Simple | +| **Standard** | ✅ Officiel (PyPA) | ⚠️ Maintenu (PyPA) | ✅ Très répandu | ✅ Montée en puissance | --- @@ -775,16 +892,22 @@ poetry publish - ✅ Projet de taille moyenne - ✅ Vous voulez une meilleure gestion des dépendances que pip - ✅ Migration facile depuis requirements.txt -- ✅ Recommandation officielle de Python.org +- ✅ Vous appréciez sa syntaxe (Pipfile) et préférez un outil maintenu par la PyPA ### Utilisez Poetry si : - ✅ Nouveau projet professionnel - ✅ Vous allez créer un package à publier - ✅ Projet avec nombreuses dépendances -- ✅ Vous voulez les meilleures performances +- ✅ Vous voulez un outil mature et éprouvé, à la documentation abondante - ✅ Workflow moderne et complet - ✅ Gestion avancée de versions +### Utilisez uv si : +- ✅ Vous démarrez un nouveau projet en 2026 (choix par défaut conseillé) +- ✅ La vitesse d'installation compte (CI, gros projets, images Docker) +- ✅ Vous voulez un seul outil pour Python, venv, dépendances et build +- ✅ Vous venez de pip et cherchez un remplaçant rapide sans tout réapprendre + --- ## Configuration de Poetry @@ -1118,7 +1241,7 @@ poetry add $(cat requirements.txt | xargs) ### De Poetry vers Pipenv ```bash -# Exporter depuis Poetry +# Exporter depuis Poetry (nécessite le plugin poetry-plugin-export, cf. plus haut) poetry export -f requirements.txt -o requirements.txt # Importer dans Pipenv @@ -1169,7 +1292,7 @@ Ce projet utilise Poetry pour la gestion des dépendances. ### Prérequis - Python 3.11+ -- Poetry 1.7+ +- Poetry 2.0+ ### Setup ```bash @@ -1325,6 +1448,7 @@ Dans cette section, vous avez appris : - Utilisation de pyproject.toml (standard PEP 518) - Gestion avancée des dépendances avec groupes - Création et publication de packages +- **uv** : gestionnaire ultra-rapide (Rust), devenu en 2026 le choix le plus souvent conseillé pour démarrer un nouveau projet - **Comparaison des outils** et quand utiliser chacun - **Exemples pratiques** avec FastAPI et Flask - **Migration** entre différents outils @@ -1333,13 +1457,14 @@ Dans cette section, vous avez appris : **Points clés à retenir :** 1. **pip + venv** : Standard, simple, mais manuel -2. **Pipenv** : Bon compromis entre simplicité et fonctionnalités -3. **Poetry** : Outil le plus complet pour projets professionnels -4. Choisissez un outil adapté à vos besoins et votre niveau -5. Les lock files sont essentiels pour la reproductibilité +2. **Pipenv** : Bon compromis, maintenu par la PyPA mais en perte de vitesse +3. **Poetry** : Outil complet pour projets professionnels (attention aux changements de la v2.0) +4. **uv** : Le plus rapide, choix conseillé pour les nouveaux projets en 2026 +5. Choisissez un outil adapté à vos besoins et votre niveau +6. Les lock files sont essentiels pour la reproductibilité Les outils modernes simplifient considérablement la gestion des dépendances Python. Bien qu'ils ajoutent une couche d'abstraction, ils résolvent de nombreux problèmes et rendent le développement plus efficace et moins sujet aux erreurs. -Pour les débutants, commencez avec pip + venv pour bien comprendre les concepts de base, puis explorez Pipenv ou Poetry quand vous serez plus à l'aise. +Pour les débutants, commencez avec pip + venv pour bien comprendre les concepts de base (environnement isolé, dépendances, lock files), puis adoptez un outil intégré : **uv** est aujourd'hui le choix le plus simple et le plus rapide, Poetry restant une valeur sûre, en particulier pour publier des packages. ⏭️ [Bibliothèques standard essentielles](/07-bibliotheques-standard/README.md) diff --git a/06-modules-et-packages/exemples/01_01_importation_modules.py b/06-modules-et-packages/exemples/01_01_importation_modules.py index eb7265a..afe5671 100644 --- a/06-modules-et-packages/exemples/01_01_importation_modules.py +++ b/06-modules-et-packages/exemples/01_01_importation_modules.py @@ -46,7 +46,8 @@ # --- import * --- print("\n=== import * ===") -from math import * +# L'import * est déconseillé ; montré ici uniquement à titre de démonstration +from math import * # noqa: F403 resultat = sqrt(64) print(resultat) # 8.0 @@ -69,3 +70,11 @@ texte = "Mon email est exemple@email.com" email = re.search(r'\S+@\S+', texte) print(email.group()) # exemple@email.com + +# --- Explorer un module avec dir() --- +print("\n=== dir() ===") + +# dir(module) renvoie la liste triée des noms définis par le module +publics = [n for n in dir(math) if not n.startswith('_')] +print("Noms publics de math (6 premiers) :", publics[:6]) +print("'pi' et 'sqrt' présents :", 'pi' in dir(math) and 'sqrt' in dir(math)) diff --git a/06-modules-et-packages/exemples/01_02_creation_modules.py b/06-modules-et-packages/exemples/01_02_creation_modules.py index e559c71..dc93f82 100644 --- a/06-modules-et-packages/exemples/01_02_creation_modules.py +++ b/06-modules-et-packages/exemples/01_02_creation_modules.py @@ -6,7 +6,6 @@ # ============================================================================ from pathlib import Path -import importlib import sys # --- Créer le module operations.py dans un sous-dossier temporaire --- diff --git a/06-modules-et-packages/exemples/01_04_imports_circulaires.py b/06-modules-et-packages/exemples/01_04_imports_circulaires.py new file mode 100644 index 0000000..c9f3ddb --- /dev/null +++ b/06-modules-et-packages/exemples/01_04_imports_circulaires.py @@ -0,0 +1,48 @@ +# ============================================================================ +# Section 6.1 : Imports circulaires (le piège et sa résolution) +# Description : Deux modules qui s'importent mutuellement au niveau top-level +# lèvent ImportError ; l'import LOCAL (dans la fonction) résout. +# Fichier source : 01-importation-et-creation-modules.md +# ============================================================================ + +import sys +import os +import tempfile +import shutil + +# On fabrique deux modules temporaires pour la démonstration. +dossier = tempfile.mkdtemp() +sys.path.insert(0, dossier) + + +def ecrire(nom, contenu): + with open(os.path.join(dossier, nom), "w", encoding="utf-8") as f: + f.write(contenu) + + +# --- Version CASSÉE : chaque module importe l'autre au chargement --- +print("=== Version cassée : import circulaire ===") +ecrire("module_a.py", "from module_b import fonction_b\n\ndef fonction_a():\n return 'A'\n") +ecrire("module_b.py", "from module_a import fonction_a\n\ndef fonction_b():\n return 'B'\n") +try: + import module_a +except ImportError as e: + # Le message complet est : cannot import name 'fonction_a' from + # partially initialized module 'module_a' (most likely due to a circular import) + print("Échec à l'import :", str(e).split(" (")[0]) + +# Vider le cache des imports partiels avant de réessayer +sys.modules.pop("module_a", None) +sys.modules.pop("module_b", None) + +# --- Version CORRIGÉE : module_b importe localement (dans la fonction) --- +print("\n=== Version corrigée : import local dans module_b ===") +ecrire("module_b.py", "def fonction_b():\n from module_a import fonction_a\n return fonction_a()\n") +import module_a +print("Import réussi, fonction_a() =", module_a.fonction_a()) + +# Nettoyage +sys.modules.pop("module_a", None) +sys.modules.pop("module_b", None) +sys.path.remove(dossier) +shutil.rmtree(dossier) diff --git a/06-modules-et-packages/exemples/02_01_package_simple.py b/06-modules-et-packages/exemples/02_01_package_simple.py index 46b8193..3a8784f 100644 --- a/06-modules-et-packages/exemples/02_01_package_simple.py +++ b/06-modules-et-packages/exemples/02_01_package_simple.py @@ -22,15 +22,19 @@ """Module contenant des opérations mathématiques de base.""" def addition(a, b): + """Additionne deux nombres.""" return a + b def soustraction(a, b): + """Soustrait b de a.""" return a - b def multiplication(a, b): + """Multiplie deux nombres.""" return a * b def division(a, b): + """Divise a par b.""" if b == 0: raise ValueError("Division par zéro impossible") return a / b @@ -43,12 +47,15 @@ def division(a, b): PI = 3.14159 def aire_cercle(rayon): + """Calcule l'aire d'un cercle.""" return PI * rayon ** 2 def aire_rectangle(largeur, hauteur): + """Calcule l'aire d'un rectangle.""" return largeur * hauteur def perimetre_rectangle(largeur, hauteur): + """Calcule le périmètre d'un rectangle.""" return 2 * (largeur + hauteur) ''', encoding='utf-8') diff --git a/06-modules-et-packages/exemples/02_05_exemple_complet_bibliotheque.py b/06-modules-et-packages/exemples/02_05_exemple_complet_bibliotheque.py index 39ac95e..2a2ab18 100644 --- a/06-modules-et-packages/exemples/02_05_exemple_complet_bibliotheque.py +++ b/06-modules-et-packages/exemples/02_05_exemple_complet_bibliotheque.py @@ -1,7 +1,7 @@ # ============================================================================ # Section 6.2 : Exemple complet - Package de gestion de bibliothèque -# Description : Package bibliotheque avec models/livre.py, -# services/gestion_livres.py, imports relatifs +# Description : Package bibliotheque complet (models : livre/auteur/emprunt, +# services : gestion_livres/gestion_emprunts), imports relatifs # Fichier source : 02-structure-des-packages.md # ============================================================================ @@ -12,11 +12,9 @@ # --- Créer la structure complète --- base = Path('_temp_biblio') -# models/ models_dir = base / 'bibliotheque' / 'models' models_dir.mkdir(parents=True, exist_ok=True) -# services/ services_dir = base / 'bibliotheque' / 'services' services_dir.mkdir(parents=True, exist_ok=True) @@ -26,15 +24,27 @@ __version__ = "1.0.0" -from .models import Livre -from .services import ajouter_livre, rechercher_livre, lister_livres - -__all__ = ['Livre', 'ajouter_livre', 'rechercher_livre', 'lister_livres'] +from .models import Livre, Auteur, Emprunt +from .services import ( + ajouter_livre, + rechercher_livre, + emprunter_livre, + retourner_livre, +) + +__all__ = [ + 'Livre', 'Auteur', 'Emprunt', + 'ajouter_livre', 'rechercher_livre', 'emprunter_livre', 'retourner_livre', +] ''', encoding='utf-8') # models/__init__.py (models_dir / '__init__.py').write_text('''\ +"""Sous-package des modèles de données.""" + from .livre import Livre +from .auteur import Auteur +from .emprunt import Emprunt ''', encoding='utf-8') # models/livre.py @@ -55,9 +65,42 @@ def __str__(self): return f"{self.titre} par {self.auteur} ({statut})" ''', encoding='utf-8') +# models/auteur.py +(models_dir / 'auteur.py').write_text('''\ +"""Modèle de données pour les auteurs.""" + +class Auteur: + """Représente un auteur.""" + + def __init__(self, nom, nationalite=None): + self.nom = nom + self.nationalite = nationalite + + def __str__(self): + return self.nom +''', encoding='utf-8') + +# models/emprunt.py +(models_dir / 'emprunt.py').write_text('''\ +"""Modèle de données pour les emprunts.""" + +from datetime import date + +class Emprunt: + """Représente l'emprunt d'un livre par une personne.""" + + def __init__(self, livre, emprunteur): + self.livre = livre + self.emprunteur = emprunteur + self.date_emprunt = date.today() +''', encoding='utf-8') + # services/__init__.py (services_dir / '__init__.py').write_text('''\ +"""Sous-package des services métier.""" + from .gestion_livres import ajouter_livre, rechercher_livre, lister_livres +from .gestion_emprunts import emprunter_livre, retourner_livre ''', encoding='utf-8') # services/gestion_livres.py @@ -83,17 +126,34 @@ def lister_livres(): return _catalogue.copy() ''', encoding='utf-8') +# services/gestion_emprunts.py +(services_dir / 'gestion_emprunts.py').write_text('''\ +"""Services de gestion des emprunts.""" + +def emprunter_livre(livre): + """Marque un livre comme emprunté.""" + livre.disponible = False + return livre + +def retourner_livre(livre): + """Marque un livre comme disponible.""" + livre.disponible = True + return livre +''', encoding='utf-8') + # --- Utilisation --- sys.path.insert(0, str(base)) print("=== Package bibliothèque ===") -from bibliotheque import ajouter_livre, rechercher_livre, lister_livres +from bibliotheque import ajouter_livre, rechercher_livre, emprunter_livre +# lister_livres n'est pas ré-exporté par bibliotheque/__init__.py : on y accède via le sous-package services +from bibliotheque.services import lister_livres # Ajouter des livres -livre1 = ajouter_livre("Python pour débutants", "John Doe", "123-456") -livre2 = ajouter_livre("JavaScript avancé", "Jane Smith", "789-012") -livre3 = ajouter_livre("Data Science avec Python", "Alice Martin", "345-678") +ajouter_livre("Python pour débutants", "John Doe", "123-456") +ajouter_livre("JavaScript avancé", "Jane Smith", "789-012") +ajouter_livre("Data Science avec Python", "Alice Martin", "345-678") # Rechercher un livre livre = rechercher_livre("Python") @@ -104,9 +164,14 @@ def lister_livres(): for l in lister_livres(): print(f" - {l}") +# Emprunter le livre trouvé, puis vérifier son statut +emprunter_livre(livre) +print(f"\nAprès emprunt : {rechercher_livre('Python')}") + # Version du package import bibliotheque print(f"\nVersion : {bibliotheque.__version__}") +print(f"Exports : {bibliotheque.__all__}") # Nettoyage sys.path.pop(0) diff --git a/06-modules-et-packages/exemples/04_02_gitignore_et_structure.py b/06-modules-et-packages/exemples/04_02_gitignore_et_structure.py index ee93f63..f225125 100644 --- a/06-modules-et-packages/exemples/04_02_gitignore_et_structure.py +++ b/06-modules-et-packages/exemples/04_02_gitignore_et_structure.py @@ -117,7 +117,7 @@ def afficher_arbre(dossier, prefixe=""): est_dernier = i == len(items) - 1 connecteur = "--- " if est_dernier else "|-- " if item.name == 'venv': - print(f"{prefixe}{connecteur}{item.name}/ (ignore par Git)") + print(f"{prefixe}{connecteur}{item.name}/ (ignoré par Git)") continue if item.is_dir(): print(f"{prefixe}{connecteur}{item.name}/") @@ -126,19 +126,19 @@ def afficher_arbre(dossier, prefixe=""): else: note = "" if item.name == '.gitignore': - note = " (committe)" + note = " (committé)" elif item.name == '.env.example': - note = " (committe)" + note = " (committé)" elif item.name == '.env': - note = " (ignore par Git)" + note = " (ignoré par Git)" elif item.name == 'requirements.txt': - note = " (committe)" + note = " (committé)" print(f"{prefixe}{connecteur}{item.name}{note}") afficher_arbre(base) # --- Ce qu'il faut committer --- -print("\n=== Fichiers a committer vs ignorer ===") +print("\n=== Fichiers à committer vs ignorer ===") a_committer = [ "Code source (.py)", @@ -157,11 +157,11 @@ def afficher_arbre(dossier, prefixe=""): "dist/ et build/", ] -print("A COMMITTER :") +print("À COMMITTER :") for item in a_committer: print(f" [OK] {item}") -print("\nA NE PAS COMMITTER :") +print("\nÀ NE PAS COMMITTER :") for item in a_ignorer: print(f" [X] {item}") @@ -172,9 +172,9 @@ def afficher_arbre(dossier, prefixe=""): "Un environnement virtuel par projet", "Toujours activer le venv avant de travailler", "Nommer le venv : venv, .venv ou env", - "Mettre a jour pip apres creation", - "Maintenir requirements.txt a jour", - "Documenter les prerequis dans README.md", + "Mettre à jour pip après création", + "Maintenir requirements.txt à jour", + "Documenter les prérequis dans README.md", ] for i, pratique in enumerate(bonnes_pratiques, 1): @@ -182,4 +182,4 @@ def afficher_arbre(dossier, prefixe=""): # Nettoyage shutil.rmtree(base) -print(f"\nNettoyage : {base} supprime") +print(f"\nNettoyage : {base} supprimé") diff --git a/06-modules-et-packages/exemples/05_01_pyproject_toml.py b/06-modules-et-packages/exemples/05_01_pyproject_toml.py index 9a29dbd..a85a784 100644 --- a/06-modules-et-packages/exemples/05_01_pyproject_toml.py +++ b/06-modules-et-packages/exemples/05_01_pyproject_toml.py @@ -70,7 +70,7 @@ else: # Pour Python < 3.11, on simule config = None - print("(tomllib necessaire Python 3.11+, simulation)") + print("(tomllib nécessaire en Python 3.11+, simulation)") if config: poetry = config.get('tool', {}).get('poetry', {}) @@ -82,7 +82,7 @@ # Dépendances principales deps = poetry.get('dependencies', {}) - print(f"\nDependances principales ({len(deps)}) :") + print(f"\nDépendances principales ({len(deps)}) :") for nom, version in deps.items(): if isinstance(version, dict): print(f" {nom} = {version}") @@ -91,27 +91,27 @@ # Dépendances de développement dev_deps = poetry.get('group', {}).get('dev', {}).get('dependencies', {}) - print(f"\nDependances de dev ({len(dev_deps)}) :") + print(f"\nDépendances de dev ({len(dev_deps)}) :") for nom, version in dev_deps.items(): print(f" {nom} = {version}") # Dépendances optionnelles (docs) docs_deps = poetry.get('group', {}).get('docs', {}).get('dependencies', {}) if docs_deps: - print(f"\nDependances docs ({len(docs_deps)}) :") + print(f"\nDépendances docs ({len(docs_deps)}) :") for nom, version in docs_deps.items(): print(f" {nom} = {version}") # Scripts scripts = poetry.get('scripts', {}) if scripts: - print(f"\nScripts (entry points) :") + print("\nScripts (entry points) :") for nom, cible in scripts.items(): print(f" {nom} -> {cible}") # Build system build = config.get('build-system', {}) - print(f"\nBuild system :") + print("\nBuild system :") print(f" requires : {build.get('requires')}") print(f" backend : {build.get('build-backend')}") diff --git a/06-modules-et-packages/exemples/05_02_pipfile_demo.py b/06-modules-et-packages/exemples/05_02_pipfile_demo.py index 01c5cfe..d63d3c8 100644 --- a/06-modules-et-packages/exemples/05_02_pipfile_demo.py +++ b/06-modules-et-packages/exemples/05_02_pipfile_demo.py @@ -66,43 +66,51 @@ # Version Python requise requires = config.get('requires', {}) print(f"\nPython requis : {requires.get('python_version')}") +else: + print("(tomllib nécessaire en Python 3.11+, simulation)") # --- Comparaison des fichiers de configuration --- print("\n=== Comparaison des formats ===") comparaison = [ - ("Fonctionnalite", "pip + venv", "Pipenv", "Poetry"), - ("-" * 20, "-" * 18, "-" * 18, "-" * 18), - ("Fichier config", "requirements.txt", "Pipfile", "pyproject.toml"), - ("Lock file", "Non", "Pipfile.lock", "poetry.lock"), - ("Gestion venv", "Manuelle", "Automatique", "Automatique"), - ("Dev vs Prod", "Fichiers separes", "[dev-packages]", "Groupes"), - ("Build packages", "setuptools", "Limite", "Integre"), - ("Publication PyPI", "twine", "Non", "poetry publish"), + ("Fonctionnalité", "pip + venv", "Pipenv", "Poetry", "uv"), + ("-" * 18, "-" * 17, "-" * 14, "-" * 15, "-" * 14), + ("Fichier config", "requirements.txt", "Pipfile", "pyproject.toml", "pyproject.toml"), + ("Lock file", "Non", "Pipfile.lock", "poetry.lock", "uv.lock"), + ("Gestion venv", "Manuelle", "Automatique", "Automatique", "Automatique"), + ("Dev vs Prod", "Fichiers séparés", "[dev-packages]", "Groupes", "Groupes"), + ("Build packages", "setuptools", "Limité", "Intégré", "Intégré"), + ("Publication PyPI", "twine", "Non", "poetry publish", "uv publish"), + ("Vitesse", "Rapide", "Lente", "Rapide", "Très rapide"), ] for ligne in comparaison: - print(f" {ligne[0]:20s} {ligne[1]:18s} {ligne[2]:18s} {ligne[3]:18s}") + print(f" {ligne[0]:18s} {ligne[1]:17s} {ligne[2]:14s} {ligne[3]:15s} {ligne[4]:14s}") # --- Quand utiliser quel outil --- print("\n=== Quand utiliser quel outil ? ===") recommandations = { "pip + venv": [ - "Debutant avec Python", - "Projet simple, peu de dependances", + "Débutant avec Python", + "Projet simple, peu de dépendances", "Script rapide ou prototype", ], "Pipenv": [ "Projet de taille moyenne", "Migration facile depuis requirements.txt", - "Recommande par Python.org", + "Outil maintenu par la PyPA (mais en perte de vitesse)", ], "Poetry": [ "Nouveau projet professionnel", - "Package a publier sur PyPI", + "Package à publier sur PyPI", "Workflow moderne et complet", ], + "uv": [ + "Nouveau projet en 2026 (choix par défaut conseillé)", + "Vitesse d'installation critique (CI, Docker)", + "Un seul outil rapide pour tout (écrit en Rust)", + ], } for outil, cas in recommandations.items(): @@ -114,12 +122,12 @@ print("\n=== Workflow typique Poetry ===") etapes = [ - "poetry new mon_projet # Creer le projet", + "poetry new mon_projet # Créer le projet", "cd mon_projet", - "poetry add requests flask # Ajouter des dependances", + "poetry add requests flask # Ajouter des dépendances", "poetry add --group dev pytest # Ajouter des deps de dev", "poetry install # Installer tout", - "poetry run python app.py # Executer", + "poetry run python app.py # Exécuter", "poetry run pytest # Tester", "poetry build # Construire le package", "poetry publish # Publier sur PyPI", @@ -137,14 +145,30 @@ "pipenv install flask # Ajouter un package", "pipenv install --dev pytest # Ajouter un package de dev", "pipenv shell # Activer l'environnement", - "python app.py # Executer", + "python app.py # Exécuter", "pipenv run pytest # Tester sans activer", - "pipenv graph # Voir les dependances", + "pipenv graph # Voir les dépendances", "exit # Quitter le shell", ] for i, etape in enumerate(etapes_pipenv, 1): print(f" {i}. {etape}") +# --- Workflow typique uv --- +print("\n=== Workflow typique uv ===") + +etapes_uv = [ + "uv init mon_projet # Créer le projet", + "cd mon_projet", + "uv add requests flask # Ajouter des dépendances", + "uv add --dev pytest # Ajouter une dépendance de dev", + "uv run python app.py # Exécuter (venv géré automatiquement)", + "uv run pytest # Tester", + "uv sync # Synchroniser depuis uv.lock", +] + +for i, etape in enumerate(etapes_uv, 1): + print(f" {i}. {etape}") + # Nettoyage fichier.unlink() diff --git a/06-modules-et-packages/exemples/README.md b/06-modules-et-packages/exemples/README.md index a8fca55..04df966 100644 --- a/06-modules-et-packages/exemples/README.md +++ b/06-modules-et-packages/exemples/README.md @@ -1,6 +1,6 @@ # Chapitre 06 - Modules et packages : Exemples testables -Ce dossier contient **13 fichiers** Python exécutables extraits des cours du chapitre 06. +Ce dossier contient **14 fichiers** Python exécutables extraits des cours du chapitre 06. ## Fichiers @@ -11,6 +11,7 @@ Ce dossier contient **13 fichiers** Python exécutables extraits des cours du ch | `01_01_importation_modules.py` | Syntaxes d'import (import, alias, from, import *), modules standard (math, datetime, os, re) | 01-importation-et-creation-modules.md | | `01_02_creation_modules.py` | Créer ses propres modules (operations.py, geometrie.py), importer et utiliser | 01-importation-et-creation-modules.md | | `01_03_name_et_organisation.py` | `__name__ == "__main__"`, exécution directe vs import, sys.path, organisation | 01-importation-et-creation-modules.md | +| `01_04_imports_circulaires.py` | Import circulaire (`ImportError` : partially initialized module) et résolution par import local | 01-importation-et-creation-modules.md | ### Section 6.2 : Structure des packages @@ -20,7 +21,7 @@ Ce dossier contient **13 fichiers** Python exécutables extraits des cours du ch | `02_02_init_avance.py` | `__init__.py` avancé : imports simplifiés, `__version__`, `__all__` | 02-structure-des-packages.md | | `02_03_sous_packages.py` | Sous-packages (utilitaires/texte/), imports relatifs, formatage et validation | 02-structure-des-packages.md | | `02_04_main_et_all.py` | `__main__.py` (python -m package), `__all__` pour contrôler les exports | 02-structure-des-packages.md | -| `02_05_exemple_complet_bibliotheque.py` | Package complet : bibliotheque avec models/livre.py, services/gestion_livres.py | 02-structure-des-packages.md | +| `02_05_exemple_complet_bibliotheque.py` | Package complet : bibliotheque (models : livre/auteur/emprunt, services : gestion_livres/gestion_emprunts), imports relatifs, démo d'emprunt | 02-structure-des-packages.md | ### Section 6.3 : Gestion des dépendances avec pip @@ -40,76 +41,95 @@ Ce dossier contient **13 fichiers** Python exécutables extraits des cours du ch | Fichier | Description | Source | |---------|-------------|--------| | `05_01_pyproject_toml.py` | Créer et parser un pyproject.toml (Poetry), groupes de dépendances, notations de versions | 05-outils-modernes-poetry-pipenv.md | -| `05_02_pipfile_demo.py` | Créer et parser un Pipfile (Pipenv), comparaison pip/Pipenv/Poetry, workflows | 05-outils-modernes-poetry-pipenv.md | +| `05_02_pipfile_demo.py` | Créer et parser un Pipfile (Pipenv), comparaison pip/Pipenv/Poetry/uv, workflows (Poetry/Pipenv/uv) | 05-outils-modernes-poetry-pipenv.md | ## Sorties attendues ### 01_01_importation_modules.py ``` -=== import math === -pi = 3.141592653589793 -sqrt(16) = 4.0 -cos(0) = 1.0 +=== import simple === +4.0 +3.141592653589793 === import avec alias === -Aujourd'hui : (date du jour) -Dans 30 jours : (date + 30 jours) +5.0 +3.141592653589793 -=== from ... import === -Dossier courant : (chemin courant) -Fichiers Python : (liste des .py) +=== from import === +6.0 +3.141592653589793 -=== Modules standard utiles === -random.randint(1, 100) = (valeur aléatoire) -re.findall('[0-9]+', 'Il a 25 ans et 3 enfants') = ['25', '3'] +=== alias sur éléments === +7.0 + +=== import * === +8.0 + +=== Modules standard === +Date actuelle : (date du jour) +Répertoire : (chemin courant) +exemple@email.com + +=== dir() === +Noms publics de math (6 premiers) : ['acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2'] +'pi' et 'sqrt' présents : True ``` ### 01_02_creation_modules.py ``` === Module operations === -addition(10, 5) = 15 -soustraction(10, 5) = 5 -multiplication(6, 7) = 42 +10 + 5 = 15 +7 x 3 = 21 +Valeur de PI : 3.14159 === Module geometrie === -aire_cercle(5) = 78.53975 -aire_rectangle(4, 6) = 24 -perimetre_rectangle(4, 6) = 20 +Aire du rectangle : 15 +Périmètre du rectangle : 16 +Aire du cercle : 153.93791 +Circonférence du cercle : 43.98226 ``` ### 01_03_name_et_organisation.py ``` -=== Exécution directe (python calculs.py) === -[module calculs] __name__ = __main__ -Exécution directe : tests... -addition(3, 5) = 8 -multiplication(4, 7) = 28 +=== __name__ === +Exécution directe : +Test du module calculs +Carré de 5 : 25 +Cube de 3 : 27 -=== Import du module === -[module calculs] __name__ = calculs -Résultat : 15 +Importation : +Carré de 10 : 100 -=== Module utilitaires === +=== Module bien structuré === Version : 1.0.0 -Documentation : Module utilitaires - fonctions d'aide. -Nom du module : utilitaires +Nettoyé : 'Bonjour' +Majuscules : PYTHON +Mots : 3 -=== sys.path (3 premiers) === +=== sys.path (premiers chemins) === (chemins du sys.path) ``` -### 02_01_package_simple.py +### 01_04_imports_circulaires.py +``` +=== Version cassée : import circulaire === +Échec à l'import : cannot import name 'fonction_a' from partially initialized module 'module_a' + +=== Version corrigée : import local dans module_b === +Import réussi, fonction_a() = A ``` -=== Méthode 1 : import complet === -5 + 3 = 8 -5 - 3 = 2 -=== Méthode 2 : import avec alias === -Aire du cercle (r=5) : 78.53975 +### 02_01_package_simple.py +``` +=== Import complet === +10 + 5 = 15 +Aire du cercle : 153.93791 -=== Méthode 3 : from ... import === +=== Import avec alias === 6 x 7 = 42 -10 / 3 = 3.33 + +=== Import spécifique === +Aire du rectangle : 15 ``` ### 02_02_init_avance.py @@ -171,7 +191,10 @@ Catalogue : - JavaScript avancé par Jane Smith (disponible) - Data Science avec Python par Alice Martin (disponible) +Après emprunt : Python pour débutants par John Doe (emprunté) + Version : 1.0.0 +Exports : ['Livre', 'Auteur', 'Emprunt', 'ajouter_livre', 'rechercher_livre', 'emprunter_livre', 'retourner_livre'] ``` ### 03_01_pip_et_requirements.py @@ -248,11 +271,11 @@ Nombre d'éléments : 0 === Structure de projet recommandée === (arbre du projet) -=== Fichiers a committer vs ignorer === -A COMMITTER : +=== Fichiers à committer vs ignorer === +À COMMITTER : [OK] Code source (.py) ... -A NE PAS COMMITTER : +À NE PAS COMMITTER : [X] venv/ ou env/ ... @@ -269,11 +292,11 @@ A NE PAS COMMITTER : === Lecture du pyproject.toml === Nom du projet : mon-api Version : 0.1.0 -Dependances principales (5) : +Dépendances principales (5) : python = ^3.11 fastapi = ^0.104.0 ... -Dependances de dev (4) : +Dépendances de dev (4) : pytest = ^7.4.0 ... Scripts (entry points) : @@ -302,12 +325,13 @@ Packages de dev (3) : ... === Comparaison des formats === - (tableau comparatif pip/Pipenv/Poetry) + (tableau comparatif pip/Pipenv/Poetry/uv) === Quand utiliser quel outil ? === pip + venv : ... Pipenv : ... Poetry : ... + uv : ... === Workflow typique Poetry === 1. poetry new mon_projet @@ -316,6 +340,10 @@ Packages de dev (3) : === Workflow typique Pipenv === 1. mkdir mon_projet && cd mon_projet ... + +=== Workflow typique uv === + 1. uv init mon_projet + ... ``` ## Exécution @@ -332,4 +360,4 @@ for f in *.py; do echo "=== $f ==="; python3 "$f"; echo; done - Tous les exemples de packages/modules créent des répertoires temporaires (`_temp_*`) et les suppriment après exécution. - Les fichiers 03, 04 et 05 concernent principalement des commandes shell (pip, venv, poetry, pipenv). Les fichiers Python démontrent les concepts programmatiquement. -- Le fichier `05_01_pyproject_toml.py` utilise `tomllib` (Python 3.11+). Pour les versions antérieures, installer `tomli`. +- Le fichier `05_01_pyproject_toml.py` parse le `pyproject.toml` avec `tomllib` (intégré depuis Python 3.11). Sur Python 3.10, il détecte l'absence de `tomllib` et affiche une **version simulée** (le parsing détaillé est ignoré) — aucune installation supplémentaire n'est requise. diff --git a/07-bibliotheques-standard/01-os-sys-subprocess.md b/07-bibliotheques-standard/01-os-sys-subprocess.md index 7980877..c4e79d1 100644 --- a/07-bibliotheques-standard/01-os-sys-subprocess.md +++ b/07-bibliotheques-standard/01-os-sys-subprocess.md @@ -150,6 +150,8 @@ for cle, valeur in os.environ.items(): os.environ["MA_VARIABLE"] = "ma_valeur" ``` +> 📝 **Portabilité :** la variable `HOME` n'existe que sur Linux/macOS (sur Windows, c'est `USERPROFILE`). Pour obtenir le dossier personnel de l'utilisateur de façon portable, utilisez plutôt `os.path.expanduser("~")` ou, plus moderne, `pathlib.Path.home()`. + ### Exemple pratique : Parcourir une arborescence ```python @@ -339,6 +341,7 @@ resultat = subprocess.run(["dir"], shell=True, capture_output=True, text=True) # text=True : Retourne des chaînes de caractères (pas des bytes) # shell=True : Exécute via le shell (nécessaire pour certaines commandes) # check=True : Lève une exception si la commande échoue +# cwd="chemin" : Exécute la commande depuis ce répertoire (le répertoire de travail du programme Python n'est pas modifié) resultat = subprocess.run( ["echo", "Bonjour"], @@ -350,6 +353,8 @@ resultat = subprocess.run( print(resultat.stdout) # "Bonjour\n" ``` +> 💡 Un paramètre de plus, très utile pour la robustesse : **`timeout=`**. Sans lui, `run()` attend **indéfiniment** que la commande se termine ; avec, une commande trop lente est interrompue et lève `subprocess.TimeoutExpired`. Indispensable pour les appels réseau ou tout processus susceptible de se bloquer. + ### Gérer les erreurs ```python @@ -380,7 +385,7 @@ import sys def obtenir_info_python(): """Obtient la version de Python via subprocess""" resultat = subprocess.run( - ["python", "--version"], + [sys.executable, "--version"], # sys.executable = le Python courant (plus portable que "python") capture_output=True, text=True ) @@ -455,6 +460,8 @@ print(f"Nombre de fichiers Python : {resultat.stdout.strip()}") **⚠️ Avertissement de sécurité** : Utiliser `shell=True` avec des données provenant d'utilisateurs peut créer des failles de sécurité (injection de commandes). Préférez toujours passer les commandes sous forme de liste sans shell=True quand c'est possible. +> 📝 **Pourquoi la liste protège-t-elle ?** Avec `shell=True`, c'est le **shell** qui reçoit votre chaîne et y interprète les caractères spéciaux (`;`, `|`, `$(...)`, `&&`…). Une donnée non fiable comme `"; rm -rf ~"` insérée dans la commande serait alors exécutée comme une **commande à part entière**. En passant une **liste** (`["ls", nom]`) sans `shell=True`, aucun shell n'intervient : Python transmet chaque élément **tel quel** au programme, comme un argument littéral — `"; rm -rf ~"` est simplement traité comme un nom de fichier (introuvable), jamais comme une commande. D'où la règle : liste + pas de `shell=True` dès qu'une entrée peut provenir de l'extérieur. + ### Exemple pratique : Créer une sauvegarde ```python @@ -620,7 +627,7 @@ chemin = os.path.join("dossier", "fichier.txt") ### 2. Préférer `subprocess.run()` aux anciennes fonctions -Les fonctions `os.system()`, `os.popen()` et `subprocess.call()` sont obsolètes. Utilisez toujours `subprocess.run()`. +Les fonctions plus anciennes comme `os.system()`, `os.popen()` ou `subprocess.call()` restent disponibles, mais sont déconseillées pour le nouveau code : `subprocess.run()` (introduit en Python 3.5) est l'interface moderne et recommandée — plus sûre, plus complète et plus simple à utiliser correctement. ### 3. Gérer les erreurs diff --git a/07-bibliotheques-standard/02-datetime-et-time.md b/07-bibliotheques-standard/02-datetime-et-time.md index 8c7b0a7..9078414 100644 --- a/07-bibliotheques-standard/02-datetime-et-time.md +++ b/07-bibliotheques-standard/02-datetime-et-time.md @@ -42,6 +42,8 @@ maintenant_utc = datetime.now(timezone.utc) print(maintenant_utc) ``` +> 📝 Dans tout ce chapitre, les exemples prennent **2025-10-27** (un lundi) comme date « actuelle » illustrative. Quand vous exécuterez `datetime.now()` ou `date.today()`, vous obtiendrez bien sûr **la date du jour** : les sorties commentées montrent le *format* attendu, pas une valeur figée. + ### Créer une date/heure spécifique ```python @@ -143,6 +145,29 @@ print(date2) # 2025-10-27 00:00:00 print(date3) # 2025-10-27 00:00:00 ``` +### Format ISO 8601 : `isoformat()` et `fromisoformat()` + +Le format **ISO 8601** (`AAAA-MM-JJTHH:MM:SS`) est le standard pour échanger des dates (JSON, API, bases de données, fichiers). `datetime` offre un aller-retour direct, sans avoir à écrire de chaîne de format : + +```python +from datetime import datetime, date + +# Objet -> chaîne ISO +dt = datetime(2025, 10, 27, 14, 30, 45) +print(dt.isoformat()) # 2025-10-27T14:30:45 + +# Chaîne ISO -> objet (l'opération inverse exacte) +dt2 = datetime.fromisoformat("2025-10-27T14:30:45") +print(dt2) # 2025-10-27 14:30:45 + +# Fonctionne aussi avec une date seule +print(date.fromisoformat("2025-10-27")) # 2025-10-27 +``` + +C'est la méthode à privilégier pour **stocker puis relire** des dates de façon fiable (plus sûr que d'inventer son propre format). + +> 📝 Depuis Python 3.11, `fromisoformat()` accepte la plupart des variantes ISO 8601 (y compris le suffixe `Z` pour l'UTC et les décalages de fuseau, ex. `+02:00`). Sur Python 3.10, il ne reconnaît que le format exact produit par `isoformat()`. + ### Exemple pratique : Calculer l'âge d'une personne ```python @@ -258,6 +283,18 @@ print(f"Jours totaux : {duree.days}") print(f"Secondes totales : {duree.total_seconds()}") ``` +> 📝 **Les attributs d'un `timedelta` : composantes vs durée totale.** En interne, un `timedelta` ne stocke que **trois** nombres : `.days`, `.seconds` et `.microseconds`. Piège classique : `.seconds` n'est **pas** la durée totale, mais seulement la composante secondes comprise entre 0 et 86399 (ce qui reste *après* les jours entiers). Pour la durée totale, utilisez **`.total_seconds()`** : +> +> ```python +> from datetime import timedelta +> d = timedelta(days=1, hours=2) +> print(d.days) # 1 +> print(d.seconds) # 7200 (les 2 heures, PAS la durée totale !) +> print(d.total_seconds()) # 93600.0 (1 jour + 2 heures, en secondes) +> ``` +> +> C'est pourquoi, pour décomposer une durée en jours/heures/minutes, on extrait `.days` puis on applique `divmod()` à `.seconds` (voir l'exemple « temps restant » plus bas). + ### Opérations arithmétiques avec les dates ```python @@ -486,6 +523,8 @@ duree = fin - debut print(f"Temps d'exécution : {duree:.6f} secondes") ``` +> 📝 Pour **mesurer une durée**, préférez `time.perf_counter()` : ce n'est pas seulement le compteur de plus haute résolution, il est surtout **monotone** (il ne « recule » jamais). `time.time()` suit l'horloge système, qui peut être ajustée pendant la mesure (synchronisation réseau NTP, passage heure d'été/hiver…) et fausser le résultat — voire produire une durée négative. + ### Exemple pratique : Chronomètre ```python @@ -543,6 +582,17 @@ with Chronometre(): Pour travailler avec des fuseaux horaires, utilisez le module `zoneinfo` de la bibliothèque standard. +> 📝 **Datetime « naïf » vs « aware » :** un datetime créé sans fuseau (ex. `datetime.now()`) est dit **naïf** (son attribut `tzinfo` vaut `None`) — il ne sait pas à quel fuseau il se rapporte. Un datetime créé **avec** un fuseau (ex. `datetime.now(timezone.utc)` ou via `zoneinfo`) est dit **aware**. On ne peut **ni comparer ni soustraire** un naïf et un aware : +> +> ```python +> from datetime import datetime, timezone +> naif = datetime.now() +> aware = datetime.now(timezone.utc) +> aware - naif # TypeError: can't subtract offset-naive and offset-aware datetimes +> ``` +> +> Règle pratique : restez **cohérent** (tout naïf, ou tout aware) ; pour une vraie application, préférez les datetimes *aware* en UTC. + ### Avec zoneinfo ```python @@ -579,6 +629,8 @@ date_utc = datetime(2025, 10, 27, 12, 0, 0, tzinfo=timezone.utc) print(date_utc) ``` +> ⚠️ **À éviter : `datetime.utcnow()`** — vous le croiserez dans beaucoup d'anciens tutoriels, mais il est **déprécié depuis Python 3.12** (et voué à être supprimé). Le piège : il renvoie un `datetime` *naïf* (sans fuseau), alors qu'il représente en réalité de l'UTC — une source classique de bugs. Utilisez toujours `datetime.now(timezone.utc)`, qui renvoie un `datetime` *aware* correctement marqué UTC. + --- ## Exemples pratiques complets diff --git a/07-bibliotheques-standard/03-math-random-statistics.md b/07-bibliotheques-standard/03-math-random-statistics.md index 8cd80b1..45acac5 100644 --- a/07-bibliotheques-standard/03-math-random-statistics.md +++ b/07-bibliotheques-standard/03-math-random-statistics.md @@ -121,6 +121,8 @@ print(round(3.5)) # 4 (arrondi vers le pair) print(round(4.5)) # 4 (arrondi vers le pair, pas 5 !) ``` +> 📝 **Pourquoi cet « arrondi vers le pair » ?** Arrondir systématiquement les `.5` vers le haut introduit un **biais** : sur une longue série de valeurs, les sommes sont régulièrement surestimées. En envoyant la moitié des cas vers le bas et l'autre moitié vers le haut (vers le chiffre **pair**), les erreurs se compensent en moyenne. C'est la règle définie par la norme **IEEE 754** et le comportement par défaut de `round()` en Python 3. Si vous avez besoin d'un autre arrondi (par exemple « toujours .5 vers le haut » pour de la comptabilité), utilisez le module `decimal` et ses modes comme `ROUND_HALF_UP`. + ### Exemple pratique : Calculer une facture ```python @@ -176,7 +178,7 @@ print(math.exp(1)) # 2.718281828459045 (e¹) print(math.exp(2)) # 7.38905609893065 (e²) # Exponentielle - 1 (plus précise pour les petites valeurs) -print(math.expm1(0.001)) # 0.0010005001667083846 +print(math.expm1(0.001)) # 0.0010005001667083417 ``` ### Exemple pratique : Calcul d'intérêts composés @@ -229,7 +231,7 @@ print(math.log(8, 2)) # 3.0 (log base 2 de 8) print(math.log(81, 3)) # 4.0 (log base 3 de 81) # Logarithme de (1 + x) - plus précis pour les petites valeurs -print(math.log1p(0.001)) # 0.0009995003330835332 +print(math.log1p(0.001)) # 0.0009995003330835331 ``` --- @@ -253,7 +255,7 @@ print(f"{angle_radians} radians = {angle_degres}°") # 90.0 # Fonctions trigonométriques de base print(f"sin(π/2) = {math.sin(math.pi / 2)}") # 1.0 print(f"cos(π) = {math.cos(math.pi)}") # -1.0 -print(f"tan(π/4) = {math.tan(math.pi / 4)}") # 1.0 +print(f"tan(π/4) = {math.tan(math.pi / 4)}") # 0.9999999999999999 (et non 1.0 : précision flottante) # Fonctions trigonométriques inverses print(f"asin(1) = {math.asin(1)}") # 1.5707... (π/2 radians) @@ -324,9 +326,9 @@ print(math.comb(5, 2)) # 10 (nombre de façons de choisir 2 éléments par print(math.perm(5, 2)) # 20 (arrangements de 2 éléments parmi 5) # Somme précise d'un itérable (évite les erreurs d'arrondi) -nombres = [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1] -print(sum(nombres)) # 1.0 (peut varier selon la version de Python) -print(math.fsum(nombres)) # 1.0 (plus précis) +nombres = [1e16, 1, -1e16] # un petit nombre (1) noyé entre deux grands +print(sum(nombres)) # 0.0 (le +1 est "absorbé" par l'arrondi de 1e16, puis annulé) +print(math.fsum(nombres)) # 1.0 (fsum calcule la somme exacte) # Produit d'un itérable - Python 3.8+ print(math.prod([2, 3, 4])) # 24 @@ -336,6 +338,30 @@ print(math.hypot(3, 4)) # 5.0 print(math.hypot(5, 12)) # 13.0 ``` +### Comparer des flottants : `math.isclose()` + +À cause de la représentation binaire des nombres à virgule flottante, **ne comparez jamais deux flottants avec `==`**. Utilisez `math.isclose()` : + +```python +import math + +# Le piège classique +print(0.1 + 0.2 == 0.3) # False ! +print(0.1 + 0.2) # 0.30000000000000004 + +# La bonne façon +print(math.isclose(0.1 + 0.2, 0.3)) # True + +# Idem pour le tan(π/4) vu plus haut (qui vaut 0.9999999999999999) +print(math.isclose(math.tan(math.pi / 4), 1.0)) # True + +# Détecter les valeurs spéciales +print(math.isnan(float('nan'))) # True +print(math.isinf(float('inf'))) # True +``` + +> 📝 `math.isclose(a, b)` utilise par défaut une tolérance **relative** de `1e-09`. On peut l'ajuster : `math.isclose(a, b, rel_tol=1e-6)`, ou ajouter une tolérance **absolue** `abs_tol` (indispensable pour comparer à zéro, où la tolérance relative ne suffit pas). + ### Exemple pratique : Calculateur de probabilités ```python @@ -369,6 +395,17 @@ print(f"Soit : {(1/total_combinaisons)*100:.10f}%") Le module `random` permet de générer des nombres pseudo-aléatoires, utiles pour les simulations, les jeux, l'échantillonnage, etc. +> ⚠️ **`random` n'est PAS cryptographiquement sûr.** Ses valeurs sont *prédictibles* (générateur Mersenne Twister initialisé par une graine). Ne l'utilisez **jamais** pour des usages sensibles à la sécurité : mots de passe, jetons, clés, identifiants de session… Pour cela, utilisez le module **`secrets`** : +> +> ```python +> import secrets +> secrets.token_hex(16) # jeton hexadécimal aléatoire sûr +> secrets.choice(['a', 'b', 'c']) # choix sûr dans une séquence +> secrets.randbelow(100) # entier sûr dans [0, 100[ +> ``` +> +> `random` reste parfait pour les simulations, les jeux et les statistiques. + ### Import du module ```python @@ -606,7 +643,7 @@ import random # Définir une graine random.seed(42) -print(random.random()) # Toujours le même résultat avec seed=42 +print(random.random()) # 0.6394267984578837 (toujours le même avec seed=42) print(random.randint(1, 100)) # Réinitialiser avec la même graine @@ -614,7 +651,7 @@ random.seed(42) print(random.random()) # Même résultat qu'avant print(random.randint(1, 100)) # Même résultat qu'avant -# Graine aléatoire (par défaut, basée sur l'horloge système) +# Graine « aléatoire » : tirée de l'entropie de l'OS (os.urandom) si disponible, sinon de l'horloge random.seed() print(random.random()) # Résultat différent à chaque exécution ``` @@ -644,7 +681,7 @@ donnees = [2, 4, 6, 8, 10] # Moyenne arithmétique moyenne = statistics.mean(donnees) -print(f"Moyenne : {moyenne}") # 6.0 +print(f"Moyenne : {moyenne}") # 6 # Données avec différents types donnees_mixtes = [1, 2.5, 3, 4.5, 5] @@ -751,11 +788,11 @@ donnees = [2, 4, 6, 8, 10] # Variance de population variance_pop = statistics.pvariance(donnees) -print(f"Variance de population : {variance_pop}") # 8.0 +print(f"Variance de population : {variance_pop}") # 8 # Variance d'échantillon (estimation) variance_ech = statistics.variance(donnees) -print(f"Variance d'échantillon : {variance_ech}") # 10.0 +print(f"Variance d'échantillon : {variance_ech}") # 10 # Écart-type de population ecart_type_pop = statistics.pstdev(donnees) @@ -836,6 +873,8 @@ analyser_performances(temps_reponse) ## Corrélation et covariance +> 📝 `statistics.covariance()` et `statistics.correlation()` ont été ajoutées en **Python 3.10**. + ```python import statistics diff --git a/07-bibliotheques-standard/04-itertools-et-functools.md b/07-bibliotheques-standard/04-itertools-et-functools.md index 2493198..1af661b 100644 --- a/07-bibliotheques-standard/04-itertools-et-functools.md +++ b/07-bibliotheques-standard/04-itertools-et-functools.md @@ -246,6 +246,8 @@ for age, groupe in itertools.groupby(personnes_triees, key=lambda p: p['age']): # Âge 30: ['Charlie', 'David'] ``` +> 📝 **Pourquoi faut-il trier avant `groupby` ?** `groupby` fait **un seul passage** sur la séquence et démarre un nouveau groupe **chaque fois que la clé change** par rapport à l'élément précédent : il ne regroupe donc que les éléments **consécutifs**. C'est pourquoi, dans le premier exemple, les `'A'` séparés par des `'B'`/`'C'` produisent **deux** groupes distincts. Pour obtenir un seul groupe par clé, il faut d'abord **trier** par cette même clé, ce qui rassemble toutes ses occurrences côte à côte. + ### Exemple pratique : Traitement de logs ```python @@ -478,6 +480,8 @@ concatenation = itertools.accumulate(mots, lambda a, b: a + b) print(list(concatenation)) # ['Hello', 'Hello ', 'Hello World', 'Hello World!'] ``` +> 📝 **Le module `operator`.** `operator.mul` (ci-dessus), `operator.add`, `operator.sub`… sont simplement les opérateurs `*`, `+`, `-`… sous forme de **fonctions** — pratique pour les passer en argument à `accumulate`, `reduce`, `map`… (`operator.add` est plus lisible et un peu plus rapide que `lambda x, y: x + y`). Le module fournit aussi `itemgetter('cle')` / `itemgetter(0)` (extraire une clé de dictionnaire ou un index) et `attrgetter('attribut')`, très utiles comme `key=` de `sorted()` ou `groupby()` — on s'en sert dans l'exemple complet plus bas. + ### tee() - Dupliquer un itérateur ```python @@ -491,7 +495,7 @@ print(list(iter1)) # [1, 2, 3, 4, 5] print(list(iter2)) # [1, 2, 3, 4, 5] print(list(iter3)) # [1, 2, 3, 4, 5] -# Utile pour parcourir plusieurs fois sans stocker en mémoire +# Utile pour re-parcourir une source à passage unique (ex. un générateur) data = range(1000000) iter_pairs, iter_impairs = itertools.tee(data, 2) @@ -499,6 +503,8 @@ pairs = (x for x in iter_pairs if x % 2 == 0) impairs = (x for x in iter_impairs if x % 2 == 1) ``` +> ⚠️ `tee()` **met en tampon** les éléments déjà produits tant que tous les itérateurs ne les ont pas consommés. Si vous épuisez un itérateur **avant** l'autre (au lieu de les faire avancer en parallèle), `tee` finit par stocker **toute** la séquence en mémoire — dans ce cas, convertir la source en `list()` une fois est préférable. + ### zip_longest() - Zip avec remplissage ```python @@ -525,12 +531,31 @@ for nom, age, ville in personnes: print(f"{nom} - {age} ans - {ville}") ``` +### pairwise() et batched() + +```python +import itertools + +# pairwise() (Python 3.10+) : paires d'éléments consécutifs (fenêtre glissante de 2) +temperatures = [12, 15, 18, 14, 20] +for hier, aujourdhui in itertools.pairwise(temperatures): + print(f"{hier}° -> {aujourdhui}° (variation : {aujourdhui - hier:+d})") +# 12° -> 15° (+3), 15° -> 18° (+3), 18° -> 14° (-4), 14° -> 20° (+6) + +# batched() (Python 3.12+) : découpe un itérable en lots de taille fixe +for lot in itertools.batched("ABCDEFG", 3): + print(lot) +# ('A', 'B', 'C'), ('D', 'E', 'F'), ('G',) <- le dernier lot peut être plus court +``` + --- ## Le module `functools` - Outils de programmation fonctionnelle Le module `functools` fournit des outils pour travailler avec des fonctions d'ordre supérieur (fonctions qui prennent ou retournent d'autres fonctions). +> 📝 Certaines de ces fonctions — `reduce`, `partial`, `wraps`, `lru_cache`/`cache` — ont déjà été rencontrées au chapitre [5 sur la programmation fonctionnelle](/05-programmation-fonctionnelle/README.md). Ce chapitre les regroupe comme **référence du module `functools`** et y ajoute des outils nouveaux : `total_ordering` et `singledispatch`. + ### Import du module ```python @@ -715,6 +740,8 @@ print(f"\nInfos cache : {fibonacci_rapide.cache_info()}") fibonacci_rapide.cache_clear() ``` +> 📝 **Pourquoi le cache change-t-il tout ici ?** Sans cache, `fibonacci_lent(30)` rappelle `fibonacci_lent(29)` **et** `fibonacci_lent(28)`, qui redemandent à leur tour les mêmes valeurs… Le même calcul est refait un nombre **exponentiel** de fois — **2 692 537 appels** pour `n=30` ! Avec `lru_cache`, chaque `fibonacci(k)` n'est calculé qu'**une seule fois** (ici 31 calculs, de `0` à `30`) puis relu depuis le cache : on passe d'un coût exponentiel à un coût **linéaire**. C'est le principe de la *mémorisation* (programmation dynamique), et `cache_info()` le confirme (`hits=28, misses=31`). + ### Exemple pratique : Calcul de factorielles avec cache ```python @@ -972,6 +999,8 @@ analyseur.statistiques_globales() print(f"\n🔍 Cache info : {analyseur.total_par_client.cache_info()}") ``` +> 📝 **Attention — `@lru_cache` sur une méthode :** ici `total_par_client` est une *méthode* décorée par `lru_cache`. Cela fonctionne, mais le cache conserve une référence à l'instance (`self`) tant qu'il existe : sur des objets à longue durée de vie, cela peut empêcher leur libération par le ramasse-miettes (fuite mémoire). Pour des cas réels, préférez mettre en cache une **fonction** indépendante, ou recalculez le total une fois et stockez-le dans l'instance. + --- ## Exemple pratique : Pipeline de traitement de texte diff --git a/07-bibliotheques-standard/05-logging-et-configuration.md b/07-bibliotheques-standard/05-logging-et-configuration.md index 5bc47b0..c8bbe97 100644 --- a/07-bibliotheques-standard/05-logging-et-configuration.md +++ b/07-bibliotheques-standard/05-logging-et-configuration.md @@ -221,6 +221,8 @@ logger.info("Message d'info - fichier ET console") logger.error("Message d'erreur - fichier ET console") ``` +> 📝 **Deux niveaux qui se cumulent : logger puis handler.** Un message doit franchir **deux** filtres pour être émis : d'abord le niveau du **logger** (porte d'entrée globale), puis le niveau de **chaque handler** (filtre propre à chaque destination). Ici le logger est à `DEBUG` (il laisse tout passer), mais le handler console est à `INFO` : un `logger.debug(...)` traverse le logger, atteint les deux handlers, est écrit par le handler fichier (seuil `DEBUG`) et **rejeté** par le handler console (seuil `INFO`) — d'où « seulement dans le fichier ». ⚠️ Conséquence : si le niveau du *logger* est trop haut (ex. `WARNING`), le message est bloqué d'emblée et **aucun** handler ne le voit, même réglé plus bas. + --- ## Loggers nommés @@ -274,6 +276,8 @@ logger_fonction.info("Log de la fonction") # app.module.fonction - Log de la fonction ``` +> 📝 **À quoi sert cette hiérarchie ? À la propagation.** Un message émis par un logger « enfant » **remonte** automatiquement vers ses parents : `app.module.fonction` → `app.module` → `app` → le logger racine, et **chaque ancêtre applique ses propres handlers**. C'est pratique (configurer les handlers une seule fois sur `app` couvre tous ses descendants), mais cela peut produire des messages **en double** si un enfant et un parent possèdent chacun un handler. Pour couper cette remontée sur un logger donné, on met `propagate = False` — c'est exactement le rôle de l'option `'propagate': False` que vous verrez dans les configurations par dictionnaire plus bas. + --- ## Exemple pratique : Application avec logging @@ -872,8 +876,8 @@ logging.basicConfig(level=logging.DEBUG) def fonction_risquee(): try: - # Code qui pourrait échouer - resultat = 10 / 0 + # Code qui pourrait échouer (division par zéro volontaire) + 10 / 0 except Exception: # logging.exception() est équivalent à logging.error(..., exc_info=True) logging.exception("Une erreur s'est produite") @@ -920,7 +924,7 @@ if __name__ == "__main__": ```python # ❌ Dans une bibliothèque/module import logging -logging.basicConfig(...) # NE JAMAIS FAIRE ÇA ! +logging.basicConfig(level=logging.INFO) # NE JAMAIS FAIRE ÇA ! # ✅ Dans une bibliothèque/module import logging @@ -961,8 +965,8 @@ logger.debug("Valeur: %s", variable) logger.debug("Utilisateur %s a effectué %d actions", username, count) ``` -> **Note :** Le module `logging` utilise le formatage `%` par défaut (comme `"message %s" % args`). -> La syntaxe `{}` (style `str.format()`) ne fonctionne **pas** avec les arguments positionnels du logger. +> **Note :** Le module `logging` utilise le formatage `%` par défaut (comme `"message %s" % args`). +> La syntaxe `{}` (style `str.format()`) ne fonctionne **pas** avec les arguments positionnels du logger. > Le lazy formatting diffère le coût de la **conversion en chaîne**, mais les expressions passées en arguments sont toujours évaluées. ### 6. Éviter les informations sensibles dans les logs @@ -1091,6 +1095,10 @@ LOGGING_CONFIG = { } } +# Les FileHandler ne créent pas leur dossier parent : on crée 'logs/' d'abord +import os +os.makedirs('logs', exist_ok=True) + logging.config.dictConfig(LOGGING_CONFIG) class SimpleWebApp: @@ -1257,7 +1265,7 @@ logger.debug("Valeurs: x=%d, y=%d", x, y) # Exceptions try: - resultat = 10 / 0 + 10 / 0 except Exception: logger.exception("Erreur de division") ``` diff --git a/07-bibliotheques-standard/06-typing-annotations-avancees.md b/07-bibliotheques-standard/06-typing-annotations-avancees.md index 2d657a9..5f4d2d6 100644 --- a/07-bibliotheques-standard/06-typing-annotations-avancees.md +++ b/07-bibliotheques-standard/06-typing-annotations-avancees.md @@ -14,6 +14,8 @@ Les annotations de type offrent plusieurs avantages : **Important** : Les annotations de type ne changent pas le comportement de Python à l'exécution. Ce sont des "indices" pour les développeurs et les outils d'analyse. +> 📝 **Vous avez déjà vu les bases.** Les annotations simples (variables, fonctions, collections `list[...]`/`dict[...]`, `X | None`) ont été introduites au chapitre [1.6 — Type Hints et annotations](/01-fondamentaux-et-syntaxe/06-type-hints-et-annotations.md). Les premières sections ci-dessous en font un **rappel rapide** ; ce chapitre se concentre ensuite sur les types **avancés** (`TypeVar`, `Generic`, `Protocol`, `Literal`, `@overload`…). + --- ## Annotations de type basiques @@ -84,7 +86,7 @@ resultat: str | None = None # remplace Optional[str] from typing import Any, Callable, TypeVar, Generic, Protocol, Literal, Final ``` -> **Note historique :** Avant Python 3.9, il fallait importer `List`, `Dict`, `Tuple`, `Set` depuis `typing`. Avant Python 3.10, il fallait utiliser `Union` et `Optional`. Ces formes fonctionnent encore mais sont considérées comme **obsolètes**. +> **Note historique :** Avant Python 3.9, il fallait importer `List`, `Dict`, `Tuple`, `Set` depuis `typing` ; ces alias sont maintenant **dépréciés** au profit des types natifs (`list`, `dict`…). Avant Python 3.10, on utilisait `Union[X, Y]` et `Optional[X]` ; ceux-ci ne sont **pas** dépréciés et restent parfaitement valides, mais la syntaxe `X | Y` leur est aujourd'hui préférée. --- @@ -287,6 +289,16 @@ users: ListeUtilisateurs = [ > **Note :** `TypeAlias` rend l'intention explicite — sans lui, `UserId = int` pourrait être confondu avec une simple variable. +> 🆕 **Python 3.12+ — l'instruction `type` (PEP 695) :** depuis Python 3.12, la façon recommandée de définir un alias est l'instruction `type`, plus concise et qui gère nativement les références anticipées : +> +> ```python +> type UserId = int +> type Coordonnees = tuple[float, float] +> type ListeUtilisateurs = list[dict[str, str]] +> ``` +> +> `typing.TypeAlias` (montré ci-dessus) reste nécessaire si vous ciblez Python 3.10/3.11 ; sur 3.12+, il est déprécié au profit de l'instruction `type`. + --- ## Callable - Types de fonctions @@ -430,6 +442,19 @@ pile_strings.empiler("b") print(pile_strings.depiler()) # "b" ``` +> 🆕 **Python 3.12+ — syntaxe générique native (PEP 695) :** depuis Python 3.12, on paramètre fonctions et classes **sans** importer `TypeVar` ni hériter de `Generic`, grâce à la syntaxe `[T]` : +> +> ```python +> def premier_element[T](liste: list[T]) -> T: +> return liste[0] +> +> class Pile[T]: +> def __init__(self) -> None: +> self.items: list[T] = [] +> ``` +> +> Les formes avec `TypeVar` / `Generic[T]` ci-dessus restent valables (et nécessaires pour Python 3.10/3.11). + ### Exemple pratique : Cache générique ```python @@ -634,6 +659,8 @@ dessiner_forme(carre) # OK # Pas besoin d'héritage explicite! ``` +> 📝 **Le `...` dans le corps d'une méthode.** Les `...` (l'objet `Ellipsis`) que vous voyez dans `def draw(self) -> str: ...` forment un **corps vide** : ils signifient « cette méthode n'a pas d'implémentation ici, on ne décrit que sa signature ». C'est la convention pour les `Protocol`, les surcharges `@overload` et les *stubs* de types. On pourrait écrire `pass` à la place, mais `...` est l'usage idiomatique pour signaler « signature seulement ». (À ne pas confondre avec `tuple[int, ...]`, où `...` signifie « longueur variable ».) + ### Exemple pratique : Protocol pour un système de stockage ```python @@ -716,6 +743,99 @@ traiter_donnees(stockage_memoire, "test", "valeur") --- +## TypedDict - Dictionnaires structurés + +Un dictionnaire annoté `dict[str, X]` impose **le même** type de valeur pour **toutes** les clés. Or beaucoup de dictionnaires réels ont des clés **connues à l'avance**, chacune avec son propre type — typiquement un enregistrement ou une réponse JSON. `TypedDict` (PEP 589, Python 3.8+) décrit précisément cette structure. + +```python +from typing import TypedDict + +class Film(TypedDict): + titre: str + annee: int + note: float + +# À l'exécution, c'est un dictionnaire ordinaire ; ce sont les outils +# (mypy, IDE) qui vérifient les clés présentes et le type de chaque valeur. +inception: Film = {"titre": "Inception", "annee": 2010, "note": 8.8} + +def resumer(film: Film) -> str: + return f"{film['titre']} ({film['annee']}) - {film['note']}/10" + +print(resumer(inception)) # Inception (2010) - 8.8/10 + +# Erreurs repérées par mypy (mais acceptées par Python à l'exécution) : +# {"titre": "X", "annee": "2010", "note": 7.0} # annee : str au lieu de int +# {"titre": "X", "annee": 2010} # clé 'note' manquante +# {"titre": "X", "annee": 2010, "note": 7.0, "duree": 148} # clé 'duree' inconnue +``` + +Par défaut, **toutes les clés sont obligatoires**. Pour les rendre facultatives, on passe `total=False` : + +```python +from typing import TypedDict + +class Preferences(TypedDict, total=False): + couleur: str + taille: int + +p1: Preferences = {"couleur": "rouge"} # OK : taille absente +p2: Preferences = {"couleur": "bleu", "taille": 42} # OK +p3: Preferences = {} # OK : tout est facultatif +``` + +> 🆕 **Python 3.11+ — granularité par clé (`NotRequired` / `Required`, PEP 655) :** au lieu de tout obligatoire ou tout facultatif, on mélange les deux dans une même classe : +> +> ```python +> from typing import TypedDict, NotRequired +> +> class Utilisateur(TypedDict): +> nom: str # requis +> email: str # requis +> telephone: NotRequired[str] # facultatif +> ``` + +> **`TypedDict` ou `@dataclass` ?** Les deux décrivent une structure à champs nommés. On choisit `TypedDict` quand les données **sont déjà des dictionnaires** (issues de JSON, d'une API, d'une base de données) et qu'on veut les typer sans changer leur nature ; on choisit une `dataclass` quand on crée de **vrais objets**, avec méthodes, valeurs par défaut et comportement propre. + +--- + +## `Self` - Le type de l'instance courante (Python 3.11+) + +`Self` (depuis Python 3.11) annote une méthode qui **renvoie l'instance elle-même**. C'est idéal pour les interfaces « fluides » (chaînage de méthodes) et toute méthode qui retourne `self`. + +```python +from typing import Self + +class RequeteSQL: + """Construit une requête par chaînage de méthodes (interface fluide).""" + + def __init__(self) -> None: + self.parties: list[str] = [] + + def select(self, colonnes: str) -> Self: # Self = « le type de cette classe » + self.parties.append(f"SELECT {colonnes}") + return self + + def depuis(self, table: str) -> Self: + self.parties.append(f"FROM {table}") + return self + + def ou(self, condition: str) -> Self: + self.parties.append(f"WHERE {condition}") + return self + + def construire(self) -> str: + return " ".join(self.parties) + +# Chaînage : chaque méthode renvoie l'instance +requete = RequeteSQL().select("*").depuis("utilisateurs").ou("id = 1").construire() +print(requete) # SELECT * FROM utilisateurs WHERE id = 1 +``` + +> 📝 Avant Python 3.11, il fallait simuler ce comportement avec un `TypeVar` lié (`TypeVar("T", bound="RequeteSQL")`), plus verbeux. Bonus : si une **sous-classe** hérite d'une méthode annotée `-> Self`, le type désigné est automatiquement celui de la sous-classe. + +--- + ## NewType - Créer de nouveaux types `NewType` crée un type distinct pour éviter les confusions entre types similaires. @@ -1068,7 +1188,7 @@ resultat_erreur: str = additionner(5, 3) # Erreur de type! mypy exemple.py # Sortie: -# exemple.py:5: error: Incompatible types in assignment (expression has type "int", variable has type "str") +# exemple.py:6: error: Incompatible types in assignment (expression has type "int", variable has type "str") [assignment] ``` ### Configuration mypy diff --git a/07-bibliotheques-standard/exemples/01_02_os_path_et_fichiers.py b/07-bibliotheques-standard/exemples/01_02_os_path_et_fichiers.py index 50a4e17..3ac9550 100644 --- a/07-bibliotheques-standard/exemples/01_02_os_path_et_fichiers.py +++ b/07-bibliotheques-standard/exemples/01_02_os_path_et_fichiers.py @@ -61,7 +61,7 @@ print("\n=== Renommer un fichier ===") os.rename("_test_fichier.txt", "_test_renomme.txt") -print(f"Fichier renommé : '_test_fichier.txt' -> '_test_renomme.txt'") +print("Fichier renommé : '_test_fichier.txt' -> '_test_renomme.txt'") print(f"'_test_renomme.txt' existe ? {os.path.exists('_test_renomme.txt')}") print(f"'_test_fichier.txt' existe ? {os.path.exists('_test_fichier.txt')}") diff --git a/07-bibliotheques-standard/exemples/01_03_sys_interpreteur.py b/07-bibliotheques-standard/exemples/01_03_sys_interpreteur.py index 887b09a..cf2008e 100644 --- a/07-bibliotheques-standard/exemples/01_03_sys_interpreteur.py +++ b/07-bibliotheques-standard/exemples/01_03_sys_interpreteur.py @@ -7,7 +7,6 @@ import sys import subprocess -import os from pathlib import Path # --- Informations sur l'interpréteur --- diff --git a/07-bibliotheques-standard/exemples/01_05_exemple_complet_analyseur.py b/07-bibliotheques-standard/exemples/01_05_exemple_complet_analyseur.py index c45f699..1223f27 100644 --- a/07-bibliotheques-standard/exemples/01_05_exemple_complet_analyseur.py +++ b/07-bibliotheques-standard/exemples/01_05_exemple_complet_analyseur.py @@ -82,7 +82,7 @@ def analyser_projet(repertoire): print("Pas de depot Git") # Informations système - print(f"\nEnvironnement :") + print("\nEnvironnement :") print(f" Python : {sys.version.split()[0]}") print(f" Plateforme : {sys.platform}") print(f" Répertoire de travail : {os.getcwd()}") diff --git a/07-bibliotheques-standard/exemples/02_01_datetime_base.py b/07-bibliotheques-standard/exemples/02_01_datetime_base.py index 1552ff6..39ad717 100644 --- a/07-bibliotheques-standard/exemples/02_01_datetime_base.py +++ b/07-bibliotheques-standard/exemples/02_01_datetime_base.py @@ -64,6 +64,14 @@ print(f"'2025-10-27' -> {date1}") print(f"'10/27/25' -> {date3}") +# --- Format ISO 8601 (isoformat / fromisoformat) --- +print("\n=== Format ISO 8601 ===") + +dt = datetime(2025, 10, 27, 14, 30, 45) +iso = dt.isoformat() +print(f"isoformat() : {iso}") +print(f"fromisoformat() : {datetime.fromisoformat(iso)}") # l'opération inverse exacte + # --- Calculer l'âge --- print("\n=== Calculer l'âge ===") diff --git a/07-bibliotheques-standard/exemples/02_04_fuseaux_horaires.py b/07-bibliotheques-standard/exemples/02_04_fuseaux_horaires.py index 8d11cb6..7d79bab 100644 --- a/07-bibliotheques-standard/exemples/02_04_fuseaux_horaires.py +++ b/07-bibliotheques-standard/exemples/02_04_fuseaux_horaires.py @@ -26,7 +26,7 @@ date_tokyo = date_paris.astimezone(ZoneInfo("Asia/Tokyo")) date_ny = date_paris.astimezone(ZoneInfo("America/New_York")) -print(f"14h30 à Paris :") +print("14h30 à Paris :") print(f" -> Tokyo : {date_tokyo.strftime('%H:%M %Z')}") print(f" -> New York : {date_ny.strftime('%H:%M %Z')}") diff --git a/07-bibliotheques-standard/exemples/03_02_math_avance.py b/07-bibliotheques-standard/exemples/03_02_math_avance.py index 32df3e3..e88aaf4 100644 --- a/07-bibliotheques-standard/exemples/03_02_math_avance.py +++ b/07-bibliotheques-standard/exemples/03_02_math_avance.py @@ -14,8 +14,11 @@ print(f"pow(2, 3) = {math.pow(2, 3)}") print(f"pow(5, 2) = {math.pow(5, 2)}") print(f"2 ** 3 = {2 ** 3}") -print(f"cbrt(27) = {math.cbrt(27)}") -print(f"27 ** (1/3) = {27 ** (1/3)}") +if hasattr(math, "cbrt"): # math.cbrt : Python 3.11+ + print(f"cbrt(27) = {math.cbrt(27)}") +else: + print("cbrt : nécessite Python 3.11+") +print(f"27 ** (1/3) = {27 ** (1/3)}") # alternative universelle print(f"exp(1) = {math.exp(1)}") print(f"exp(2) = {math.exp(2)}") print(f"expm1(0.001) = {math.expm1(0.001)}") @@ -103,9 +106,9 @@ def angle_entre_points(x1, y1, x2, y2): print(f"comb(5, 2) = {math.comb(5, 2)}") print(f"perm(5, 2) = {math.perm(5, 2)}") -nombres = [0.1] * 10 -print(f"\nsum([0.1]*10) = {sum(nombres)}") -print(f"fsum([0.1]*10) = {math.fsum(nombres)}") +nombres = [1e16, 1, -1e16] # un petit nombre (1) noyé entre deux grands +print(f"\nsum([1e16, 1, -1e16]) = {sum(nombres)}") # 0.0 : le +1 est absorbé puis annulé +print(f"fsum([1e16, 1, -1e16]) = {math.fsum(nombres)}") # 1.0 : somme exacte print(f"prod([2, 3, 4]) = {math.prod([2, 3, 4])}") print(f"hypot(3, 4) = {math.hypot(3, 4)}") print(f"hypot(5, 12) = {math.hypot(5, 12)}") @@ -125,3 +128,11 @@ def angle_entre_points(x1, y1, x2, y2): print(f"log(0) -> ValueError : {e}") print(f"type(sqrt(4)) = {type(math.sqrt(4))}") + +# --- Comparer des flottants : isclose --- +print("\n=== Comparer des flottants (isclose) ===") +print(f"0.1 + 0.2 == 0.3 : {0.1 + 0.2 == 0.3}") +print(f"isclose(0.1+0.2, 0.3) : {math.isclose(0.1 + 0.2, 0.3)}") +print(f"isclose(tan(pi/4), 1.0) : {math.isclose(math.tan(math.pi / 4), 1.0)}") +print(f"isnan(nan) : {math.isnan(float('nan'))}") +print(f"isinf(inf) : {math.isinf(float('inf'))}") diff --git a/07-bibliotheques-standard/exemples/03_03_random.py b/07-bibliotheques-standard/exemples/03_03_random.py index 21e8fab..88daeb5 100644 --- a/07-bibliotheques-standard/exemples/03_03_random.py +++ b/07-bibliotheques-standard/exemples/03_03_random.py @@ -132,3 +132,12 @@ def cartes_restantes(self): print(f"normalvariate(100, 15) = {random.normalvariate(100, 15):.2f}") print(f"triangular(0, 10, 5) = {random.triangular(0, 10, 5):.4f}") print(f"expovariate(1.5) = {random.expovariate(1.5):.4f}") + +# --- secrets : aléatoire cryptographiquement sûr --- +# random n'est PAS sûr pour la sécurité (mots de passe, jetons...) : utiliser secrets +print("\n=== secrets (aléatoire sécurisé) ===") +import secrets + +print(f"token_hex(16) : {secrets.token_hex(16)}") +print(f"choice : {secrets.choice(['rouge', 'bleu', 'vert'])}") +print(f"randbelow(100) : {secrets.randbelow(100)}") diff --git a/07-bibliotheques-standard/exemples/03_04_statistics.py b/07-bibliotheques-standard/exemples/03_04_statistics.py index 1936d2f..cd46cc6 100644 --- a/07-bibliotheques-standard/exemples/03_04_statistics.py +++ b/07-bibliotheques-standard/exemples/03_04_statistics.py @@ -131,7 +131,7 @@ ventes_glaces = [50, 65, 85, 110, 140, 160, 180, 210, 250, 280] correlation = statistics.correlation(temperatures, ventes_glaces) -print(f"\nTempérature vs ventes de glaces :") +print("\nTempérature vs ventes de glaces :") print(f"Corrélation : {correlation:.3f}") if correlation > 0.7: print("-> Forte corrélation positive") diff --git a/07-bibliotheques-standard/exemples/03_05_casino_monte_carlo.py b/07-bibliotheques-standard/exemples/03_05_casino_monte_carlo.py index b11c455..6d2352d 100644 --- a/07-bibliotheques-standard/exemples/03_05_casino_monte_carlo.py +++ b/07-bibliotheques-standard/exemples/03_05_casino_monte_carlo.py @@ -68,7 +68,7 @@ def blackjack_simplifie(self, mise): 'resultat': resultat, 'gain': gain, 'capital': self.capital} def statistiques(self): - print(f"\nStatistiques de jeu") + print("\nStatistiques de jeu") print("=" * 50) print(f"Capital initial : {self.historique[0]:.2f} EUR") print(f"Capital actuel : {self.capital:.2f} EUR") diff --git a/07-bibliotheques-standard/exemples/04_03_itertools_combinatoires.py b/07-bibliotheques-standard/exemples/04_03_itertools_combinatoires.py index 1c9ad2e..67ea477 100644 --- a/07-bibliotheques-standard/exemples/04_03_itertools_combinatoires.py +++ b/07-bibliotheques-standard/exemples/04_03_itertools_combinatoires.py @@ -116,3 +116,19 @@ personnes = itertools.zip_longest(noms, ages, villes, fillvalue='N/A') for nom, age, ville in personnes: print(f" {nom} - {age} ans - {ville}") + +# --- pairwise (3.10+) et batched (3.12+) --- +print("\n=== pairwise et batched ===") + +temperatures = [12, 15, 18, 14, 20] +print("pairwise (paires consécutives) :") +for hier, aujourdhui in itertools.pairwise(temperatures): + print(f" {hier} -> {aujourdhui} (variation : {aujourdhui - hier:+d})") + +# batched n'existe que depuis Python 3.12 +if hasattr(itertools, "batched"): + print("batched (lots de 3) :") + for lot in itertools.batched("ABCDEFG", 3): + print(f" {lot}") +else: + print("batched : nécessite Python 3.12+") diff --git a/07-bibliotheques-standard/exemples/04_06_exemple_complet_transactions.py b/07-bibliotheques-standard/exemples/04_06_exemple_complet_transactions.py index a3afe86..ab0c17e 100644 --- a/07-bibliotheques-standard/exemples/04_06_exemple_complet_transactions.py +++ b/07-bibliotheques-standard/exemples/04_06_exemple_complet_transactions.py @@ -67,7 +67,7 @@ def statistiques_globales(self): print(f"Montant moyen : {sum(montants) / len(montants):.2f} EUR") print(f"Montant min : {min(montants):.2f} EUR") print(f"Montant max : {max(montants):.2f} EUR") - print(f"\nEvolution CA cumulé :") + print("\nEvolution CA cumulé :") for i, total in enumerate(cumul, 1): barre = "#" * int(total / 20) print(f" Transaction {i}: {total:7.2f} EUR {barre}") @@ -113,7 +113,7 @@ def filtrer_mots_courts(mots, longueur_min=3): for mot, freq in itertools.islice(compteur.most_common(), 5): print(f" {mot:15s} : {freq} fois") -print(f"\nStatistiques :") +print("\nStatistiques :") print(f" Nombre total de mots : {len(tous_les_mots)}") print(f" Mots uniques : {len(compteur)}") print(f" Hapax (1 seule fois) : {sum(1 for f in compteur.values() if f == 1)}") diff --git a/07-bibliotheques-standard/exemples/05_02_logging_fichiers_handlers.py b/07-bibliotheques-standard/exemples/05_02_logging_fichiers_handlers.py index 0fc534a..ea49b4e 100644 --- a/07-bibliotheques-standard/exemples/05_02_logging_fichiers_handlers.py +++ b/07-bibliotheques-standard/exemples/05_02_logging_fichiers_handlers.py @@ -120,7 +120,7 @@ # Vérifier les fichiers créés fichiers_log = sorted([f for f in os.listdir(tmpdir) if f.startswith('app_rotating')]) -print(f"Fichiers créés par rotation :") +print("Fichiers créés par rotation :") for f in fichiers_log: taille = os.path.getsize(os.path.join(tmpdir, f)) print(f" {f} ({taille} octets)") diff --git a/07-bibliotheques-standard/exemples/05_03_loggers_nommes_hierarchie.py b/07-bibliotheques-standard/exemples/05_03_loggers_nommes_hierarchie.py index c1c0e1e..d292a87 100644 --- a/07-bibliotheques-standard/exemples/05_03_loggers_nommes_hierarchie.py +++ b/07-bibliotheques-standard/exemples/05_03_loggers_nommes_hierarchie.py @@ -93,12 +93,12 @@ def filter(self, record): logger_filtre.critical("Critical - dans errors.log") # Vérifier les fichiers -print(f"\nContenu de errors.log :") +print("\nContenu de errors.log :") with open(errors_log) as f: for ligne in f: print(f" {ligne.strip()}") -print(f"\nContenu de info.log :") +print("\nContenu de info.log :") with open(info_log) as f: for ligne in f: print(f" {ligne.strip()}") @@ -111,4 +111,4 @@ def filter(self, record): info_handler.close() shutil.rmtree(tmpdir) -print(f"\nNettoyage terminé.") +print("\nNettoyage terminé.") diff --git a/07-bibliotheques-standard/exemples/05_04_dictconfig.py b/07-bibliotheques-standard/exemples/05_04_dictconfig.py index e8e69d5..538c611 100644 --- a/07-bibliotheques-standard/exemples/05_04_dictconfig.py +++ b/07-bibliotheques-standard/exemples/05_04_dictconfig.py @@ -88,12 +88,12 @@ logger.error("Message d'erreur") # Vérifier les fichiers -print(f"\nContenu de app.log :") +print("\nContenu de app.log :") with open(app_log) as f: for ligne in f: print(f" {ligne.strip()}") -print(f"\nContenu de errors.log :") +print("\nContenu de errors.log :") with open(errors_log) as f: for ligne in f: print(f" {ligne.strip()}") @@ -159,7 +159,7 @@ logger_ini.warning("Avertissement via config INI") # Vérifier le fichier -print(f"\nContenu de ini_app.log :") +print("\nContenu de ini_app.log :") with open(ini_log) as f: for ligne in f: print(f" {ligne.strip()}") @@ -167,6 +167,6 @@ # ========================================== # Nettoyage # ========================================== -print(f"\nNettoyage du dossier temporaire...") +print("\nNettoyage du dossier temporaire...") shutil.rmtree(tmpdir) print("Nettoyage terminé.") diff --git a/07-bibliotheques-standard/exemples/05_06_logging_exceptions.py b/07-bibliotheques-standard/exemples/05_06_logging_exceptions.py index 137cd57..f9bfd1d 100644 --- a/07-bibliotheques-standard/exemples/05_06_logging_exceptions.py +++ b/07-bibliotheques-standard/exemples/05_06_logging_exceptions.py @@ -40,7 +40,7 @@ def diviser(a, b): def fonction_risquee(): try: - resultat = 10 / 0 + 10 / 0 # déclenche une ZeroDivisionError (capturée ci-dessous) except Exception: # logging.exception() est équivalent à logging.error(..., exc_info=True) logging.exception("Une erreur s'est produite") diff --git a/07-bibliotheques-standard/exemples/05_07_exemple_complet_ecommerce.py b/07-bibliotheques-standard/exemples/05_07_exemple_complet_ecommerce.py index 3d19719..f4989e7 100644 --- a/07-bibliotheques-standard/exemples/05_07_exemple_complet_ecommerce.py +++ b/07-bibliotheques-standard/exemples/05_07_exemple_complet_ecommerce.py @@ -241,7 +241,7 @@ def main(): print(f" {ligne.strip()}") # Nettoyage - print(f"\nNettoyage des logs temporaires...") + print("\nNettoyage des logs temporaires...") shutil.rmtree(tmpdir) print("Nettoyage terminé.") diff --git a/07-bibliotheques-standard/exemples/05_08_exemple_webapp.py b/07-bibliotheques-standard/exemples/05_08_exemple_webapp.py index fc23f13..fb4c441 100644 --- a/07-bibliotheques-standard/exemples/05_08_exemple_webapp.py +++ b/07-bibliotheques-standard/exemples/05_08_exemple_webapp.py @@ -7,7 +7,6 @@ import logging import logging.config -from datetime import datetime import tempfile import os import shutil @@ -180,6 +179,6 @@ def process_data(self, data): print(" (vide)") # Nettoyage -print(f"\nNettoyage des logs temporaires...") +print("\nNettoyage des logs temporaires...") shutil.rmtree(tmpdir) print("Nettoyage terminé.") diff --git a/07-bibliotheques-standard/exemples/06_01_annotations_base.py b/07-bibliotheques-standard/exemples/06_01_annotations_base.py index b3e7dba..d847096 100644 --- a/07-bibliotheques-standard/exemples/06_01_annotations_base.py +++ b/07-bibliotheques-standard/exemples/06_01_annotations_base.py @@ -22,7 +22,7 @@ # Python n'applique PAS ces types à l'exécution age_test: int = 25 -age_test = "vingt-cinq" # Pas d'erreur à l'exécution ! +age_test = "vingt-cinq" # Pas d'erreur à l'exécution, mais mypy signale : Incompatible types print(f"\nage_test: {age_test} (type: {type(age_test).__name__}) - Python n'empêche pas ça") # ========================================== diff --git a/07-bibliotheques-standard/exemples/06_02_union_optional_any.py b/07-bibliotheques-standard/exemples/06_02_union_optional_any.py index fcd1c26..f3c73d3 100644 --- a/07-bibliotheques-standard/exemples/06_02_union_optional_any.py +++ b/07-bibliotheques-standard/exemples/06_02_union_optional_any.py @@ -116,6 +116,6 @@ def obtenir_position() -> Coordonnees: pos = obtenir_position() print(f"Position: {pos}") -print(f"Liste d'utilisateurs:") +print("Liste d'utilisateurs:") for u in users: print(f" {u}") diff --git a/07-bibliotheques-standard/exemples/06_04_generic_classes.py b/07-bibliotheques-standard/exemples/06_04_generic_classes.py index d20378f..c6019ae 100644 --- a/07-bibliotheques-standard/exemples/06_04_generic_classes.py +++ b/07-bibliotheques-standard/exemples/06_04_generic_classes.py @@ -132,3 +132,39 @@ def nettoyer_expires(self) -> int: # Nettoyer les expirés (rien à nettoyer, tout est frais) nb_expires = cache_users.nettoyer_expires() print(f"Entrées expirées nettoyées: {nb_expires}") + +# ========================================== +# 3. Self : méthodes qui renvoient l'instance (Python 3.11+) +# ========================================== +print("\n=== Self (interface fluide, 3.11+) ===") + +import sys + +if sys.version_info >= (3, 11): + from typing import Self + + class RequeteSQL: + """Construit une requête par chaînage (chaque méthode renvoie Self).""" + + def __init__(self) -> None: + self.parties: list[str] = [] + + def select(self, colonnes: str) -> Self: + self.parties.append(f"SELECT {colonnes}") + return self + + def depuis(self, table: str) -> Self: + self.parties.append(f"FROM {table}") + return self + + def ou(self, condition: str) -> Self: + self.parties.append(f"WHERE {condition}") + return self + + def construire(self) -> str: + return " ".join(self.parties) + + requete = RequeteSQL().select("*").depuis("utilisateurs").ou("id = 1").construire() + print(f"Requête : {requete}") +else: + print("Self : nécessite Python 3.11+") diff --git a/07-bibliotheques-standard/exemples/06_05_literal_final_protocol.py b/07-bibliotheques-standard/exemples/06_05_literal_final_protocol.py index 34bb36b..d64cbaf 100644 --- a/07-bibliotheques-standard/exemples/06_05_literal_final_protocol.py +++ b/07-bibliotheques-standard/exemples/06_05_literal_final_protocol.py @@ -6,9 +6,6 @@ # ============================================================================ from typing import Literal, Final, Protocol, NewType, overload -import tempfile -import os -import shutil # ========================================== # 1. Literal - Valeurs spécifiques autorisées diff --git a/07-bibliotheques-standard/exemples/06_07_typeddict.py b/07-bibliotheques-standard/exemples/06_07_typeddict.py new file mode 100644 index 0000000..fc7802b --- /dev/null +++ b/07-bibliotheques-standard/exemples/06_07_typeddict.py @@ -0,0 +1,71 @@ +# ============================================================================ +# Section 7.6 : Le module typing - Annotations avancées +# Description : TypedDict (dictionnaires structurés) - clés requises, +# total=False (clés facultatives), NotRequired (3.11+) +# Fichier source : 06-typing-annotations-avancees.md +# ============================================================================ + +import sys +from typing import TypedDict + +# ========================================== +# 1. TypedDict de base - clés et types fixes +# ========================================== +print("=== TypedDict de base ===") + +class Film(TypedDict): + titre: str + annee: int + note: float + +# À l'exécution, c'est un dictionnaire ordinaire ; ce sont les outils +# (mypy, IDE) qui vérifient les clés présentes et le type de chaque valeur. +inception: Film = {"titre": "Inception", "annee": 2010, "note": 8.8} +matrix: Film = {"titre": "Matrix", "annee": 1999, "note": 8.7} + +def resumer(film: Film) -> str: + return f"{film['titre']} ({film['annee']}) - {film['note']}/10" + +print(f" {resumer(inception)}") +print(f" {resumer(matrix)}") + +# C'est bien un dict : les opérations habituelles fonctionnent +print(f" Clés de 'inception' : {list(inception.keys())}") +print(f" Type réel : {type(inception).__name__}") + +# ========================================== +# 2. total=False - toutes les clés facultatives +# ========================================== +print("\n=== total=False (clés facultatives) ===") + +class Preferences(TypedDict, total=False): + couleur: str + taille: int + +p1: Preferences = {"couleur": "rouge"} +p2: Preferences = {"couleur": "bleu", "taille": 42} +p3: Preferences = {} + +for i, p in enumerate((p1, p2, p3), start=1): + print(f" p{i} = {p}") + +# ========================================== +# 3. NotRequired - granularité par clé (Python 3.11+) +# ========================================== +print("\n=== NotRequired (granularité par clé, 3.11+) ===") + +if sys.version_info >= (3, 11): + from typing import NotRequired + + class Utilisateur(TypedDict): + nom: str # requis + email: str # requis + telephone: NotRequired[str] # facultatif + + u1: Utilisateur = {"nom": "Alice", "email": "alice@example.com"} + u2: Utilisateur = {"nom": "Bob", "email": "bob@example.com", + "telephone": "06 00 00 00 00"} + print(f" u1 = {u1}") + print(f" u2 = {u2}") +else: + print(" NotRequired : nécessite Python 3.11+") diff --git a/07-bibliotheques-standard/exemples/README.md b/07-bibliotheques-standard/exemples/README.md index cc31971..9b3d021 100644 --- a/07-bibliotheques-standard/exemples/README.md +++ b/07-bibliotheques-standard/exemples/README.md @@ -1,6 +1,6 @@ # Exemples - Chapitre 07 : Bibliothèques standard -Ce dossier contient **35 fichiers** d'exemples exécutables couvrant les 6 sections du chapitre 7. +Ce dossier contient **36 fichiers** d'exemples exécutables couvrant les 6 sections du chapitre 7. ## Section 7.1 : os, sys et subprocess @@ -16,7 +16,7 @@ Ce dossier contient **35 fichiers** d'exemples exécutables couvrant les 6 secti | Fichier | Description | Fichier source | |---------|-------------|----------------| -| `02_01_datetime_base.py` | datetime.now, timezone, création, composants, strftime, strptime, calcul d'âge, parsing sécurisé | `02-datetime-et-time.md` | +| `02_01_datetime_base.py` | datetime.now, timezone, création, composants, strftime, strptime, isoformat/fromisoformat, calcul d'âge, parsing sécurisé | `02-datetime-et-time.md` | | `02_02_date_time_timedelta.py` | date.today, classe time, timedelta (création, arithmétique), différences de dates, comparaisons, min/max | `02-datetime-et-time.md` | | `02_03_module_time.py` | timestamp, localtime, conversion datetime/timestamp, sleep, perf_counter, classe Chronometre (context manager) | `02-datetime-et-time.md` | | `02_04_fuseaux_horaires.py` | ZoneInfo (Paris, Tokyo, New York), conversions de fuseaux, UTC, bonnes pratiques | `02-datetime-et-time.md` | @@ -27,8 +27,8 @@ Ce dossier contient **35 fichiers** d'exemples exécutables couvrant les 6 secti | Fichier | Description | Fichier source | |---------|-------------|----------------| | `03_01_math_base.py` | Constantes (pi, e, tau, inf, nan), calculs cercle, fabs/copysign, ceil/floor/trunc/round, facture avec TVA | `03-math-random-statistics.md` | -| `03_02_math_avance.py` | sqrt/pow/cbrt/exp, intérêts composés, logarithmes, trigonométrie, distance euclidienne, gcd/lcm/factorial/comb/perm, probabilités loto | `03-math-random-statistics.md` | -| `03_03_random.py` | random/uniform/randint, choice/choices/sample, shuffle, PaquetDeCartes, distributions (seed=42) | `03-math-random-statistics.md` | +| `03_02_math_avance.py` | sqrt/pow/cbrt/exp, intérêts composés, logarithmes, trigonométrie, isclose/isnan/isinf, distance euclidienne, gcd/lcm/factorial/comb/perm, probabilités loto | `03-math-random-statistics.md` | +| `03_03_random.py` | random/uniform/randint, choice/choices/sample, shuffle, PaquetDeCartes, distributions (seed=42), secrets (aléatoire sécurisé) | `03-math-random-statistics.md` | | `03_04_statistics.py` | mean/geometric_mean/harmonic_mean, median, mode/multimode, analyse salaires, variance/stdev, quantiles, correlation | `03-math-random-statistics.md` | | `03_05_casino_monte_carlo.py` | Simulation casino (roulette+blackjack), estimation de Pi par Monte Carlo, simulation de notes (seed=42) | `03-math-random-statistics.md` | @@ -38,7 +38,7 @@ Ce dossier contient **35 fichiers** d'exemples exécutables couvrant les 6 secti |---------|-------------|----------------| | `04_01_itertools_infinis.py` | count, cycle, repeat, GestionnaireID | `04-itertools-et-functools.md` | | `04_02_itertools_filtrage.py` | chain, compress, dropwhile/takewhile, filterfalse, islice, groupby, analyse de logs | `04-itertools-et-functools.md` | -| `04_03_itertools_combinatoires.py` | product, permutations, combinations, combinations_with_replacement, grilles loto, accumulate, tee, zip_longest | `04-itertools-et-functools.md` | +| `04_03_itertools_combinatoires.py` | product, permutations, combinations, combinations_with_replacement, grilles loto, accumulate, tee, zip_longest, pairwise/batched | `04-itertools-et-functools.md` | | `04_04_functools_reduce_partial.py` | reduce (somme, produit, max), fusion de dictionnaires, partial (puissance, multiplier, print, conversion d'unités) | `04-itertools-et-functools.md` | | `04_05_functools_cache_et_classes.py` | lru_cache (fibonacci), triangle de Pascal, wraps (décorateur chronomètre), total_ordering, singledispatch | `04-itertools-et-functools.md` | | `04_06_exemple_complet_transactions.py` | AnalyseurTransactions (groupby catégorie, top clients, CA cumulé) + pipeline de traitement de texte avec Counter | `04-itertools-et-functools.md` | @@ -63,9 +63,10 @@ Ce dossier contient **35 fichiers** d'exemples exécutables couvrant les 6 secti | `06_01_annotations_base.py` | Annotations de variables et fonctions, types de collections (list, dict, tuple, set) | `06-typing-annotations-avancees.md` | | `06_02_union_optional_any.py` | Union (|), X \| None, Any, TypeAlias | `06-typing-annotations-avancees.md` | | `06_03_callable_typevar.py` | Callable (types de fonctions), TypeVar (génériques, contraintes, bornes) | `06-typing-annotations-avancees.md` | -| `06_04_generic_classes.py` | Classes génériques avec Generic (Pile[T], Cache[K, V] avec expiration) | `06-typing-annotations-avancees.md` | +| `06_04_generic_classes.py` | Classes génériques avec Generic (Pile[T], Cache[K, V] avec expiration), Self (interface fluide, 3.11+) | `06-typing-annotations-avancees.md` | | `06_05_literal_final_protocol.py` | Literal, Final, Protocol (duck typing structurel), NewType, @overload | `06-typing-annotations-avancees.md` | | `06_06_exemple_complet_taches.py` | Système de gestion de tâches (dataclass, Literal, Protocol, TypeAlias, notifications) | `06-typing-annotations-avancees.md` | +| `06_07_typeddict.py` | TypedDict (dictionnaires structurés) : clés requises, `total=False` (clés facultatives), `NotRequired` (3.11+) | `06-typing-annotations-avancees.md` | ## Sorties attendues @@ -158,8 +159,8 @@ Date du jour : YYYY-MM-DD === Journal (avec fichier temporaire) === ... === Calculateur de temps de travail === -Durée travaillée : 8:00:00 -Salaire : 120.00 EUR +Temps de travail total : 8.00 heures +Salaire estimé (15.0 EUR/h) : 120.00 EUR ... ``` @@ -177,7 +178,8 @@ Pi = 3.141592653589793 === Racines et puissances === ... === Intérêts composés === -Capital après 10 ans : 13,439.16 EUR +Capital final : 13439.16 EUR +Intérêts gagnés : 3439.16 EUR ... ``` @@ -436,10 +438,31 @@ Tâches en retard: 1 - Revue de code ``` +### 06_07 - TypedDict +``` +=== TypedDict de base === + Inception (2010) - 8.8/10 + Matrix (1999) - 8.7/10 + Clés de 'inception' : ['titre', 'annee', 'note'] + Type réel : dict + +=== total=False (clés facultatives) === + p1 = {'couleur': 'rouge'} + p2 = {'couleur': 'bleu', 'taille': 42} + p3 = {} + +=== NotRequired (granularité par clé, 3.11+) === + u1 = {'nom': 'Alice', 'email': 'alice@example.com'} + u2 = {'nom': 'Bob', 'email': 'bob@example.com', 'telephone': '06 00 00 00 00'} +``` +*(La dernière section requiert Python 3.11+ ; sur 3.10 elle affiche `NotRequired : nécessite Python 3.11+`.)* + ## Notes - Les exemples `05_*` (logging) utilisent des dossiers temporaires pour les fichiers de log, nettoyés automatiquement à la fin de l'exécution. - Les exemples `05_01` utilisent des sous-processus pour contourner la limitation de `basicConfig()` (un seul appel effectif par processus). - Les exemples `03_03` et `03_05` (random) utilisent `seed(42)` pour des résultats reproductibles. -- Les exemples `06_*` (typing) sont des annotations qui n'affectent pas l'exécution mais rendent le code plus clair pour les outils d'analyse (mypy). +- Les exemples `06_*` (typing) sont des annotations qui n'affectent pas l'exécution mais aident les outils d'analyse (mypy). `06_01` contient **volontairement** une affectation incompatible (`age_test = "vingt-cinq"`) pour illustrer que mypy la détecte alors que Python l'accepte à l'exécution — c'est la seule « erreur » mypy attendue des exemples typing. +- La syntaxe **PEP 695** (Python 3.12+ : `type Alias = …`, `def f[T](...)`, `class C[T]`) est présentée dans le cours (`06-typing-annotations-avancees.md`) mais **pas** reprise dans ces exemples exécutables : c'est une syntaxe vérifiée *au parsing*, donc un fichier qui la contient échoue avec `SyntaxError` sur Python 3.10/3.11 — même placée derrière un test de version. Les exemples utilisent donc l'équivalent classique `TypeVar`/`Generic`, exécutable sur toutes les versions visées. - Tous les exemples créant des fichiers/dossiers temporaires nettoient leurs résidus à la fin. +- Pour rester compatibles avec **Python 3.10** (version minimale du cours), quelques exemples gardent les fonctions récentes derrière un test de version : `math.cbrt`, `typing.Self` et `typing.NotRequired` (Python 3.11+), `itertools.batched` (3.12+). Sur une version antérieure, ces blocs affichent un message de repli au lieu de planter. *(Vérifié : les 36 exemples s'exécutent sur Python 3.10, 3.12 et 3.14.)* diff --git a/08-programmation-concurrente/01-threading-et-multiprocessing.md b/08-programmation-concurrente/01-threading-et-multiprocessing.md index 5dea532..f6caf6c 100644 --- a/08-programmation-concurrente/01-threading-et-multiprocessing.md +++ b/08-programmation-concurrente/01-threading-et-multiprocessing.md @@ -49,6 +49,8 @@ Le GIL est un verrou dans CPython (l'implémentation standard de Python) qui emp **Exception** : Les opérations I/O libèrent le GIL, donc les threads sont efficaces pour ces tâches. +> **Note (Python 3.13+)** : un build *free-threaded* (sans GIL) existe depuis Python 3.13 — expérimental en 3.13, officiellement supporté en 3.14, mais toujours optionnel (voir le [README du chapitre](/08-programmation-concurrente/README.md)). Sauf si vous avez explicitement installé ce build particulier, le GIL décrit ici s'applique, et tout ce qui suit reste valable. + --- ## Threading - Exécution avec des Threads @@ -221,7 +223,7 @@ if __name__ == '__main__': print("Le processus est terminé") ``` -**Note importante** : Le `if __name__ == '__main__':` est nécessaire sous Windows pour éviter des erreurs. +**Note importante** : Le `if __name__ == '__main__':` est **indispensable** dès qu'on crée des processus. Selon la *méthode de démarrage*, l'interpréteur **réimporte le module principal** dans chaque processus enfant ; sans ce garde-fou, le code de création des processus se réexécuterait dans l'enfant, provoquant une cascade infinie de processus (ou une erreur). C'est le cas sous **Windows** et **macOS** (méthode `spawn`), et désormais sous **Linux** à partir de **Python 3.14** (la méthode par défaut passe de `fork` à `forkserver`). En pratique : placez toujours votre code multiprocessing sous ce garde. ### Exemple 2 : Calculs parallèles avec Pool @@ -265,18 +267,18 @@ def calcul_intensif(n): def execution_sequentielle(nombres): """Exécution séquentielle""" - debut = time.time() + debut = time.perf_counter() # perf_counter : horloge monotone pour mesurer une durée resultats = [calcul_intensif(n) for n in nombres] - duree = time.time() - debut + duree = time.perf_counter() - debut print(f"Séquentiel: {duree:.2f} secondes") return resultats def execution_parallele(nombres): """Exécution parallèle""" - debut = time.time() + debut = time.perf_counter() with multiprocessing.Pool() as pool: resultats = pool.map(calcul_intensif, nombres) - duree = time.time() - debut + duree = time.perf_counter() - debut print(f"Parallèle: {duree:.2f} secondes") return resultats @@ -337,6 +339,92 @@ if __name__ == '__main__': --- +## `concurrent.futures` - L'API moderne unifiée + +Gérer manuellement des threads ou des processus (création, `start()`, `join()`, collecte des résultats) devient vite fastidieux. Le module **`concurrent.futures`** offre une **interface de haut niveau** qui fonctionne **à l'identique** pour les threads et les processus : il suffit de changer une seule classe. + +- `ThreadPoolExecutor` : un pool de **threads** (pour les tâches I/O-bound) +- `ProcessPoolExecutor` : un pool de **processus** (pour les tâches CPU-bound) + +Les deux exposent exactement la même API. C'est aujourd'hui la **façon recommandée** d'écrire du code concurrent en Python pour la plupart des besoins courants. + +### Exemple 1 : `map()` pour traiter une liste + +```python +from concurrent.futures import ThreadPoolExecutor + +def traiter(n): + return n * n + +with ThreadPoolExecutor(max_workers=4) as executor: + resultats = list(executor.map(traiter, [1, 2, 3, 4, 5])) + +print(resultats) # [1, 4, 9, 16, 25] +``` + +Pour passer aux processus (tâche CPU-bound), il suffit de remplacer `ThreadPoolExecutor` par `ProcessPoolExecutor` — le reste du code est identique : + +```python +from concurrent.futures import ProcessPoolExecutor + +def traiter(n): + return n * n + +if __name__ == '__main__': # requis : voir la note sur multiprocessing + with ProcessPoolExecutor() as executor: + resultats = list(executor.map(traiter, [1, 2, 3, 4, 5])) + print(resultats) +``` + +### Exemple 2 : `submit()` et objets `Future` + +`submit()` lance une tâche et renvoie **immédiatement** un objet **`Future`** : une promesse de résultat que l'on récupère plus tard avec `.result()`. + +```python +from concurrent.futures import ThreadPoolExecutor + +def telecharger(url): + # ... travail I/O (requête réseau) ... + return f"contenu de {url}" + +urls = ["a.com", "b.com", "c.com"] + +with ThreadPoolExecutor(max_workers=3) as executor: + # Soumettre les tâches : un Future par tâche, associé à son URL + futures = {executor.submit(telecharger, url): url for url in urls} + + # .result() attend la fin de la tâche et renvoie sa valeur + for future, url in futures.items(): + print(f"{url} -> {future.result()}") +``` + +### Exemple 3 : `as_completed()` - traiter les résultats au fil de l'eau + +`as_completed()` renvoie les `Future` **dès qu'ils se terminent**, sans attendre les plus lents. Idéal pour afficher une progression. + +```python +from concurrent.futures import ThreadPoolExecutor, as_completed +import time + +def tache(n): + time.sleep(n) # les tâches courtes finissent en premier + return f"tâche {n} terminée" + +with ThreadPoolExecutor(max_workers=3) as executor: + futures = [executor.submit(tache, duree) for duree in (3, 1, 2)] + + for future in as_completed(futures): + print(future.result()) # ordre d'arrivée : 1, puis 2, puis 3 +``` + +> **Gestion des erreurs** : une exception levée dans une tâche n'est pas perdue. Elle est **re-levée** au moment de l'appel à `future.result()`, ce qui permet de l'entourer d'un `try/except`. C'est un avantage majeur sur la gestion manuelle des threads, où les exceptions passent souvent inaperçues. + +> **Choisir le bon pool** : `ThreadPoolExecutor` pour l'I/O-bound (le GIL est libéré pendant l'attente), `ProcessPoolExecutor` pour le CPU-bound (vrai parallélisme sur plusieurs cœurs). C'est la même distinction que Threading vs Multiprocessing, mais avec une API bien plus simple. + +> **Et en Python 3.14** : un troisième exécuteur, `InterpreterPoolExecutor`, rejoint la famille. Il répartit le travail sur des **sous-interpréteurs** (chacun avec son propre GIL), pour un vrai parallélisme CPU plus léger que le multiprocessing — avec la même API. Voir le [README du chapitre](/08-programmation-concurrente/README.md). + +--- + ## Quand utiliser Threading ou Multiprocessing ? ### Utilisez **Threading** pour : @@ -387,7 +475,7 @@ if __name__ == '__main__': ### 1. Toujours utiliser `if __name__ == '__main__':` -Avec multiprocessing, c'est essentiel pour éviter des erreurs, surtout sous Windows : +Avec multiprocessing, c'est essentiel pour éviter des erreurs avec les méthodes de démarrage `spawn` et `forkserver` (Windows, macOS, et Linux à partir de Python 3.14) : ```python if __name__ == '__main__': @@ -433,6 +521,7 @@ En threading, utilisez toujours des verrous pour les variables partagées : lock = threading.Lock() def modifier_variable_partagee(): + global variable_globale with lock: # Modification sécurisée variable_globale += 1 @@ -448,7 +537,7 @@ import threading def fonction_avec_erreur(): try: # Code qui peut échouer - resultat = 1 / 0 + 1 / 0 except Exception as e: print(f"Erreur dans le thread: {e}") @@ -498,7 +587,7 @@ class TelechargeParallele: threads = [] print(f"Démarrage de {len(urls)} téléchargements...") - debut = time.time() + debut = time.perf_counter() # Créer et démarrer les threads for url in urls: @@ -515,7 +604,7 @@ class TelechargeParallele: for thread in threads: thread.join() - duree_totale = time.time() - debut + duree_totale = time.perf_counter() - debut print(f"\n✓ Tous les téléchargements terminés en {duree_totale:.2f}s") return self.resultats @@ -545,7 +634,8 @@ if __name__ == '__main__': 4. Utilisez **Lock** pour protéger les données partagées en threading 5. Utilisez **Queue** pour la communication entre processus 6. **Pool** simplifie la parallélisation de listes de tâches -7. Toujours mesurer les performances avant/après parallélisation +7. **`concurrent.futures`** (`ThreadPoolExecutor` / `ProcessPoolExecutor`) est l'API de haut niveau recommandée : le même code fonctionne pour les threads et les processus +8. Toujours mesurer les performances avant/après parallélisation --- @@ -555,7 +645,7 @@ Dans la section suivante (8.2), nous explorerons la **programmation asynchrone a **Pour aller plus loin** : - Documentation officielle : `threading` et `multiprocessing` -- Explorez `concurrent.futures` pour une API unifiée +- Approfondissez `concurrent.futures` : passage de `ThreadPoolExecutor` à `ProcessPoolExecutor`, paramètre `chunksize` de `map()`, annulation de `Future` - Apprenez `asyncio` pour une approche asynchrone moderne ⏭️ [Programmation asynchrone avec asyncio](/08-programmation-concurrente/02-programmation-asynchrone-asyncio.md) diff --git a/08-programmation-concurrente/02-programmation-asynchrone-asyncio.md b/08-programmation-concurrente/02-programmation-asynchrone-asyncio.md index 0e8c181..4514a08 100644 --- a/08-programmation-concurrente/02-programmation-asynchrone-asyncio.md +++ b/08-programmation-concurrente/02-programmation-asynchrone-asyncio.md @@ -158,7 +158,7 @@ async def faire_cafe(nom): async def main(): """Exécute plusieurs préparations en parallèle""" - debut = time.time() + debut = time.perf_counter() # Créer plusieurs tâches tache1 = asyncio.create_task(faire_cafe("Alice")) @@ -170,7 +170,7 @@ async def main(): resultat2 = await tache2 resultat3 = await tache3 - duree = time.time() - debut + duree = time.perf_counter() - debut print(f"\n✅ Tous les cafés prêts en {duree:.2f}s") print(f"Résultats: {resultat1}, {resultat2}, {resultat3}") @@ -192,6 +192,8 @@ asyncio.run(main()) **Magie** : Les 3 cafés sont préparés en 2 secondes au lieu de 6 ! Ils se préparent en parallèle. +> 📝 **Pourquoi 2 secondes et non 6 ?** Au premier `await asyncio.sleep(2)` (celui d'Alice), la coroutine rend la main à l'event loop **sans bloquer** ; celui-ci en profite pour démarrer les tâches de Bob puis de Charlie, qui se mettent à leur tour en attente. Les trois `sleep(2)` s'écoulent donc **en même temps**, et au bout de 2 s les trois tâches reprennent. C'est tout l'intérêt d'`await` : pendant qu'une tâche attend, les autres avancent. (Avec `time.sleep(2)` à la place, l'event loop resterait bloqué et on retomberait à 6 s — voir « ne jamais bloquer l'event loop » plus bas.) + --- ## asyncio.gather() - Attendre plusieurs coroutines @@ -239,6 +241,48 @@ asyncio.run(main()) --- +## asyncio.TaskGroup() - Concurrence structurée (Python 3.11+) + +Depuis **Python 3.11**, `asyncio.TaskGroup` est la façon **recommandée** de lancer plusieurs tâches concurrentes. C'est ce qu'on appelle la *concurrence structurée* : toutes les tâches créées dans le bloc `async with` sont garanties terminées (ou annulées) à la sortie du bloc. + +### Exemple : TaskGroup en action + +```python +import asyncio + +async def telecharger(nom, duree): + await asyncio.sleep(duree) + print(f"✅ {nom} téléchargé") + return nom + +async def main(): + # Toutes les tâches sont créées dans le bloc ; on en sort quand tout est fini + async with asyncio.TaskGroup() as tg: + t1 = tg.create_task(telecharger("video", 2)) + t2 = tg.create_task(telecharger("image", 1)) + t3 = tg.create_task(telecharger("document", 1.5)) + # Ici, les 3 tâches sont forcément terminées : on peut lire leurs résultats + print(f"Résultats : {t1.result()}, {t2.result()}, {t3.result()}") + +asyncio.run(main()) +``` + +### Pourquoi TaskGroup plutôt que gather() ? + +| Aspect | `gather()` | `TaskGroup` (3.11+) | +|--------|------------|---------------------| +| **Si une tâche échoue** | les autres continuent en arrière-plan | les autres sont **annulées** automatiquement | +| **Propagation d'erreurs** | la 1re exception remonte (ou liste avec `return_exceptions`) | **toutes** les erreurs, regroupées dans un `ExceptionGroup` | +| **Portée** | manuelle | structurée (`async with`) : impossible d'« oublier » une tâche | + +L'annulation automatique évite un piège classique : avec `gather()`, si une requête échoue, les autres continuent inutilement à tourner. `TaskGroup` les arrête proprement. + +> **Quand garder `gather()` ?** Quand vous voulez justement que **toutes** les tâches aillent au bout malgré les erreurs (avec `return_exceptions=True`), ou pour rester compatible avec Python 3.10. Sinon, préférez `TaskGroup`. + +> **ExceptionGroup** : quand plusieurs tâches d'un `TaskGroup` échouent, les erreurs sont regroupées dans un `ExceptionGroup` (Python 3.11+), que l'on capture avec la nouvelle syntaxe `except* ValueError:` (notez l'astérisque). + +--- + ## asyncio.wait_for() - Timeout Parfois, on veut limiter le temps d'attente d'une opération. @@ -272,6 +316,32 @@ Début de l'opération longue... ❌ Timeout! L'opération a pris trop de temps ``` +### Alternative moderne : `asyncio.timeout()` (Python 3.11+) + +Depuis Python 3.11, on peut utiliser le gestionnaire de contexte `asyncio.timeout()`, souvent plus lisible — surtout pour imposer un même délai global à **plusieurs** `await` : + +```python +import asyncio + +async def operation_longue(): + await asyncio.sleep(10) + return "Opération terminée" + +async def main(): + try: + async with asyncio.timeout(3.0): + resultat = await operation_longue() + print(f"Résultat: {resultat}") + except TimeoutError: + print("❌ Timeout! L'opération a pris trop de temps") + +asyncio.run(main()) +``` + +Différence clé : `wait_for()` enveloppe **une seule** coroutine, tandis que `asyncio.timeout()` impose un délai à **tout un bloc** de code (qui peut contenir plusieurs `await`). + +> **Note** : depuis Python 3.11, `asyncio.TimeoutError` est devenu un simple **alias** de l'exception intégrée `TimeoutError`. Les deux sont interchangeables, et `asyncio.timeout()` lève `TimeoutError`. + --- ## Exemple pratique : Scraper web asynchrone @@ -295,27 +365,27 @@ async def fetch_page(url, duree): async def scraper_synchrone(urls): """Version synchrone (une page après l'autre)""" print("=== VERSION SYNCHRONE ===") - debut = time.time() + debut = time.perf_counter() resultats = [] for url, duree in urls: resultat = await fetch_page(url, duree) resultats.append(resultat) - duree_totale = time.time() - debut + duree_totale = time.perf_counter() - debut print(f"⏱️ Temps total: {duree_totale:.2f}s\n") return resultats async def scraper_asynchrone(urls): """Version asynchrone (toutes les pages en parallèle)""" print("=== VERSION ASYNCHRONE ===") - debut = time.time() + debut = time.perf_counter() # Lancer toutes les requêtes en parallèle taches = [fetch_page(url, duree) for url, duree in urls] resultats = await asyncio.gather(*taches) - duree_totale = time.time() - debut + duree_totale = time.perf_counter() - debut print(f"⏱️ Temps total: {duree_totale:.2f}s\n") return resultats @@ -484,6 +554,62 @@ asyncio.run(main()) --- +## Annuler une tâche : `cancel()` et `CancelledError` + +Une tâche lancée avec `create_task()` peut être **annulée** avant la fin. C'est le mécanisme qui se cache derrière `asyncio.timeout()` / `wait_for()` (qui annulent la tâche à l'expiration du délai) et derrière `TaskGroup` (qui annule les tâches sœurs quand l'une échoue). + +### Comment ça marche + +`tache.cancel()` ne stoppe pas la tâche immédiatement : il programme l'injection d'une exception `asyncio.CancelledError` à l'intérieur de la coroutine, au prochain `await`. La tâche peut donc l'intercepter pour faire un peu de nettoyage avant de s'arrêter. + +```python +import asyncio + +async def tache_longue(): + try: + print("Tâche : démarrage") + await asyncio.sleep(10) + print("Tâche : terminée") # jamais atteint si annulée + except asyncio.CancelledError: + print("Tâche : annulation reçue, nettoyage...") + raise # IMPORTANT : on relance pour confirmer l'annulation + +async def main(): + tache = asyncio.create_task(tache_longue()) + await asyncio.sleep(1) # on la laisse démarrer + tache.cancel() # on demande l'annulation + try: + await tache + except asyncio.CancelledError: + print("Main : la tâche a bien été annulée") + +asyncio.run(main()) +``` + +**Sortie** : +``` +Tâche : démarrage +Tâche : annulation reçue, nettoyage... +Main : la tâche a bien été annulée +``` + +### Deux règles importantes + +1. **Ne « mangez » pas l'annulation.** Si vous interceptez `CancelledError`, faites votre nettoyage puis **relancez-la** avec `raise`. Une tâche qui avale son annulation refuse en réalité de s'arrêter — source de bugs difficiles à diagnostiquer. + +2. **`except Exception` n'attrape pas `CancelledError`.** Depuis Python 3.8, `asyncio.CancelledError` hérite de `BaseException` (et non de `Exception`). Un `try/except Exception` autour de votre logique métier laisse donc passer l'annulation — ce qui est exactement le comportement souhaité : + +```python +async def traiter(): + try: + await operation() + except Exception as e: # n'intercepte PAS l'annulation + print(f"Erreur métier : {e}") + # Une CancelledError, elle, continue de se propager normalement +``` + +--- + ## Patterns courants avec asyncio ### Pattern 1 : Queue asynchrone (Producer-Consumer) @@ -541,6 +667,8 @@ async def main(): asyncio.run(main()) ``` +> 📝 **`task_done()` et `queue.join()`.** Une `asyncio.Queue` (comme une `queue.Queue`) tient un **compteur** d'items non terminés : chaque `put()` l'incrémente, et chaque **`task_done()`** — appelé par le consommateur après avoir traité un item — le décrémente. **`await queue.join()`** bloque tant que ce compteur n'est pas revenu à zéro : c'est ainsi qu'on attend que **tout** ce qui a été produit ait réellement été traité, sans compter les items soi-même. (Ces méthodes sont détaillées en 8.3.) + ### Pattern 2 : Limiter le nombre de tâches concurrentes ```python @@ -589,7 +717,7 @@ def operation_io_thread(numero): def executer_avec_threads(nombre): """Exécute avec threads""" - debut = time.time() + debut = time.perf_counter() threads = [] resultats = [None] * nombre @@ -604,7 +732,7 @@ def executer_avec_threads(nombre): for thread in threads: thread.join() - duree = time.time() - debut + duree = time.perf_counter() - debut print(f"🧵 Threading: {duree:.2f}s pour {nombre} opérations") return resultats @@ -616,12 +744,12 @@ async def operation_io_async(numero): async def executer_avec_asyncio(nombre): """Exécute avec asyncio""" - debut = time.time() + debut = time.perf_counter() taches = [operation_io_async(i) for i in range(nombre)] resultats = await asyncio.gather(*taches) - duree = time.time() - debut + duree = time.perf_counter() - debut print(f"⚡ Asyncio: {duree:.2f}s pour {nombre} opérations") return resultats @@ -648,7 +776,7 @@ asyncio.run(main()) ``` **Avantages d'asyncio** : -- Moins de mémoire (pas de stack par tâche) +- Bien moins de mémoire : un thread réserve sa propre pile d'exécution (souvent plusieurs Mo), alors qu'une tâche asyncio n'est qu'un objet léger sur le tas - Plus scalable (peut gérer des milliers de connexions) - Code plus lisible avec async/await @@ -690,6 +818,25 @@ async def bonne_pratique(): **Règle** : N'utilisez jamais `time.sleep()` dans du code asynchrone, utilisez `await asyncio.sleep()`. +**Et si une fonction bloquante est inévitable ?** (par exemple une bibliothèque sans version asynchrone, ou un calcul lourd). Déléguez-la à un thread avec `asyncio.to_thread()` (Python 3.9+), pour ne pas geler l'event loop : + +```python +import asyncio +import time + +def fonction_bloquante(): + """Code synchrone qui prend du temps (lib sans équivalent async, etc.)""" + time.sleep(2) + return "résultat" + +async def main(): + # Exécutée dans un thread séparé : l'event loop reste libre pendant ce temps + resultat = await asyncio.to_thread(fonction_bloquante) + print(resultat) + +asyncio.run(main()) +``` + ### 3. Utiliser des bibliothèques asynchrones ```python @@ -731,7 +878,7 @@ async def utiliser_plusieurs_ressources(): ### 5. Attention aux listes de compréhension ```python -# ❌ Mauvais - crée les tâches mais ne les exécute pas en parallèle +# ❌ Mauvais - exécute les coroutines une par une (séquentiel, pas en parallèle) async def mauvais(): resultats = [await ma_coroutine(i) for i in range(10)] @@ -856,7 +1003,7 @@ class GestionnaireTelechargement: """Télécharge un fichier avec gestion d'erreurs et timeout""" async with self.semaphore: self.statistiques['total'] += 1 - debut = time.time() + debut = time.perf_counter() try: print(f"⬇️ Début: {url}") @@ -867,7 +1014,7 @@ class GestionnaireTelechargement: timeout=timeout ) - duree = time.time() - debut + duree = time.perf_counter() - debut self.statistiques['reussis'] += 1 print(f"✅ Succès: {url} ({duree:.2f}s)") @@ -907,7 +1054,7 @@ class GestionnaireTelechargement: print(f"📊 Concurrence max: {self.max_concurrent}") print("-" * 50) - debut_total = time.time() + debut_total = time.perf_counter() # Créer toutes les tâches taches = [self.telecharger_fichier(url) for url in urls] @@ -915,7 +1062,7 @@ class GestionnaireTelechargement: # Exécuter avec progression resultats = await asyncio.gather(*taches, return_exceptions=True) - duree_totale = time.time() - debut_total + duree_totale = time.perf_counter() - debut_total # Afficher les statistiques print("-" * 50) @@ -982,10 +1129,14 @@ Asyncio n'est pas toujours la meilleure solution. **Évitez asyncio** dans ces c | **await** | Attend sans bloquer | `await ma_coroutine()` | | **Task** | Coroutine planifiée | `asyncio.create_task(coro)` | | **gather** | Exécute plusieurs coroutines | `await asyncio.gather(*coros)` | +| **TaskGroup** | Concurrence structurée (3.11+) | `async with asyncio.TaskGroup() as tg:` | | **sleep** | Pause non-bloquante | `await asyncio.sleep(1)` | | **run** | Lance l'event loop | `asyncio.run(main())` | | **Queue** | File asynchrone | `asyncio.Queue()` | | **Semaphore** | Limite la concurrence | `asyncio.Semaphore(n)` | +| **timeout** | Délai sur tout un bloc (3.11+) | `async with asyncio.timeout(5):` | +| **to_thread** | Délègue du code bloquant à un thread (3.9+) | `await asyncio.to_thread(fn)` | +| **cancel** | Annule une tâche (lève `CancelledError`) | `tache.cancel()` | --- @@ -996,9 +1147,11 @@ Asyncio n'est pas toujours la meilleure solution. **Évitez asyncio** dans ces c 3. **Event loop** = Orchestre l'exécution des coroutines 4. **create_task()** = Lance une coroutine en arrière-plan 5. **gather()** = Attend plusieurs coroutines en parallèle -6. **Ne jamais bloquer l'event loop** = Utilisez des versions asynchrones -7. **Gérez les erreurs** = Utilisez try/except et return_exceptions -8. **Semaphore** = Contrôle le nombre de tâches simultanées +6. **TaskGroup (3.11+)** = la façon moderne et recommandée de lancer des tâches concurrentes (annule les autres en cas d'erreur) +7. **Ne jamais bloquer l'event loop** = Utilisez des versions asynchrones (ou `asyncio.to_thread()` pour le code bloquant inévitable) +8. **Gérez les erreurs** = Utilisez try/except et return_exceptions +9. **Annulation** = `cancel()` injecte `CancelledError` ; nettoyez puis relancez-la (ne l'avalez pas) +10. **Semaphore** = Contrôle le nombre de tâches simultanées --- diff --git a/08-programmation-concurrente/03-verrous-et-synchronisation.md b/08-programmation-concurrente/03-verrous-et-synchronisation.md index f789c76..725d365 100644 --- a/08-programmation-concurrente/03-verrous-et-synchronisation.md +++ b/08-programmation-concurrente/03-verrous-et-synchronisation.md @@ -54,11 +54,12 @@ for t in threads: for t in threads: t.join() -print(f"Compteur: {compteur}") # Résultat imprévisible! -# Devrait être 500000, mais sera probablement moins +print(f"Compteur: {compteur}") # Pas garanti d'afficher 500000 ! ``` -**Résultat typique** : `Compteur: 347823` (au lieu de 500000) +**Pourquoi c'est un bug** : `compteur += 1` n'est **pas atomique** — c'est en réalité trois étapes (lire `compteur`, ajouter 1, réécrire le résultat). Si un autre thread s'intercale entre la lecture et l'écriture, sa propre mise à jour est écrasée, donc perdue. + +> ⚠️ **Le piège du GIL** : sur CPython récent, cette boucle est si rapide que le GIL bascule rarement en plein milieu de l'opération. Vous obtiendrez donc **souvent 500000 quand même** (voire systématiquement sur les versions récentes), comme si tout allait bien. **Ne vous y fiez surtout pas** : le code reste incorrect. Le bug réapparaît dès que la section critique s'allonge (du vrai travail entre la lecture et l'écriture), sous forte charge, sur une autre implémentation de Python, ou sur un build *free-threaded* sans GIL (Python 3.13+). La seule garantie est un **verrou** — voir la section suivante. ### Deadlock (Interblocage) @@ -104,6 +105,7 @@ Python propose plusieurs outils pour synchroniser les threads et les coroutines | **Event** | Signaler un événement | ✅ | ✅ | | **Condition** | Attendre une condition | ✅ | ✅ | | **Barrier** | Synchroniser plusieurs threads | ✅ | ❌ | +| **Queue** | Échanger des données entre threads (thread-safe) | ✅ | ✅ | --- @@ -216,6 +218,8 @@ async def main(): asyncio.run(main()) ``` +> 📝 **Ce verrou est-il vraiment nécessaire ici ?** En réalité **non**, et c'est une différence fondamentale avec le threading. En asyncio, **une seule tâche s'exécute à la fois** : une coroutine ne peut être interrompue qu'à un point `await`. Comme `compteur += 1` ne contient **aucun** `await`, il s'exécute d'un seul tenant — pas de race condition, donc pas besoin de verrou ici. `asyncio.Lock` devient indispensable seulement quand la section critique contient un `await` (typiquement : lire une valeur, `await` une opération, puis réécrire — une autre tâche peut s'intercaler pendant l'`await`). L'exemple ci-dessus illustre donc surtout la *syntaxe* `async with`. + --- ## RLock (Verrou Réentrant) @@ -618,6 +622,8 @@ cons2.join() print("✅ Production/consommation terminée") ``` +> 📝 **Pourquoi `while` et non `if` avant `wait()` ?** Quand `wait()` rend la main, rien ne garantit que la condition attendue soit *encore* vraie : un autre thread réveillé en même temps a pu reprendre l'unique item disponible avant vous, ou un « réveil spurieux » a pu se produire. En re-testant la condition **en boucle** (`while`), on ne progresse que si elle est réellement satisfaite ; avec un simple `if`, on risquerait d'agir sur un buffer vide (ou plein). C'est la règle d'or des variables de condition : **toujours attendre dans une boucle**. + ### Méthodes de Condition | Méthode | Description | @@ -626,6 +632,53 @@ print("✅ Production/consommation terminée") | `notify()` | Réveille un thread en attente | | `notify_all()` | Réveille tous les threads en attente | +> **`notify()` ou `notify_all()` ?** `notify()` ne réveille **qu'un seul** thread en attente, choisi arbitrairement. C'est suffisant **uniquement** si n'importe quel thread réveillé peut effectivement progresser. Dans l'exemple ci-dessus, c'est le cas : un producteur n'attend que si le buffer est **plein**, un consommateur que s'il est **vide** — ces deux situations s'excluent, donc le thread réveillé est toujours du bon type. Dès que cette garantie n'est plus évidente (plusieurs conditions d'attente différentes sur le même verrou), préférez `notify_all()` : on réveille parfois des threads qui se rendormiront, mais on évite à coup sûr l'interblocage par « réveil perdu ». + +--- + +## `queue.Queue` : la file thread-safe prête à l'emploi + +Vous venez de construire un buffer producteur-consommateur **à la main** avec une `Condition`. En pratique, la bibliothèque standard fournit déjà cet outil : **`queue.Queue`** est une file **thread-safe** (elle gère ses verrous en interne). C'est souvent la façon la **plus simple et la plus sûre** de faire communiquer des threads — sans manipuler de `Lock` ni de `Condition` soi-même. + +```python +import queue +import threading + +file = queue.Queue(maxsize=10) # maxsize=0 => taille illimitée + +def producteur(): + for i in range(5): + file.put(f"tâche-{i}") # bloque si la file est pleine + file.put(None) # sentinelle de fin + +def consommateur(): + while True: + item = file.get() # bloque si la file est vide + if item is None: + break + print(f"Traité : {item}") + file.task_done() # signale que cet item est terminé + +t_prod = threading.Thread(target=producteur) +t_cons = threading.Thread(target=consommateur) +t_prod.start(); t_cons.start() +t_prod.join(); t_cons.join() +``` + +### Méthodes essentielles + +| Méthode | Rôle | +|---------|------| +| `put(item)` | Ajoute un élément (bloque si la file est pleine) | +| `get()` | Retire un élément (bloque si la file est vide) | +| `get(timeout=2)` | Comme `get()`, mais lève `queue.Empty` après 2 secondes | +| `task_done()` | Signale qu'un élément récupéré a été traité | +| `join()` | Attend que **tous** les éléments aient été traités | + +> **À retenir** : dès que des threads s'échangent des données, pensez à `queue.Queue` **avant** de sortir les verrous. Elle élimine une grande partie des risques de race condition et de deadlock, et c'est l'outil utilisé dans la plupart des patterns du chapitre 8.4 (Producer-Consumer, Worker Pool, Pipeline…). + +> **Variantes** : `queue.LifoQueue` (pile, LIFO) et `queue.PriorityQueue` (par priorité) partagent la même interface. L'équivalent asynchrone est `asyncio.Queue` (section 8.2), et pour les processus `multiprocessing.Queue` (section 8.1). + --- ## Barrier (Barrière) @@ -735,20 +788,21 @@ def incrementer_threading(): compteur += 1 threads = [threading.Thread(target=incrementer_threading) for _ in range(5)] -debut = time.time() +debut = time.perf_counter() for t in threads: t.start() for t in threads: t.join() -print(f"Threading: {compteur} en {time.time() - debut:.2f}s") +print(f"Threading: {compteur} en {time.perf_counter() - debut:.2f}s") ``` ### Synchronisation en Asyncio ```python import asyncio +import time verrou_async = asyncio.Lock() compteur_async = 0 @@ -760,12 +814,12 @@ async def incrementer_asyncio(): compteur_async += 1 async def main(): - debut = time.time() + debut = time.perf_counter() taches = [incrementer_asyncio() for _ in range(5)] await asyncio.gather(*taches) - print(f"Asyncio: {compteur_async} en {time.time() - debut:.2f}s") + print(f"Asyncio: {compteur_async} en {time.perf_counter() - debut:.2f}s") asyncio.run(main()) ``` @@ -1076,14 +1130,14 @@ threads = [ ] print("🚀 Démarrage des workers") -debut = time.time() +debut = time.perf_counter() for t in threads: t.start() for t in threads: t.join() -duree = time.time() - debut +duree = time.perf_counter() - debut # Afficher les statistiques stats = cache.get_stats() @@ -1124,6 +1178,7 @@ class Singleton: ```python import threading +import time class ReadWriteLock: """Lock optimisé pour lectures multiples, écriture exclusive""" @@ -1191,6 +1246,7 @@ def ecrivain(donnees, writer_id, nouvelle_valeur): | **Event** | Signaler un événement | Notification de fin de tâche | | **Condition** | Attendre une condition spécifique | Producer/Consumer avec buffer | | **Barrier** | Synchroniser plusieurs threads | Simulation en phases | +| **Queue** | Échanger des données sans verrou manuel | Producer/Consumer, Worker Pool | --- @@ -1202,10 +1258,11 @@ def ecrivain(donnees, writer_id, nouvelle_valeur): 4. **Event** = Notification simple entre threads 5. **Condition** = Attente d'une condition avec notification 6. **Barrier** = Synchronisation de groupe -7. **Toujours utiliser `with`** pour garantir la libération -8. **Minimiser les sections critiques** pour les performances -9. **Ordre cohérent** d'acquisition pour éviter les deadlocks -10. **Documenter** les invariants et les contraintes de synchronisation +7. **`queue.Queue`** = file thread-safe ; le moyen le plus simple d'échanger des données entre threads, sans verrou manuel +8. **Toujours utiliser `with`** pour garantir la libération +9. **Minimiser les sections critiques** pour les performances +10. **Ordre cohérent** d'acquisition pour éviter les deadlocks +11. **Documenter** les invariants et les contraintes de synchronisation --- @@ -1215,7 +1272,7 @@ def ecrivain(donnees, writer_id, nouvelle_valeur): - Documentation officielle : `threading` et `asyncio.locks` - Explorez `concurrent.futures` pour une abstraction plus haute - Étudiez les patterns de concurrence avancés -- Apprenez les structures de données thread-safe : `queue.Queue` +- Approfondissez les variantes de files : `queue.LifoQueue`, `queue.PriorityQueue`, `queue.SimpleQueue` **Dans la prochaine section** (8.4), nous explorerons les **patterns de concurrence** pour construire des systèmes robustes et scalables. diff --git a/08-programmation-concurrente/04-patterns-de-concurrence.md b/08-programmation-concurrente/04-patterns-de-concurrence.md index af2081c..0488c79 100644 --- a/08-programmation-concurrente/04-patterns-de-concurrence.md +++ b/08-programmation-concurrente/04-patterns-de-concurrence.md @@ -395,14 +395,14 @@ threads = [ # Démarrer le pipeline print("🚀 Démarrage du pipeline\n") -debut = time.time() +debut = time.perf_counter() for t in threads: t.start() for t in threads: t.join() -duree = time.time() - debut +duree = time.perf_counter() - debut print(f"\n✅ Pipeline complet en {duree:.2f}s") ``` @@ -507,7 +507,7 @@ thread_collecteur = threading.Thread( # Démarrer print("🚀 Démarrage Fan-Out/Fan-In\n") -debut = time.time() +debut = time.perf_counter() thread_dispatcher.start() for t in threads_workers: @@ -520,7 +520,7 @@ for t in threads_workers: t.join() thread_collecteur.join() -duree = time.time() - debut +duree = time.perf_counter() - debut print(f"\n✅ Fan-Out/Fan-In complet en {duree:.2f}s") ``` @@ -674,25 +674,26 @@ def reduce_function(resultats): """Phase Reduce: Somme tous les carrés""" return sum(resultats) -# Données -nombres = list(range(1, 101)) - -# Phase Map (parallèle) -with ProcessPoolExecutor() as executor: - debut = time.time() - carres = list(executor.map(map_function, nombres)) - duree_map = time.time() - debut - -# Phase Reduce -debut = time.time() -total = reduce_function(carres) -duree_reduce = time.time() - debut - -print(f"📊 Map-Reduce:") -print(f" • Nombres: 1-100") -print(f" • Somme des carrés: {total}") -print(f" • Temps Map: {duree_map:.3f}s") -print(f" • Temps Reduce: {duree_reduce:.3f}s") +if __name__ == '__main__': # requis pour ProcessPoolExecutor (voir 8.1) + # Données + nombres = list(range(1, 101)) + + # Phase Map (parallèle) + with ProcessPoolExecutor() as executor: + debut = time.perf_counter() + carres = list(executor.map(map_function, nombres)) + duree_map = time.perf_counter() - debut + + # Phase Reduce + debut = time.perf_counter() + total = reduce_function(carres) + duree_reduce = time.perf_counter() - debut + + print("📊 Map-Reduce:") + print(" • Nombres: 1-100") + print(f" • Somme des carrés: {total}") + print(f" • Temps Map: {duree_map:.3f}s") + print(f" • Temps Reduce: {duree_reduce:.3f}s") ``` ### Exemple avancé : Analyse de texte @@ -714,26 +715,27 @@ def fusionner_compteurs(compteurs): resultat.update(compteur) return resultat -# Données: plusieurs documents -documents = [ - "Python est un langage de programmation. Python est facile.", - "La programmation est amusante. Python est populaire.", - "Le langage Python est utilisé en science des données.", - "Python est un excellent langage pour débuter.", -] +if __name__ == '__main__': # requis pour ProcessPoolExecutor (voir 8.1) + # Données: plusieurs documents + documents = [ + "Python est un langage de programmation. Python est facile.", + "La programmation est amusante. Python est populaire.", + "Le langage Python est utilisé en science des données.", + "Python est un excellent langage pour débuter.", + ] -# Phase Map: Compter les mots de chaque document en parallèle -with ProcessPoolExecutor() as executor: - compteurs = list(executor.map(compter_mots, documents)) + # Phase Map: Compter les mots de chaque document en parallèle + with ProcessPoolExecutor() as executor: + compteurs = list(executor.map(compter_mots, documents)) -# Phase Reduce: Fusionner tous les compteurs -compteur_total = fusionner_compteurs(compteurs) + # Phase Reduce: Fusionner tous les compteurs + compteur_total = fusionner_compteurs(compteurs) -# Afficher les 5 mots les plus fréquents -print("📊 Analyse Map-Reduce:") -print("\nTop 5 des mots les plus fréquents:") -for mot, compte in compteur_total.most_common(5): - print(f" • {mot}: {compte} fois") + # Afficher les 5 mots les plus fréquents + print("📊 Analyse Map-Reduce:") + print("\nTop 5 des mots les plus fréquents:") + for mot, compte in compteur_total.most_common(5): + print(f" • {mot}: {compte} fois") ``` --- @@ -932,6 +934,8 @@ async def main(): asyncio.run(main()) ``` +> 📝 **`asyncio.wait()` vs `gather()`.** Là où `gather()` attend **toutes** les coroutines et renvoie leurs résultats, `asyncio.wait()` prend des **tâches** et rend la main selon `return_when` — ici **`FIRST_COMPLETED`**, donc dès que **la première** se termine. Il renvoie deux ensembles `(done, pending)` : les tâches finies et celles encore en cours (qu'on annule ensuite avec `.cancel()`). Autre différence : `asyncio.wait()` ne **propage pas** les exceptions (on les lit via `tache.result()`). + --- ## Pattern 9 : Rate Limiting (Limitation de débit) @@ -1093,6 +1097,7 @@ class WebScraperSystem: """Système de scraping combinant plusieurs patterns""" def __init__(self, max_concurrent=5, rate_limit=10): + self.max_concurrent = max_concurrent self.semaphore = asyncio.Semaphore(max_concurrent) self.rate_limiter = RateLimiter(rate_limit, period=1.0) self.resultats = [] @@ -1158,9 +1163,9 @@ class WebScraperSystem: Pattern: Fan-Out/Fan-In + Worker Pool """ print(f"🚀 Démarrage du scraping de {len(urls)} URLs") - print(f"📊 Config: max {self.semaphore._value} concurrent, rate limit {self.rate_limiter.max_calls}/s\n") + print(f"📊 Config: max {self.max_concurrent} concurrent, rate limit {self.rate_limiter.max_calls}/s\n") - debut = time.time() + debut = time.perf_counter() # Simuler une session HTTP session = None # En vrai: aiohttp.ClientSession() @@ -1171,7 +1176,7 @@ class WebScraperSystem: # Fan-In: Collecter tous les résultats resultats = await asyncio.gather(*taches, return_exceptions=True) - duree = time.time() - debut + duree = time.perf_counter() - debut # Statistiques succes = sum(1 for r in resultats if isinstance(r, dict) and r.get('status') != 'error') @@ -1234,7 +1239,7 @@ async def main(): scraper = WebScraperSystem(max_concurrent=5, rate_limit=10) # Lancer le scraping - resultats = await scraper.scraper_urls(urls) + await scraper.scraper_urls(urls) # Analyser les résultats scraper.analyser_resultats() @@ -1269,9 +1274,9 @@ import time def mesurer_performance(fonction, *args): """Mesure le temps d'exécution""" - debut = time.time() + debut = time.perf_counter() resultat = fonction(*args) - duree = time.time() - debut + duree = time.perf_counter() - debut print(f"⏱️ {fonction.__name__}: {duree:.2f}s") return resultat @@ -1323,6 +1328,20 @@ for i in range(10000): asyncio.create_task(traiter()) ``` +### 6. Préférez la concurrence structurée (Python 3.11+) + +Plusieurs patterns ci-dessus reposent sur `asyncio.gather()`. Depuis Python 3.11, `asyncio.TaskGroup` offre une alternative plus sûre : si une tâche échoue, les autres sont **automatiquement annulées** (voir la section 8.2). C'est particulièrement utile pour **Fan-Out/Fan-In** et **Scatter-Gather**, où l'on lance plusieurs tâches d'un coup. + +```python +import asyncio + +async def fan_out_fan_in(items): + async with asyncio.TaskGroup() as tg: + taches = [tg.create_task(traiter(item)) for item in items] + # Toutes les tâches sont terminées ici ; une erreur en annule proprement les autres + return [t.result() for t in taches] +``` + --- ## Résumé des patterns diff --git a/08-programmation-concurrente/README.md b/08-programmation-concurrente/README.md index 26c27d8..b4b1c9a 100644 --- a/08-programmation-concurrente/README.md +++ b/08-programmation-concurrente/README.md @@ -243,6 +243,38 @@ processes = [multiprocessing.Process(target=calcul_intensif) for _ in range(4)] | **Création** | Rapide | Plus lent | | **Mémoire** | Partagée | Séparée | +> Ce tableau décrit le build **standard** de CPython, celui que vous utilisez presque certainement. Il décrit donc la règle qui s'applique par défaut aujourd'hui. + +### Le GIL en évolution : le « free-threading » (Python 3.13+) + +Le GIL n'est plus une fatalité définitive. La **PEP 703** a introduit une variante de CPython **sans GIL**, dite *free-threaded* : + +- **Python 3.13** (octobre 2024) : première version *expérimentale*, à installer séparément (l'exécutable se nomme `python3.13t`). Le GIL peut être désactivé, mais le code mono-thread paie alors un surcoût notable (de l'ordre de 40 %). +- **Python 3.14** (octobre 2025) : le mode free-threaded devient **officiellement supporté** (PEP 779) — il n'est plus qualifié d'« expérimental », même s'il reste **optionnel** (ce n'est pas le build par défaut). Le surcoût mono-thread tombe à environ 5-10 %. + +Dans ce build, plusieurs threads peuvent exécuter du code Python **vraiment en parallèle**, y compris pour des tâches CPU-bound — ce que le GIL interdit dans le build standard. + +> **À retenir** : tant que vous utilisez l'interpréteur **par défaut** (le cas de l'immense majorité des installations), le GIL s'applique et tout ce chapitre reste valable. Le free-threading est la direction prise par Python, mais l'écosystème (NumPy, etc.) s'y adapte encore. Sur Python 3.13+, vous pouvez vérifier l'état du GIL avec `python -c "import sys; print(sys._is_gil_enabled())"` (renvoie `True` sur un build standard, `False` si le GIL est désactivé). + +### L'autre voie : les sous-interpréteurs (Python 3.14) + +Le free-threading n'est pas la seule réponse au GIL. Depuis Python 3.12, chaque **sous-interpréteur** — plusieurs interpréteurs Python isolés au sein d'un même processus — possède **son propre GIL** (PEP 684). Ils peuvent donc s'exécuter **vraiment en parallèle**, y compris pour des calculs CPU-bound. + +La **PEP 734** rend cette possibilité accessible depuis du code Python pur en **Python 3.14**, via le module `concurrent.interpreters` et surtout un nouvel exécuteur `InterpreterPoolExecutor` — qui s'utilise exactement comme les `ThreadPoolExecutor` / `ProcessPoolExecutor` vus en 8.1 : + +```python +# Python 3.14+ +from concurrent.futures import InterpreterPoolExecutor + +def calcul(n): + return sum(i * i for i in range(n)) + +with InterpreterPoolExecutor(max_workers=4) as executor: + resultats = list(executor.map(calcul, [10_000_000] * 4)) +``` + +C'est une voie **intermédiaire entre threads et processus** : plus légère qu'un processus (on reste dans le même processus système), mais plus isolée qu'un thread — les sous-interpréteurs ne partagent pas leurs objets, les données sont copiées (via `pickle`) ou échangées par une file dédiée. Comme le free-threading, c'est récent et l'écosystème commence tout juste à s'y adapter. + --- ## Types de tâches : I/O-bound vs CPU-bound @@ -534,9 +566,9 @@ Utilisez des outils pour mesurer les performances : import time def mesurer_temps(fonction): - debut = time.time() + debut = time.perf_counter() # perf_counter : horloge monotone, idéale pour mesurer une durée resultat = fonction() - duree = time.time() - debut + duree = time.perf_counter() - debut print(f"Temps: {duree:.2f}s") return resultat ``` diff --git a/08-programmation-concurrente/exemples/01_01_threading_base.py b/08-programmation-concurrente/exemples/01_01_threading_base.py index 2ca0733..7604453 100644 --- a/08-programmation-concurrente/exemples/01_01_threading_base.py +++ b/08-programmation-concurrente/exemples/01_01_threading_base.py @@ -39,7 +39,7 @@ def telecharger_fichier(nom_fichier): fichiers = ["image1.jpg", "image2.jpg", "image3.jpg"] threads = [] -debut = time.time() +debut = time.perf_counter() for fichier in fichiers: thread = threading.Thread(target=telecharger_fichier, args=(fichier,)) threads.append(thread) @@ -48,7 +48,7 @@ def telecharger_fichier(nom_fichier): for thread in threads: thread.join() -duree = time.time() - debut +duree = time.perf_counter() - debut print(f"Tous les téléchargements sont terminés en {duree:.2f}s") print(f" (séquentiel aurait pris ~{0.3 * len(fichiers):.2f}s)") @@ -87,7 +87,7 @@ def run(self): def fonction_avec_erreur(): try: - resultat = 1 / 0 + 1 / 0 # déclenche une ZeroDivisionError except Exception as e: print(f" Erreur dans le thread: {e}") diff --git a/08-programmation-concurrente/exemples/01_02_threading_lock.py b/08-programmation-concurrente/exemples/01_02_threading_lock.py index b1702cb..1fb11e4 100644 --- a/08-programmation-concurrente/exemples/01_02_threading_lock.py +++ b/08-programmation-concurrente/exemples/01_02_threading_lock.py @@ -31,7 +31,7 @@ def incrementer_avec_lock(): thread.join() print(f"Valeur finale du compteur (avec lock): {compteur}") -print(f" Attendu: 500000") +print(" Attendu: 500000") print(f" Correct: {compteur == 500000}") # ========================================== @@ -56,7 +56,7 @@ def incrementer_sans_lock(): thread.join() print(f"Valeur finale du compteur (sans lock): {compteur_sans_lock}") -print(f" Attendu: 500000") +print(" Attendu: 500000") print(f" Correct: {compteur_sans_lock == 500000}") if compteur_sans_lock != 500000: print(f" Erreur de {500000 - compteur_sans_lock} incréments perdus (race condition)") diff --git a/08-programmation-concurrente/exemples/01_03_multiprocessing_base.py b/08-programmation-concurrente/exemples/01_03_multiprocessing_base.py index acddea5..f160e72 100644 --- a/08-programmation-concurrente/exemples/01_03_multiprocessing_base.py +++ b/08-programmation-concurrente/exemples/01_03_multiprocessing_base.py @@ -27,18 +27,18 @@ def calcul_intensif(n): def execution_sequentielle(nombres): """Exécution séquentielle""" - debut = time.time() + debut = time.perf_counter() resultats = [calcul_intensif(n) for n in nombres] - duree = time.time() - debut + duree = time.perf_counter() - debut print(f" Séquentiel: {duree:.2f} secondes") return resultats def execution_parallele(nombres): """Exécution parallèle""" - debut = time.time() + debut = time.perf_counter() with multiprocessing.Pool() as pool: resultats = pool.map(calcul_intensif, nombres) - duree = time.time() - debut + duree = time.perf_counter() - debut print(f" Parallèle: {duree:.2f} secondes") return resultats diff --git a/08-programmation-concurrente/exemples/01_05_exemple_complet_telechargeur.py b/08-programmation-concurrente/exemples/01_05_exemple_complet_telechargeur.py index 6d21fd9..41c572e 100644 --- a/08-programmation-concurrente/exemples/01_05_exemple_complet_telechargeur.py +++ b/08-programmation-concurrente/exemples/01_05_exemple_complet_telechargeur.py @@ -39,7 +39,7 @@ def telecharger_liste(self, urls: list[str]): threads = [] print(f"Démarrage de {len(urls)} téléchargements...") - debut = time.time() + debut = time.perf_counter() # Créer et démarrer les threads for url in urls: @@ -56,7 +56,7 @@ def telecharger_liste(self, urls: list[str]): for thread in threads: thread.join() - duree_totale = time.time() - debut + duree_totale = time.perf_counter() - debut print(f"\nTous les téléchargements terminés en {duree_totale:.2f}s") return self.resultats diff --git a/08-programmation-concurrente/exemples/01_06_concurrent_futures.py b/08-programmation-concurrente/exemples/01_06_concurrent_futures.py new file mode 100644 index 0000000..e71d590 --- /dev/null +++ b/08-programmation-concurrente/exemples/01_06_concurrent_futures.py @@ -0,0 +1,66 @@ +# ============================================================================ +# Section 8.1 : Threading et Multiprocessing +# Description : concurrent.futures - l'API unifiee : ThreadPoolExecutor, +# ProcessPoolExecutor, map(), submit()/Future, as_completed() +# Fichier source : 01-threading-et-multiprocessing.md +# ============================================================================ + +from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, as_completed +import time + +# Fonctions definies au niveau du module : indispensable pour ProcessPoolExecutor +# avec les methodes de demarrage spawn/forkserver (defaut Windows/macOS, et Linux +# a partir de Python 3.14), ou le processus enfant reimporte le module. + +def traiter(n): + """Tache simple : carre d'un nombre""" + return n * n + +def tache_lente(duree): + """Tache I/O simulee : attend puis renvoie un message""" + time.sleep(duree) + return f"termine apres {duree}s" + +def calcul_cpu(n): + """Tache CPU-bound : somme des carres jusqu'a n""" + return sum(i * i for i in range(n)) + +if __name__ == '__main__': + # ========================================== + # 1. ThreadPoolExecutor.map() - I/O-bound + # ========================================== + print("=== ThreadPoolExecutor.map() ===") + with ThreadPoolExecutor(max_workers=4) as executor: + resultats = list(executor.map(traiter, [1, 2, 3, 4, 5])) + print(f"map(traiter, 1..5) = {resultats}") + + # ========================================== + # 2. submit() et objets Future + # ========================================== + print("\n=== submit() et Future ===") + with ThreadPoolExecutor(max_workers=3) as executor: + futures = {executor.submit(tache_lente, d): d for d in (0.3, 0.1, 0.2)} + for future in futures: + print(f" Future(duree={futures[future]}) -> {future.result()}") + + # ========================================== + # 3. as_completed() - resultats au fil de l'eau + # ========================================== + print("\n=== as_completed() (ordre d'arrivee) ===") + with ThreadPoolExecutor(max_workers=3) as executor: + futures = [executor.submit(tache_lente, d) for d in (0.3, 0.1, 0.2)] + for future in as_completed(futures): + print(f" arrive: {future.result()}") + + # ========================================== + # 4. ProcessPoolExecutor - CPU-bound (vrai parallelisme) + # ========================================== + print("\n=== ProcessPoolExecutor.map() (CPU-bound) ===") + nombres = [100000, 200000, 300000, 400000] + debut = time.perf_counter() + with ProcessPoolExecutor() as executor: + totaux = list(executor.map(calcul_cpu, nombres)) + duree = time.perf_counter() - debut + print(f"Sommes des carres : {len(totaux)} resultats calcules") + print("Pour passer des threads aux processus : ThreadPoolExecutor -> ProcessPoolExecutor") + print(f"Calcul parallele en {duree:.3f}s") diff --git a/08-programmation-concurrente/exemples/01_07_subinterpreteurs.py b/08-programmation-concurrente/exemples/01_07_subinterpreteurs.py new file mode 100644 index 0000000..563cb87 --- /dev/null +++ b/08-programmation-concurrente/exemples/01_07_subinterpreteurs.py @@ -0,0 +1,33 @@ +# ============================================================================ +# Section 8.1 : Threading et Multiprocessing +# Description : Sous-interpreteurs (PEP 734) - InterpreterPoolExecutor, +# vrai parallelisme CPU sans multiprocessing (Python 3.14+) +# Fichier source : README.md +# ============================================================================ + +import sys + +# Fonction au niveau du module (executee dans un sous-interpreteur isole). +def calcul_cpu(n): + """Tache CPU-bound : somme des carres jusqu'a n""" + return sum(i * i for i in range(n)) + +if __name__ == '__main__': + # InterpreterPoolExecutor expose plusieurs interpreteurs Python dans le meme + # processus ; depuis Python 3.12 chaque sous-interpreteur a son propre GIL + # (PEP 684), donc ils s'executent vraiment en parallele pour le CPU-bound. + # L'API (PEP 734) arrive dans la bibliotheque standard en Python 3.14. + if sys.version_info >= (3, 14): + from concurrent.futures import InterpreterPoolExecutor + + print("=== InterpreterPoolExecutor (PEP 734, Python 3.14+) ===") + nombres = [100000, 200000, 300000, 400000] + with InterpreterPoolExecutor(max_workers=4) as executor: + totaux = list(executor.map(calcul_cpu, nombres)) + print(f"Sommes des carres : {len(totaux)} resultats calcules en parallele") + print("Chaque sous-interpreteur a son propre GIL : vrai parallelisme CPU,") + print("plus leger qu'un processus (on reste dans le meme processus systeme).") + else: + v = f"{sys.version_info.major}.{sys.version_info.minor}" + print("InterpreterPoolExecutor (sous-interpreteurs) necessite Python 3.14+") + print(f"Version actuelle : {v} -> exemple ignore") diff --git a/08-programmation-concurrente/exemples/02_01_asyncio_base.py b/08-programmation-concurrente/exemples/02_01_asyncio_base.py index bb92ab1..a7e669e 100644 --- a/08-programmation-concurrente/exemples/02_01_asyncio_base.py +++ b/08-programmation-concurrente/exemples/02_01_asyncio_base.py @@ -50,7 +50,7 @@ async def faire_cafe(nom): return f"Café pour {nom}" async def main_cafe(): - debut = time.time() + debut = time.perf_counter() tache1 = asyncio.create_task(faire_cafe("Alice")) tache2 = asyncio.create_task(faire_cafe("Bob")) @@ -60,7 +60,7 @@ async def main_cafe(): resultat2 = await tache2 resultat3 = await tache3 - duree = time.time() - debut + duree = time.perf_counter() - debut print(f"\n Tous les cafés prêts en {duree:.2f}s") print(f" Résultats: {resultat1}, {resultat2}, {resultat3}") print(f" (séquentiel aurait pris ~{0.3 * 3:.2f}s)") diff --git a/08-programmation-concurrente/exemples/02_02_gather_timeout.py b/08-programmation-concurrente/exemples/02_02_gather_timeout.py index 163d0a2..bb97503 100644 --- a/08-programmation-concurrente/exemples/02_02_gather_timeout.py +++ b/08-programmation-concurrente/exemples/02_02_gather_timeout.py @@ -29,13 +29,13 @@ async def main_gather(): ("musique.mp3", 3) ] - debut = time.time() + debut = time.perf_counter() resultats = await asyncio.gather( *[telecharger_fichier(nom, taille) for nom, taille in fichiers] ) - duree = time.time() - debut + duree = time.perf_counter() - debut print(f"\n Tous les fichiers téléchargés: {len(resultats)} en {duree:.2f}s") for resultat in resultats: print(f" - {resultat}") diff --git a/08-programmation-concurrente/exemples/02_03_scraper_comparaison.py b/08-programmation-concurrente/exemples/02_03_scraper_comparaison.py index 7eabe86..b384ddb 100644 --- a/08-programmation-concurrente/exemples/02_03_scraper_comparaison.py +++ b/08-programmation-concurrente/exemples/02_03_scraper_comparaison.py @@ -18,26 +18,26 @@ async def fetch_page(url, duree): async def scraper_synchrone(urls): """Version synchrone (une page après l'autre)""" print("=== VERSION SYNCHRONE ===") - debut = time.time() + debut = time.perf_counter() resultats = [] for url, duree in urls: resultat = await fetch_page(url, duree) resultats.append(resultat) - duree_totale = time.time() - debut + duree_totale = time.perf_counter() - debut print(f" Temps total: {duree_totale:.2f}s\n") return resultats async def scraper_asynchrone(urls): """Version asynchrone (toutes les pages en parallèle)""" print("=== VERSION ASYNCHRONE ===") - debut = time.time() + debut = time.perf_counter() taches = [fetch_page(url, duree) for url, duree in urls] resultats = await asyncio.gather(*taches) - duree_totale = time.time() - debut + duree_totale = time.perf_counter() - debut print(f" Temps total: {duree_totale:.2f}s\n") return resultats diff --git a/08-programmation-concurrente/exemples/02_05_queue_semaphore.py b/08-programmation-concurrente/exemples/02_05_queue_semaphore.py index 75fcb1a..14a8eab 100644 --- a/08-programmation-concurrente/exemples/02_05_queue_semaphore.py +++ b/08-programmation-concurrente/exemples/02_05_queue_semaphore.py @@ -78,9 +78,9 @@ async def main_semaphore(): taches = [tache_longue(i, semaphore) for i in range(1, 9)] import time - debut = time.time() + debut = time.perf_counter() resultats = await asyncio.gather(*taches) - duree = time.time() - debut + duree = time.perf_counter() - debut print(f"\n Toutes les tâches terminées: {resultats}") print(f" Durée: {duree:.2f}s (8 tâches, max 3 en parallèle)") diff --git a/08-programmation-concurrente/exemples/02_06_asyncio_vs_threading.py b/08-programmation-concurrente/exemples/02_06_asyncio_vs_threading.py index f8e8702..acec2bc 100644 --- a/08-programmation-concurrente/exemples/02_06_asyncio_vs_threading.py +++ b/08-programmation-concurrente/exemples/02_06_asyncio_vs_threading.py @@ -17,7 +17,7 @@ def operation_io_thread(numero): def executer_avec_threads(nombre): """Exécute avec threads""" - debut = time.time() + debut = time.perf_counter() threads = [] resultats = [None] * nombre @@ -32,7 +32,7 @@ def wrapper(i): for thread in threads: thread.join() - duree = time.time() - debut + duree = time.perf_counter() - debut print(f" Threading: {duree:.2f}s pour {nombre} opérations") return resultats @@ -44,12 +44,12 @@ async def operation_io_async(numero): async def executer_avec_asyncio(nombre): """Exécute avec asyncio""" - debut = time.time() + debut = time.perf_counter() taches = [operation_io_async(i) for i in range(nombre)] resultats = await asyncio.gather(*taches) - duree = time.time() - debut + duree = time.perf_counter() - debut print(f" Asyncio: {duree:.2f}s pour {nombre} opérations") return resultats @@ -68,9 +68,9 @@ async def main(): # Vérifier que les résultats sont identiques print(f"\n Résultats identiques: {r_thread == list(r_async)}") - print(f"\n Avantages d'asyncio :") - print(f" - Moins de mémoire (pas de stack par tâche)") - print(f" - Plus scalable (milliers de connexions)") - print(f" - Code plus lisible avec async/await") + print("\n Avantages d'asyncio :") + print(" - Moins de mémoire (pas de stack par tâche)") + print(" - Plus scalable (milliers de connexions)") + print(" - Code plus lisible avec async/await") asyncio.run(main()) diff --git a/08-programmation-concurrente/exemples/02_07_exemple_complet_gestionnaire.py b/08-programmation-concurrente/exemples/02_07_exemple_complet_gestionnaire.py index bd00b3b..4870cf2 100644 --- a/08-programmation-concurrente/exemples/02_07_exemple_complet_gestionnaire.py +++ b/08-programmation-concurrente/exemples/02_07_exemple_complet_gestionnaire.py @@ -24,7 +24,7 @@ async def telecharger_fichier(self, url: str, timeout: float = 10.0) -> dict: """Télécharge un fichier avec gestion d'erreurs et timeout""" async with self.semaphore: self.statistiques['total'] += 1 - debut = time.time() + debut = time.perf_counter() try: print(f" Début: {url}") @@ -34,7 +34,7 @@ async def telecharger_fichier(self, url: str, timeout: float = 10.0) -> dict: timeout=timeout ) - duree = time.time() - debut + duree = time.perf_counter() - debut self.statistiques['reussis'] += 1 print(f" Succès: {url} ({duree:.2f}s)") @@ -74,15 +74,15 @@ async def telecharger_liste(self, urls: list[str]) -> list[dict]: print(f"Concurrence max: {self.max_concurrent}") print("-" * 50) - debut_total = time.time() + debut_total = time.perf_counter() taches = [self.telecharger_fichier(url) for url in urls] resultats = await asyncio.gather(*taches, return_exceptions=True) - duree_totale = time.time() - debut_total + duree_totale = time.perf_counter() - debut_total print("-" * 50) - print(f"\nStatistiques:") + print("\nStatistiques:") print(f" Total: {self.statistiques['total']}") print(f" Réussis: {self.statistiques['reussis']}") print(f" Échoués: {self.statistiques['echoues']}") diff --git a/08-programmation-concurrente/exemples/02_08_taskgroup_timeout.py b/08-programmation-concurrente/exemples/02_08_taskgroup_timeout.py new file mode 100644 index 0000000..c5304ae --- /dev/null +++ b/08-programmation-concurrente/exemples/02_08_taskgroup_timeout.py @@ -0,0 +1,52 @@ +# ============================================================================ +# Section 8.2 : Programmation Asynchrone avec Asyncio +# Description : Concurrence structuree avec asyncio.TaskGroup et delai global +# avec asyncio.timeout (Python 3.11+) +# Fichier source : 02-programmation-asynchrone-asyncio.md +# ============================================================================ + +import asyncio +import sys + +async def telecharger(nom, duree): + """Simule un telechargement""" + await asyncio.sleep(duree) + return nom + +async def operation_longue(): + """Operation qui prend du temps""" + await asyncio.sleep(5) + return "termine" + +async def demo_taskgroup(): + """TaskGroup : toutes les taches du bloc sont terminees a la sortie ; + si l'une echoue, les autres sont annulees automatiquement.""" + print("=== asyncio.TaskGroup (3.11+) ===") + async with asyncio.TaskGroup() as tg: + t1 = tg.create_task(telecharger("video", 0.2)) + t2 = tg.create_task(telecharger("image", 0.1)) + t3 = tg.create_task(telecharger("document", 0.15)) + print(f" Tous termines : {t1.result()}, {t2.result()}, {t3.result()}") + +async def demo_timeout(): + """asyncio.timeout : impose un delai a tout un bloc (plusieurs await).""" + print("\n=== asyncio.timeout (3.11+) ===") + try: + async with asyncio.timeout(0.5): + await operation_longue() + except TimeoutError: + print(" Timeout ! Le bloc a ete interrompu apres 0.5s") + +async def main(): + await demo_taskgroup() + await demo_timeout() + +if __name__ == '__main__': + # TaskGroup et asyncio.timeout sont des constructions d'execution (3.11+) : + # on peut donc les proteger par un simple test de version. + if sys.version_info >= (3, 11): + asyncio.run(main()) + else: + v = f"{sys.version_info.major}.{sys.version_info.minor}" + print("asyncio.TaskGroup et asyncio.timeout necessitent Python 3.11+") + print(f"Version actuelle : {v} -> equivalents 3.10 : asyncio.gather et asyncio.wait_for") diff --git a/08-programmation-concurrente/exemples/02_09_annulation_to_thread.py b/08-programmation-concurrente/exemples/02_09_annulation_to_thread.py new file mode 100644 index 0000000..b247374 --- /dev/null +++ b/08-programmation-concurrente/exemples/02_09_annulation_to_thread.py @@ -0,0 +1,53 @@ +# ============================================================================ +# Section 8.2 : Programmation Asynchrone avec Asyncio +# Description : Annulation de taches (cancel / CancelledError) et delegation +# de code bloquant a un thread (asyncio.to_thread) +# Fichier source : 02-programmation-asynchrone-asyncio.md +# ============================================================================ + +import asyncio +import time + +# ========================================== +# 1. Annulation : cancel() et CancelledError +# ========================================== + +async def tache_longue(): + """Tache qui peut etre annulee ; elle nettoie puis relance l'annulation.""" + try: + print(" Tache : demarrage") + await asyncio.sleep(10) + print(" Tache : terminee") # jamais atteint si annulee + except asyncio.CancelledError: + print(" Tache : annulation recue, nettoyage...") + raise # IMPORTANT : on relance pour confirmer l'annulation + +async def demo_annulation(): + print("=== Annulation (cancel / CancelledError) ===") + tache = asyncio.create_task(tache_longue()) + await asyncio.sleep(0.2) # on la laisse demarrer + tache.cancel() # on demande l'annulation + try: + await tache + except asyncio.CancelledError: + print(" Main : la tache a bien ete annulee") + +# ========================================== +# 2. to_thread : executer du code bloquant sans geler l'event loop +# ========================================== + +def fonction_bloquante(): + """Code synchrone qui prend du temps (lib sans equivalent async, etc.)""" + time.sleep(0.3) + return "resultat (calcule dans un thread separe)" + +async def demo_to_thread(): + print("\n=== asyncio.to_thread (code bloquant) ===") + resultat = await asyncio.to_thread(fonction_bloquante) + print(f" {resultat}") + +async def main(): + await demo_annulation() + await demo_to_thread() + +asyncio.run(main()) diff --git a/08-programmation-concurrente/exemples/03_01_lock_rlock.py b/08-programmation-concurrente/exemples/03_01_lock_rlock.py index a515396..46ddc6f 100644 --- a/08-programmation-concurrente/exemples/03_01_lock_rlock.py +++ b/08-programmation-concurrente/exemples/03_01_lock_rlock.py @@ -7,7 +7,6 @@ import threading import asyncio -import time # ========================================== # 1. Race condition (sans Lock) diff --git a/08-programmation-concurrente/exemples/03_02_semaphore.py b/08-programmation-concurrente/exemples/03_02_semaphore.py index 66c5c18..991c705 100644 --- a/08-programmation-concurrente/exemples/03_02_semaphore.py +++ b/08-programmation-concurrente/exemples/03_02_semaphore.py @@ -24,12 +24,12 @@ def acceder_ressource(thread_id): threads = [threading.Thread(target=acceder_ressource, args=(i,)) for i in range(8)] -debut = time.time() +debut = time.perf_counter() for t in threads: t.start() for t in threads: t.join() -duree = time.time() - debut +duree = time.perf_counter() - debut print(f"Tous les threads ont terminé en {duree:.2f}s") print(f" (8 threads, max 3 en parallèle, ~{0.2 * (8/3):.2f}s minimum)") diff --git a/08-programmation-concurrente/exemples/03_03_event_condition.py b/08-programmation-concurrente/exemples/03_03_event_condition.py index 58e2bae..a9cb038 100644 --- a/08-programmation-concurrente/exemples/03_03_event_condition.py +++ b/08-programmation-concurrente/exemples/03_03_event_condition.py @@ -82,7 +82,7 @@ def __init__(self, taille_max=3): def produire(self, item): with self.condition: while len(self.buffer) >= self.taille_max: - print(f" Buffer plein, producteur attend...") + print(" Buffer plein, producteur attend...") self.condition.wait() self.buffer.append(item) @@ -92,7 +92,7 @@ def produire(self, item): def consommer(self): with self.condition: while len(self.buffer) == 0: - print(f" Buffer vide, consommateur attend...") + print(" Buffer vide, consommateur attend...") self.condition.wait() item = self.buffer.pop(0) diff --git a/08-programmation-concurrente/exemples/03_05_exemple_complet_cache.py b/08-programmation-concurrente/exemples/03_05_exemple_complet_cache.py index 2e626f5..0b77dd6 100644 --- a/08-programmation-concurrente/exemples/03_05_exemple_complet_cache.py +++ b/08-programmation-concurrente/exemples/03_05_exemple_complet_cache.py @@ -83,16 +83,16 @@ def travailleur_cache(cache, worker_id, operations): for i in range(3) ] -debut = time.time() +debut = time.perf_counter() for t in threads: t.start() for t in threads: t.join() -duree = time.time() - debut +duree = time.perf_counter() - debut stats = cache.get_stats() total_ops = stats['hits'] + stats['misses'] -print(f"\nStatistiques finales:") +print("\nStatistiques finales:") print(f" Hits: {stats['hits']}") print(f" Misses: {stats['misses']}") print(f" Expirations: {stats['expirations']}") diff --git a/08-programmation-concurrente/exemples/03_06_queue.py b/08-programmation-concurrente/exemples/03_06_queue.py new file mode 100644 index 0000000..dbc1fba --- /dev/null +++ b/08-programmation-concurrente/exemples/03_06_queue.py @@ -0,0 +1,58 @@ +# ============================================================================ +# Section 8.3 : Gestion des Verrous et Synchronisation +# Description : queue.Queue - file thread-safe prete a l'emploi, et ses +# variantes LifoQueue (pile) et PriorityQueue (par priorite) +# Fichier source : 03-verrous-et-synchronisation.md +# ============================================================================ + +import queue +import threading + +def producteur(file, nb_items): + """Produit des items dans la file (thread-safe, sans verrou manuel).""" + for i in range(nb_items): + file.put(f"tache-{i}") + file.put(None) # sentinelle de fin + +def consommateur(file, resultats): + """Consomme les items jusqu'a la sentinelle.""" + while True: + item = file.get() + if item is None: + break + resultats.append(item) + file.task_done() + +# ========================================== +# 1. queue.Queue : producteur-consommateur +# ========================================== +print("=== queue.Queue (producteur-consommateur) ===") + +file = queue.Queue(maxsize=10) +resultats = [] + +t_prod = threading.Thread(target=producteur, args=(file, 5)) +t_cons = threading.Thread(target=consommateur, args=(file, resultats)) +t_prod.start() +t_cons.start() +t_prod.join() +t_cons.join() + +print(f"{len(resultats)} items traites : {resultats}") + +# ========================================== +# 2. Variantes : LifoQueue et PriorityQueue +# ========================================== +print("\n=== Variantes ===") + +# LifoQueue : pile (dernier entre, premier sorti) +pile = queue.LifoQueue() +for x in [1, 2, 3]: + pile.put(x) +print(f"LifoQueue (LIFO) : {pile.get()}, {pile.get()}, {pile.get()}") + +# PriorityQueue : tri par priorite (plus petit en premier) +prio = queue.PriorityQueue() +for priorite, nom in [(3, "basse"), (1, "haute"), (2, "moyenne")]: + prio.put((priorite, nom)) +print(f"PriorityQueue : {prio.get()[1]}, {prio.get()[1]}, {prio.get()[1]} (par priorite)") diff --git a/08-programmation-concurrente/exemples/04_03_pipeline.py b/08-programmation-concurrente/exemples/04_03_pipeline.py index b30886b..035d43e 100644 --- a/08-programmation-concurrente/exemples/04_03_pipeline.py +++ b/08-programmation-concurrente/exemples/04_03_pipeline.py @@ -85,12 +85,12 @@ def etape_4_sauvegarde(queue_entree): # Demarrer le pipeline print("Demarrage du pipeline\n") -debut = time.time() +debut = time.perf_counter() for t in threads: t.start() for t in threads: t.join() -duree = time.time() - debut +duree = time.perf_counter() - debut print(f"\nPipeline complet en {duree:.2f}s") diff --git a/08-programmation-concurrente/exemples/04_04_fan_out_fan_in.py b/08-programmation-concurrente/exemples/04_04_fan_out_fan_in.py index 416a871..a9b9879 100644 --- a/08-programmation-concurrente/exemples/04_04_fan_out_fan_in.py +++ b/08-programmation-concurrente/exemples/04_04_fan_out_fan_in.py @@ -78,7 +78,7 @@ def collecteur(queue_resultats, nombre_taches): # Demarrer print("Demarrage Fan-Out/Fan-In\n") -debut = time.time() +debut = time.perf_counter() thread_dispatcher.start() for t in threads_workers: @@ -91,5 +91,5 @@ def collecteur(queue_resultats, nombre_taches): t.join() thread_collecteur.join() -duree = time.time() - debut +duree = time.perf_counter() - debut print(f"\nFan-Out/Fan-In complet en {duree:.2f}s") diff --git a/08-programmation-concurrente/exemples/04_06_map_reduce.py b/08-programmation-concurrente/exemples/04_06_map_reduce.py index 77f19b6..7487e98 100644 --- a/08-programmation-concurrente/exemples/04_06_map_reduce.py +++ b/08-programmation-concurrente/exemples/04_06_map_reduce.py @@ -13,7 +13,11 @@ # ========================================== # 1. Map-Reduce simple (somme des carres) # ========================================== -print("=== Map-Reduce (somme des carres) ===\n") +# Note : les fonctions passees a un ProcessPoolExecutor doivent etre definies +# au niveau du module (et non dans le bloc if __name__). Sinon, avec les +# methodes de demarrage 'spawn'/'forkserver' (defaut sous Windows/macOS, et +# sous Linux a partir de Python 3.14), le processus enfant reimporte le module +# sans les voir -> BrokenProcessPool. def map_function(nombre): """Phase Map: Calcule le carre""" @@ -23,22 +27,40 @@ def reduce_function(resultats): """Phase Reduce: Somme tous les carres""" return sum(resultats) +# ========================================== +# 2. Map-Reduce : Analyse de texte +# ========================================== + +def compter_mots(texte): + """Phase Map: Compte les mots dans un texte""" + mots = re.findall(r'\w+', texte.lower()) + return Counter(mots) + +def fusionner_compteurs(compteurs): + """Phase Reduce: Fusionne tous les compteurs""" + resultat = Counter() + for compteur in compteurs: + resultat.update(compteur) + return resultat + if __name__ == '__main__': + print("=== Map-Reduce (somme des carres) ===\n") + nombres = list(range(1, 101)) # Phase Map (parallele) with ProcessPoolExecutor() as executor: - debut = time.time() + debut = time.perf_counter() carres = list(executor.map(map_function, nombres)) - duree_map = time.time() - debut + duree_map = time.perf_counter() - debut # Phase Reduce - debut = time.time() + debut = time.perf_counter() total = reduce_function(carres) - duree_reduce = time.time() - debut + duree_reduce = time.perf_counter() - debut - print(f"Map-Reduce:") - print(f" Nombres: 1-100") + print("Map-Reduce:") + print(" Nombres: 1-100") print(f" Somme des carres: {total}") print(f" Temps Map: {duree_map:.3f}s") print(f" Temps Reduce: {duree_reduce:.3f}s") @@ -48,7 +70,7 @@ def reduce_function(resultats): print(f" Verification: {total == attendu} (attendu: {attendu})") # ========================================== - # 2. Map-Reduce : Analyse de texte + # Analyse de texte # ========================================== print("\n=== Map-Reduce (analyse de texte) ===\n") @@ -59,18 +81,6 @@ def reduce_function(resultats): "Python est un excellent langage pour debuter.", ] - def compter_mots(texte): - """Phase Map: Compte les mots dans un texte""" - mots = re.findall(r'\w+', texte.lower()) - return Counter(mots) - - def fusionner_compteurs(compteurs): - """Phase Reduce: Fusionne tous les compteurs""" - resultat = Counter() - for compteur in compteurs: - resultat.update(compteur) - return resultat - # Phase Map with ProcessPoolExecutor() as executor: compteurs = list(executor.map(compter_mots, documents)) diff --git a/08-programmation-concurrente/exemples/04_09_rate_limiting.py b/08-programmation-concurrente/exemples/04_09_rate_limiting.py index 2c737bf..0aa587d 100644 --- a/08-programmation-concurrente/exemples/04_09_rate_limiting.py +++ b/08-programmation-concurrente/exemples/04_09_rate_limiting.py @@ -98,6 +98,6 @@ async def demo_token_bucket(): bucket = TokenBucket(rate=5.0, capacity=5) taches = [tache_limitee(i, bucket) for i in range(12)] await asyncio.gather(*taches) - print(f"\n12 taches executees avec Token Bucket") + print("\n12 taches executees avec Token Bucket") asyncio.run(demo_token_bucket()) diff --git a/08-programmation-concurrente/exemples/04_10_exemple_complet_scraper.py b/08-programmation-concurrente/exemples/04_10_exemple_complet_scraper.py index f360215..57a54d4 100644 --- a/08-programmation-concurrente/exemples/04_10_exemple_complet_scraper.py +++ b/08-programmation-concurrente/exemples/04_10_exemple_complet_scraper.py @@ -36,6 +36,7 @@ class WebScraperSystem: """Systeme de scraping combinant plusieurs patterns""" def __init__(self, max_concurrent=5, rate_limit=10): + self.max_concurrent = max_concurrent self.semaphore = asyncio.Semaphore(max_concurrent) self.rate_limiter = RateLimiter(rate_limit, period=1.0) self.resultats = [] @@ -99,9 +100,9 @@ async def scraper_urls(self, urls: list[str]): Pattern: Fan-Out/Fan-In + Worker Pool """ print(f"Demarrage du scraping de {len(urls)} URLs") - print(f"Config: max {self.semaphore._value} concurrent, rate limit {self.rate_limiter.max_calls}/s\n") + print(f"Config: max {self.max_concurrent} concurrent, rate limit {self.rate_limiter.max_calls}/s\n") - debut = time.time() + debut = time.perf_counter() session = None # En vrai: aiohttp.ClientSession() @@ -111,7 +112,7 @@ async def scraper_urls(self, urls: list[str]): # Fan-In: Collecter tous les resultats resultats = await asyncio.gather(*taches, return_exceptions=True) - duree = time.time() - debut + duree = time.perf_counter() - debut # Statistiques succes = sum(1 for r in resultats if isinstance(r, dict) and r.get('status') != 'error') @@ -140,7 +141,7 @@ def analyser_resultats(self): print(f" Total de mots: {len(tous_les_mots)}") print(f" Mots uniques: {len(compteur)}") - print(f" Top 5 mots:") + print(" Top 5 mots:") for mot, count in compteur.most_common(5): print(f" - {mot}: {count} fois") @@ -152,7 +153,7 @@ async def main(): scraper = WebScraperSystem(max_concurrent=5, rate_limit=10) - resultats = await scraper.scraper_urls(urls) + await scraper.scraper_urls(urls) scraper.analyser_resultats() diff --git a/08-programmation-concurrente/exemples/README.md b/08-programmation-concurrente/exemples/README.md index 90296af..991d1cd 100644 --- a/08-programmation-concurrente/exemples/README.md +++ b/08-programmation-concurrente/exemples/README.md @@ -1,5 +1,15 @@ # Chapitre 08 - Programmation Concurrente : Exemples +Ce dossier contient les exemples exécutables du chapitre 8, un fichier `.py` par thème, numérotés selon la section du cours (`01_*` → 8.1, `02_*` → 8.2, `03_*` → 8.3, `04_*` → 8.4). + +**Exécution** : chaque fichier est autonome. + +```bash +python3 01_01_threading_base.py +``` + +> Les exemples qui mesurent des durées ou utilisent du hasard (`random`) produisent des valeurs **variables** selon la machine et l'exécution ; seules les valeurs **déterministes** (compteurs, sommes, décomptes) sont indiquées ci-dessous comme « Sortie attendue ». + ## Section 8.1 : Threading et Multiprocessing ### 01_01_threading_base.py @@ -44,6 +54,23 @@ - 5 fichiers telecharges en parallele (max 3 threads), ~0.60s - Resultats affiches avec taille et duree +### 01_06_concurrent_futures.py +- **Section** : 8.1 - Threading et Multiprocessing +- **Description** : concurrent.futures - ThreadPoolExecutor (map, submit/Future, as_completed) et ProcessPoolExecutor, l'API unifiee threads/processus +- **Fichier source** : `01-threading-et-multiprocessing.md` +- **Sortie attendue** : + - map(traiter, 1..5) = [1, 4, 9, 16, 25] + - as_completed : resultats dans l'ordre d'arrivee (0.1s, 0.2s, 0.3s) + - ProcessPoolExecutor : 4 sommes de carres calculees en parallele + +### 01_07_subinterpreteurs.py +- **Section** : 8.1 - Threading et Multiprocessing +- **Description** : Sous-interpreteurs (PEP 734) - InterpreterPoolExecutor, vrai parallelisme CPU plus leger que le multiprocessing +- **Fichier source** : `README.md` +- **Sortie attendue** : + - Sur Python 3.14+ : 4 sommes de carres calculees en parallele (chaque sous-interpreteur a son propre GIL) + - Sur Python < 3.14 : message indiquant que l'exemple necessite 3.14+ + ## Section 8.2 : Programmation Asynchrone (asyncio) ### 02_01_asyncio_base.py @@ -105,6 +132,22 @@ - 8 fichiers telecharges avec semaphore(3) - Statistiques : succes, echecs, duree totale, vitesse +### 02_08_taskgroup_timeout.py +- **Section** : 8.2 - Programmation asynchrone avec asyncio +- **Description** : Concurrence structuree avec asyncio.TaskGroup et delai global avec asyncio.timeout (Python 3.11+) +- **Fichier source** : `02-programmation-asynchrone-asyncio.md` +- **Sortie attendue** : + - Sur Python 3.11+ : TaskGroup execute 3 taches (video, image, document) ; timeout interrompt un bloc apres 0.5s + - Sur Python 3.10 : message renvoyant aux equivalents (gather, wait_for) + +### 02_09_annulation_to_thread.py +- **Section** : 8.2 - Programmation asynchrone avec asyncio +- **Description** : Annulation de taches (cancel / CancelledError avec nettoyage puis relance) et delegation de code bloquant a un thread (asyncio.to_thread) +- **Fichier source** : `02-programmation-asynchrone-asyncio.md` +- **Sortie attendue** : + - Annulation : tache demarree puis annulee proprement (nettoyage + confirmation cote main) + - to_thread : resultat calcule dans un thread separe sans bloquer l'event loop + ## Section 8.3 : Gestion des Verrous et Synchronisation ### 03_01_lock_rlock.py @@ -151,6 +194,15 @@ - Singleton : s1 is s2 = True - ReadWriteLock : lectures paralleles, ecriture exclusive +### 03_06_queue.py +- **Section** : 8.3 - Gestion des Verrous et Synchronisation +- **Description** : queue.Queue (file thread-safe prete a l'emploi, sans verrou manuel) et variantes LifoQueue (pile) et PriorityQueue (par priorite) +- **Fichier source** : `03-verrous-et-synchronisation.md` +- **Sortie attendue** : + - queue.Queue : 5 items produits et consommes + - LifoQueue : 3, 2, 1 (ordre LIFO) + - PriorityQueue : haute, moyenne, basse (par priorite) + ## Section 8.4 : Patterns de Concurrence ### 04_01_producteur_consommateur.py @@ -236,3 +288,10 @@ - 20 URLs scrapees avec semaphore(5) et rate limit 10/s - 20 succes, 0 echecs - Analyse Map-Reduce : 80 mots, top 5 affiches + +## Notes + +- **Style des fichiers** : les accents français sont conservés (é, è, à…), mais les émojis, le symbole € et les flèches sont rendus en ASCII (`->`, `EUR`…) pour une portabilité maximale des sorties terminal. Les `.md` du cours, eux, utilisent des émojis : c'est une différence de présentation voulue, pas une incohérence. +- **Multiprocessing** : les fonctions exécutées dans un `ProcessPoolExecutor` / `multiprocessing.Pool` sont définies **au niveau du module** (jamais à l'intérieur du bloc `if __name__ == '__main__'`). C'est indispensable avec les méthodes de démarrage `spawn` / `forkserver` (défaut sous Windows/macOS, et sous **Linux à partir de Python 3.14**), où le processus enfant réimporte le module : une fonction définie sous le garde y serait invisible (`BrokenProcessPool`). +- **Mesure du temps** : les durées utilisent `time.perf_counter()` (horloge monotone, adaptée aux mesures d'intervalle), comme dans le cours. `time.time()` n'est conservé que pour les *timestamps* horodatés (expiration de cache, rate-limiting). +- **Compatibilité** : les 32 exemples s'exécutent sur **Python 3.10 à 3.14** (vérifié). La plupart utilisent le style compatible 3.10 (`asyncio.wait_for`, `asyncio.gather`). Les exemples reposant sur des nouveautés récentes sont protégés par un test `sys.version_info` qui affiche un message de repli sur les versions antérieures : `02_08` (`asyncio.TaskGroup` / `asyncio.timeout`, Python 3.11+) et `01_07` (`InterpreterPoolExecutor`, Python 3.14+). diff --git a/09-erreurs-et-debogage/01-hierarchie-des-exceptions.md b/09-erreurs-et-debogage/01-hierarchie-des-exceptions.md index b4ecbcb..6ff5e89 100644 --- a/09-erreurs-et-debogage/01-hierarchie-des-exceptions.md +++ b/09-erreurs-et-debogage/01-hierarchie-des-exceptions.md @@ -74,6 +74,8 @@ BaseException └── FutureWarning ``` +> **Depuis Python 3.11**, deux branches complètent cet arbre : `BaseExceptionGroup` (sous `BaseException`) et `ExceptionGroup` (sous `Exception`). Elles permettent de regrouper *plusieurs* exceptions levées en même temps — voir la section « Les groupes d'exceptions » plus bas. + ## Les exceptions les plus courantes ### 1. **BaseException** - La racine de tout @@ -350,6 +352,40 @@ except LookupError: print("Erreur de recherche") # Plus général ensuite ``` +## Les groupes d'exceptions : `ExceptionGroup` et `except*` (Python 3.11+) + +Parfois, plusieurs erreurs surviennent **en même temps** : par exemple plusieurs tâches asynchrones qui échouent (`asyncio.TaskGroup`, voir la section 8.2), ou plusieurs validations qui échouent ensemble. Depuis Python 3.11, on peut les regrouper dans un **`ExceptionGroup`**. + +```python +def valider_formulaire(): + raise ExceptionGroup("Le formulaire contient des erreurs", [ + ValueError("L'âge doit être positif"), + TypeError("Le nom doit être une chaîne"), + ValueError("L'email est invalide"), + ]) +``` + +Pour les traiter, on utilise la nouvelle syntaxe **`except*`** (avec un astérisque), qui capture *toutes* les exceptions d'un type donné dans le groupe : + +```python +try: + valider_formulaire() +except* ValueError as eg: + print(f"{len(eg.exceptions)} erreur(s) de valeur") +except* TypeError as eg: + print(f"{len(eg.exceptions)} erreur(s) de type") +``` + +**Sortie** : +``` +2 erreur(s) de valeur +1 erreur(s) de type +``` + +Différence clé avec `except` classique : un `except` normal ne traite qu'**une seule** exception, tandis que `except*` peut déclencher **plusieurs** branches pour un même groupe — ici, la branche `ValueError` *et* la branche `TypeError` s'exécutent toutes les deux. + +> **À noter** : `except*` ne se mélange pas avec `except` classique dans un même `try`. Réservez les groupes d'exceptions aux cas où plusieurs erreurs indépendantes peuvent réellement survenir ensemble (concurrence, validations groupées) ; pour une erreur unique, l'`except` classique reste la norme. + ## Résumé - Toutes les exceptions Python sont organisées en hiérarchie diff --git a/09-erreurs-et-debogage/02-exceptions-personnalisees.md b/09-erreurs-et-debogage/02-exceptions-personnalisees.md index db40d3f..356474e 100644 --- a/09-erreurs-et-debogage/02-exceptions-personnalisees.md +++ b/09-erreurs-et-debogage/02-exceptions-personnalisees.md @@ -90,6 +90,8 @@ except AgeInvalideError as e: print(f"Âge problématique : {e.age}") # Accès à l'attribut ``` +> 📝 **À quoi sert `super().__init__(message)` ?** En transmettant le message à la classe `Exception` parente, vous le stockez dans `e.args` et vous faites en sorte que **`str(e)` affiche ce message** (c'est le comportement par défaut d'`Exception`). Sans cet appel, `print(e)` n'afficherait rien d'utile. C'est pourquoi on le retrouve dans presque toutes les exceptions personnalisées — même lorsqu'on ajoute d'autres attributs. + ## Exemples concrets d'exceptions personnalisées ### Exemple 1 : Application bancaire @@ -352,6 +354,55 @@ except ErreurTransfert as e: # On peut aussi logger les détails ``` +## Chaîner les exceptions : `raise ... from ...` + +Très souvent, une exception personnalisée naît d'une exception de plus bas niveau (un fichier absent, une erreur réseau, un parsing raté). La bonne pratique est de **traduire** l'erreur technique en erreur métier **tout en conservant la cause d'origine**, grâce à `raise ... from ...`. + +```python +class ConfigError(Exception): + """Erreur de configuration de l'application""" + pass + +def charger_config(chemin): + try: + with open(chemin, encoding="utf-8") as f: + return int(f.read()) + except FileNotFoundError as e: + raise ConfigError(f"Configuration introuvable : {chemin}") from e +``` + +Si le fichier est absent, la trace montre **les deux** exceptions, reliées : + +``` +FileNotFoundError: [Errno 2] No such file or directory: 'absent.txt' + +The above exception was the direct cause of the following exception: + +ConfigError: Configuration introuvable : absent.txt +``` + +C'est précieux pour le débogage : l'utilisateur voit l'erreur métier claire (`ConfigError`), et le développeur garde la cause technique exacte (`FileNotFoundError`). + +### Trois cas à connaître + +| Écriture | Effet | +|----------|-------| +| `raise NouvelleErreur(...) from cause` | Lie explicitement la cause (`__cause__`) — « *direct cause of* » | +| `raise NouvelleErreur(...)` dans un `except` | Chaînage **implicite** (`__context__`) — « *During handling...* » | +| `raise NouvelleErreur(...) from None` | **Supprime** la chaîne (masque la cause de bas niveau) | + +> **Astuce (Python 3.11+)** : pour ajouter du contexte sans changer le type de l'exception, utilisez `add_note()`. Les notes apparaissent dans la trace, sous le message d'erreur. + +```python +try: + traiter_ligne(ligne) +except ValueError as e: + e.add_note(f"Erreur à la ligne {numero} du fichier {chemin}") + raise +``` + +> 📝 **`raise` tout seul (sans argument) re-lève l'exception en cours.** Dans un bloc `except`, après un traitement partiel (ajouter une note, journaliser, libérer une ressource), `raise` **re-propage** l'exception d'origine **telle quelle**, avec sa trace complète intacte — pour qu'un appelant plus haut puisse la traiter. À ne pas confondre avec `raise UneException(...)`, qui lève une *nouvelle* exception. + ## Bonnes pratiques ### ✅ À FAIRE @@ -542,6 +593,7 @@ except ErreurBibliotheque as e: - Ajoutez des attributs pour stocker des informations contextuelles - Documentez vos exceptions avec des docstrings - Utilisez `super().__init__(message)` pour initialiser correctement l'exception parente +- Chaînez les exceptions avec `raise ... from ...` pour traduire une erreur de bas niveau en erreur métier sans perdre la cause d'origine --- diff --git a/09-erreurs-et-debogage/03-techniques-de-debogage.md b/09-erreurs-et-debogage/03-techniques-de-debogage.md index 1b9106f..bfbe1e9 100644 --- a/09-erreurs-et-debogage/03-techniques-de-debogage.md +++ b/09-erreurs-et-debogage/03-techniques-de-debogage.md @@ -35,6 +35,22 @@ resultat = calculer_moyenne(notes) ### Techniques avancées avec print() +**La f-string auto-documentée `f"{x=}"` (Python 3.8+) — le raccourci de débogage :** + +Le suffixe `=` dans une f-string affiche **à la fois le nom et la valeur** de l'expression. C'est l'idiome moderne du débogage par `print` : il évite de répéter le nom et le risque de le désynchroniser de la variable. + +```python +x = 42 +nom = "Alice" + +print(f"{x=}") # x=42 +print(f"{nom=}") # nom='Alice' +print(f"{x * 2=}") # x * 2=84 (fonctionne aussi avec une expression) +print(f"{x=:.2f}") # x=42.00 (avec un format) +``` + +Plus besoin d'écrire `print(f"x = {x}")` : `print(f"{x=}")` fait la même chose, en plus court et sans risque d'erreur de recopie du nom. + **Afficher le type d'une variable :** ```python valeur = "123" @@ -83,6 +99,8 @@ Une assertion est une vérification que vous placez dans votre code pour vous as assert condition, "Message d'erreur optionnel" ``` +> ⚠️ **Piège classique : pas de parenthèses autour de `condition, message` !** Écrire `assert (condition, message)` (avec des parenthèses) crée un **tuple** `(condition, message)`. Or un tuple non vide est **toujours vrai** : l'assertion réussit **systématiquement** et ne vérifie donc plus rien — un bug silencieux. La forme correcte est `assert condition, message`, **sans** parenthèses. Depuis Python 3.12, l'interpréteur émet d'ailleurs un `SyntaxWarning` pour signaler cette erreur. + ### Exemples pratiques **Vérifier qu'une valeur est positive :** @@ -234,6 +252,28 @@ def traiter_donnees(donnees): traiter_donnees([10, 20, 30, 40]) ``` +### Logger une exception avec sa trace complète + +Dans un bloc `except`, préférez **`logging.exception()`** à `logging.error()` : appelé depuis un gestionnaire d'exception, il enregistre le message **et** la trace complète (traceback), bien plus utile pour déboguer. + +```python +import logging + +logging.basicConfig(level=logging.DEBUG) + +def traiter(donnees): + try: + return sum(donnees) / len(donnees) + except ZeroDivisionError: + # À appeler uniquement dans un except : ajoute automatiquement la trace + logging.exception("Échec du traitement (liste vide ?)") + return None + +traiter([]) +``` + +`logging.error(f"...{e}")` n'affiche que le message ; `logging.exception("...")` y ajoute le traceback complet, sans avoir à le formater soi-même (c'est un raccourci pour `logging.error(..., exc_info=True)`). + --- ## 4. Le débogueur Python (pdb) - L'outil puissant @@ -270,6 +310,8 @@ def calculer_factorielle(n): calculer_factorielle(5) ``` +> 📝 **Pourquoi `breakpoint()` plutôt que `pdb.set_trace()` ?** `breakpoint()` (Python 3.7+) fait la même chose, mais en respectant la variable d'environnement **`PYTHONBREAKPOINT`**. On peut ainsi **désactiver tous les points d'arrêt sans toucher au code** (`PYTHONBREAKPOINT=0 python script.py`), ou **brancher un autre débogueur** (celui d'un IDE, par exemple) — sans avoir à chercher-remplacer les `pdb.set_trace()` dans tout le projet. + ### Commandes essentielles de pdb | Commande | Raccourci | Description | @@ -539,6 +581,49 @@ def fonction_principale(): fonction_principale() ``` +### 6.5 Le module warnings - Signaler sans planter + +Parfois, vous voulez **avertir** sans interrompre le programme : une fonction obsolète, un paramètre douteux, une situation à surveiller. C'est le rôle du module `warnings` (à ne pas confondre avec le niveau `WARNING` de `logging` : ici on signale un problème de *code*, pas un simple événement d'exécution). + +```python +import warnings + +def diviser(a, b): + if b == 0: + warnings.warn("Division par zéro évitée, retour de None", UserWarning) + return None + return a / b + +diviser(10, 0) +# script.py:5: UserWarning: Division par zéro évitée, retour de None +``` + +**Déprécier une fonction** (l'usage le plus courant) : + +```python +def ancienne_api(): + warnings.warn( + "ancienne_api() est obsolète, utilisez nouvelle_api()", + DeprecationWarning, + stacklevel=2, # pointe vers l'appelant, pas vers cette ligne + ) + return 42 +``` + +**Contrôler les avertissements** — pratique en débogage et dans les tests : + +```python +import warnings + +# Transformer TOUS les avertissements en exceptions (pour les traquer) +warnings.simplefilter("error") + +# Ou, sans modifier le code, en ligne de commande : +# python -W error mon_script.py +``` + +> **Bon à savoir** : par défaut, `DeprecationWarning` est **masqué** (sauf lorsqu'il provient du module `__main__`). C'est voulu : les utilisateurs finaux ne sont pas pollués par ces messages, mais les développeurs et les tests (lancés avec `-W error` ou `-W default`) les détectent. + --- ## 7. Stratégies et bonnes pratiques de débogage @@ -711,15 +796,12 @@ print(prenom) # prenom n'a pas été défini **Débogage :** ```python -# Vérifier si la variable existe -import sys - +# Vérifier proprement si la variable existe, avec try/except def afficher_nom(): - if 'prenom' in dir(): + try: print(prenom) - else: + except NameError: print("La variable 'prenom' n'existe pas") - print("Variables disponibles :", [v for v in dir() if not v.startswith('_')]) ``` ### Erreur 3 : TypeError diff --git a/09-erreurs-et-debogage/04-profiling-et-optimisation.md b/09-erreurs-et-debogage/04-profiling-et-optimisation.md index 58397b1..fea1d7b 100644 --- a/09-erreurs-et-debogage/04-profiling-et-optimisation.md +++ b/09-erreurs-et-debogage/04-profiling-et-optimisation.md @@ -36,15 +36,15 @@ Cela signifie qu'il faut : ## 1. Mesurer le temps d'exécution - Les bases -### 1.1 La méthode time.time() +### 1.1 La méthode time.perf_counter() -La façon la plus simple de mesurer le temps d'exécution : +La façon la plus simple de mesurer le temps d'exécution est d'encadrer le code par deux relevés d'horloge. Utilisez `time.perf_counter()` : c'est l'horloge **monotone** de plus haute résolution, conçue précisément pour mesurer des **durées** (contrairement à `time.time()`, qui donne l'heure « murale » et peut même reculer lors d'un ajustement de l'horloge système). ```python import time # Enregistrer le temps de début -debut = time.time() +debut = time.perf_counter() # Code à mesurer total = 0 @@ -52,7 +52,7 @@ for i in range(1000000): total += i # Enregistrer le temps de fin -fin = time.time() +fin = time.perf_counter() # Calculer la durée duree = fin - debut @@ -83,9 +83,9 @@ def chronometrer(fonction, *args, **kwargs): Returns: tuple : (résultat, temps_execution) """ - debut = time.time() + debut = time.perf_counter() resultat = fonction(*args, **kwargs) - fin = time.time() + fin = time.perf_counter() duree = fin - debut return resultat, duree @@ -110,9 +110,9 @@ from contextlib import contextmanager def chronometre(nom="Code"): """Gestionnaire de contexte pour chronométrer un bloc de code.""" print(f"⏱️ Début du chronométrage : {nom}") - debut = time.time() + debut = time.perf_counter() yield - fin = time.time() + fin = time.perf_counter() duree = fin - debut print(f"✅ {nom} terminé en {duree:.4f} secondes") @@ -138,7 +138,7 @@ with chronometre("Création d'une liste"): ### 2.1 Pourquoi utiliser timeit ? -Le module `timeit` est plus précis que `time.time()` car il : +Le module `timeit` est encore plus précis qu'un chronométrage manuel car il : - Exécute le code plusieurs fois pour obtenir une moyenne - Désactive temporairement le garbage collector - Fournit des résultats plus fiables @@ -227,6 +227,8 @@ print(f"Méthode rapide : {temps_rapide:.4f} secondes") print(f"Amélioration : {temps_lent/temps_rapide:.2f}x plus rapide ! 🚀") ``` +> 📝 **`timeit.timeit()` : une chaîne *ou* une fonction.** On peut passer le code à mesurer sous deux formes : une **chaîne** (`'sum(range(1000))'`) — pratique pour un extrait court, mais qui s'exécute dans un espace de noms isolé — ou un **objet appelable** (une fonction, ou un `lambda: ma_fonction(args)` pour lui passer des arguments). La forme `lambda` est la plus commode dès qu'on veut mesurer une fonction existante avec ses paramètres. + ### 2.5 timeit en ligne de commande Vous pouvez aussi utiliser timeit directement depuis le terminal : @@ -281,7 +283,7 @@ cProfile.run('programme_principal()') ``` Démarrage du programme... Résultats calculés : 5 valeurs - 15 function calls in 0.245 seconds + 18 function calls in 0.245 seconds Ordered by: standard name @@ -292,6 +294,8 @@ Résultats calculés : 5 valeurs 1 0.000 0.000 0.245 0.245 script.py:19(programme_principal) ``` +*(Tableau réduit aux fonctions principales pour la lisibilité ; les appels aux fonctions intégrées comme `print` et `len`, comptés dans le total de 18, ont été retirés. Les temps varient d'une machine à l'autre.)* + **Explication des colonnes :** - **ncalls** : Nombre d'appels de la fonction - **tottime** : Temps total passé dans la fonction (sans les sous-fonctions) @@ -412,10 +416,10 @@ mesurer_taille(texte, "Texte") **Sortie :** ``` -Petite liste: 104 octets -Grande liste: 8.00 Mo -Dictionnaire: 36.66 Ko -Texte: 58.59 Ko +Petite liste: 104.00 octets +Grande liste: 7.63 Mo +Dictionnaire: 36.09 Ko +Texte: 58.63 Ko ``` ### 4.2 Comparer l'utilisation mémoire de différentes structures @@ -447,7 +451,45 @@ def comparer_structures(n=1000): comparer_structures(10000) ``` -### 4.3 Le module memory_profiler (installation requise) +### 4.3 Le module tracemalloc (inclus dans Python) + +`tracemalloc` est le profileur mémoire **de la bibliothèque standard** (aucune installation). Il trace les allocations et indique d'où vient la mémoire consommée, ligne par ligne. + +```python +import tracemalloc + +tracemalloc.start() # Démarrer le suivi des allocations + +# Code à analyser +donnees = [i ** 2 for i in range(100000)] +mapping = {i: str(i) for i in range(100000)} + +# Photographier l'état de la mémoire +snapshot = tracemalloc.take_snapshot() +top = snapshot.statistics('lineno') + +print("Top 3 des allocations mémoire :") +for stat in top[:3]: + print(f" {stat}") + +# Mémoire actuelle et pic atteint depuis le start() +actuel, pic = tracemalloc.get_traced_memory() +print(f"\nMémoire actuelle : {actuel / 1024:.1f} Ko ; pic : {pic / 1024:.1f} Ko") +tracemalloc.stop() +``` + +**Sortie (exemple) :** +``` +Top 3 des allocations mémoire : + script.py:6: size=12.4 MiB, count=199744, average=65 B + script.py:5: size=3907 KiB, count=99985, average=40 B + ... +Mémoire actuelle : 16626.6 Ko ; pic : 18224.3 Ko +``` + +Contrairement à `sys.getsizeof` (qui ne mesure qu'un seul objet, sans ses contenus), `tracemalloc` suit **toutes** les allocations du programme et les attribue à la ligne de code responsable — idéal pour traquer une fuite ou un pic de mémoire. + +### 4.4 Le module memory_profiler (installation requise) Pour une analyse mémoire ligne par ligne, vous pouvez installer `memory_profiler` : @@ -529,6 +571,8 @@ Recherche d'un élément : **Leçon : Utilisez un set pour les tests d'appartenance !** +> 📝 **Pourquoi un `set` est-il si rapide ?** Un `set` (comme un `dict`) est une **table de hachage** : tester `x in mon_set` calcule le *hash* de `x` et va directement à la bonne case — un temps quasi **constant** (`O(1)`), indépendant du nombre d'éléments. Une **liste**, elle, n'a pas d'index par valeur : `x in ma_liste` la parcourt **élément par élément** jusqu'à trouver (ou épuiser la liste), soit un temps **proportionnel** à sa taille (`O(n)`). D'où l'écart qui se creuse à mesure que les données grossissent. + ### 5.2 Éviter les calculs répétitifs **❌ Version non optimisée (calcule plusieurs fois la même chose) :** @@ -539,7 +583,7 @@ def calculer_distances_lente(points): distances = [] for i in range(len(points)): for j in range(len(points)): - # Calcule len(points) à chaque itération ! + # range(len(points)) ré-évalue len() à chaque tour de la boucle externe distance = abs(points[i] - points[j]) distances.append(distance) return distances @@ -674,6 +718,8 @@ print(f" Amélioration : {temps_sans/temps_avec:.0f}x plus rapide ! 🚀🚀 # Résultat typique : 100,000x plus rapide ! ``` +> **Python 3.9+** : `@functools.cache` est un raccourci pour `@lru_cache(maxsize=None)` — un cache non borné, au code plus court. Pour l'exemple ci-dessus : `from functools import cache` puis `@cache` au lieu de `@lru_cache(maxsize=None)`. + ### 5.6 Utiliser des générateurs pour économiser la mémoire **Problème : Traiter une grande quantité de données** @@ -710,6 +756,8 @@ Générateur : 200 octets Le générateur utilise 42,244x moins de mémoire ! ``` +> 📝 La taille exacte d'un générateur varie un peu selon la version de Python (≈ 100 à 200 octets), mais l'essentiel est qu'elle est **constante** — indépendante de `n` — là où la liste grossit proportionnellement à `n`. Le rapport exact importe donc moins que l'ordre de grandeur : un générateur reste minuscule, quelle que soit la quantité de données parcourues. + ### 5.7 Utiliser join() pour concaténer des chaînes ```python @@ -737,6 +785,8 @@ print(f" Méthode join : {temps_join:.4f} secondes") print(f" Amélioration : {temps_plus/temps_join:.2f}x plus rapide") ``` +> 📝 **Pourquoi `+` est-il lent pour concaténer en boucle ?** Les chaînes Python sont **immuables** : `resultat = resultat + str(i)` ne modifie pas `resultat`, il **crée une nouvelle chaîne** en recopiant tout le contenu déjà accumulé, à chaque tour. Sur `n` tours, on recopie une quantité croissante de caractères — un coût total en `O(n²)`. `"".join(...)` calcule d'abord la taille finale puis alloue le résultat **une seule fois** : un coût en `O(n)`. + --- ## 6. Optimisation avec NumPy (pour le calcul scientifique) @@ -783,6 +833,8 @@ Addition de deux séquences de 1,000,000 d'éléments : NumPy est 190.70x plus rapide ! 🚀 ``` +> 📝 **D'où vient cette rapidité ?** Trois raisons : (1) un tableau NumPy range ses nombres de façon **contiguë** en mémoire, dans un type homogène (et non comme des objets Python individuels dispersés) ; (2) les opérations (`array1 + array2`, `np.sqrt(...)`) sont **vectorisées** : la boucle s'exécute en **C compilé**, pas dans l'interpréteur Python ; (3) elles peuvent exploiter les instructions **SIMD** du processeur (plusieurs additions en une seule instruction). Une boucle Python équivalente paie, elle, le coût de l'interpréteur à *chaque* itération. + ### 6.2 Opérations vectorisées ```python @@ -1222,7 +1274,7 @@ comparer_versions() ## Conclusion L'optimisation est un art qui nécessite : -- **Mesure** : Utilisez des outils comme timeit, cProfile et memory_profiler +- **Mesure** : Utilisez les outils de la bibliothèque standard — `time.perf_counter()`, `timeit`, `cProfile`, `tracemalloc` — avant tout outil tiers - **Analyse** : Identifiez les vraies causes de lenteur - **Action** : Appliquez les bonnes techniques d'optimisation - **Vérification** : Assurez-vous que le code fonctionne toujours diff --git a/09-erreurs-et-debogage/README.md b/09-erreurs-et-debogage/README.md index e1dd59e..fb19fca 100644 --- a/09-erreurs-et-debogage/README.md +++ b/09-erreurs-et-debogage/README.md @@ -30,8 +30,8 @@ Ce sont des erreurs dans la structure même du code. Python ne peut pas comprend if x > 5 print("x est grand") -# Message d'erreur : -# SyntaxError: invalid syntax +# Message d'erreur (Python 3.10+) : +# SyntaxError: expected ':' ``` **Caractéristiques :** @@ -359,6 +359,37 @@ def niveau_1(): niveau_1() # ← Le programme démarre ici ``` +### Les messages d'erreur modernes (Python 3.10+) + +Bonne nouvelle : depuis Python 3.10, les messages d'erreur sont devenus beaucoup plus précis et utiles. C'est une raison de plus de travailler sur une version récente (3.13+ recommandé). + +**Erreurs de syntaxe explicites** (3.10+) — Python indique ce qui manque : +```python +if x > 5 + print("x") +# Avant : SyntaxError: invalid syntax +# Maintenant : SyntaxError: expected ':' +``` + +**Suggestions en cas de faute de frappe** (3.10+) : +```python +variable_inexistant = 5 +print(variable_inexistante) +# NameError: name 'variable_inexistante' is not defined. Did you mean: 'variable_inexistant'? +``` + +**Localisation précise dans la trace** (3.11+, PEP 657) — des `^` soulignent l'expression exacte qui a échoué : +``` + File "script.py", line 2, in calculer + return x / y + ~~^~~ +ZeroDivisionError: division by zero +``` + +**Tracebacks en couleur** (3.13+) — dans le terminal, les traces sont désormais colorisées, ce qui les rend plus faciles à lire. + +> Ces améliorations font de la simple lecture du message d'erreur l'outil de débogage le plus rapide. Ne le survolez pas : il vous dit souvent exactement quoi corriger, et même où. + --- ## Les blocs try/except - Introduction @@ -369,7 +400,7 @@ niveau_1() # ← Le programme démarre ici try: # Code qui pourrait générer une erreur code_risque() -except TypeException: +except ExceptionSpecifique: # Code exécuté si une erreur de ce type se produit gerer_erreur() ``` @@ -420,6 +451,7 @@ else: ### Le bloc finally (toujours exécuté) ```python +fichier = None try: fichier = open("donnees.txt", 'r', encoding='utf-8') contenu = fichier.read() @@ -429,12 +461,12 @@ except FileNotFoundError: finally: # Ce bloc s'exécute TOUJOURS, qu'il y ait eu une erreur ou non print("🔒 Fermeture des ressources...") - try: + if fichier is not None: # fichier n'est défini que si open() a réussi fichier.close() - except: - pass # Le fichier n'était pas ouvert ``` +> En pratique, pour un fichier on préfère le gestionnaire de contexte `with open(...) as f:` (vu plus haut), qui ferme automatiquement le fichier — même en cas d'erreur — et rend ce `finally` inutile. Le `finally` reste précieux pour les ressources qui n'ont pas de gestionnaire de contexte. + ### Structure complète ```python @@ -587,6 +619,8 @@ except KeyError: valeur = dictionnaire.get(cle) ``` +> **Nuance** : Python recourt pourtant volontiers aux exceptions de façon idiomatique (style *EAFP*, « *easier to ask forgiveness than permission* ») quand le cas d'erreur est **rare et inattendu** (par exemple `try: int(x) except ValueError`). La règle est surtout d'éviter les exceptions pour des cas **fréquents et prévisibles** — comme une clé absente, mieux gérée par `.get()`. + 2. **Masquer les bugs** ```python # ❌ Mauvais : cache les vrais problèmes diff --git a/09-erreurs-et-debogage/exemples/01_01_hierarchie_exceptions.py b/09-erreurs-et-debogage/exemples/01_01_hierarchie_exceptions.py index 746168b..0a382a4 100644 --- a/09-erreurs-et-debogage/exemples/01_01_hierarchie_exceptions.py +++ b/09-erreurs-et-debogage/exemples/01_01_hierarchie_exceptions.py @@ -123,7 +123,7 @@ print("\n=== ImportError ===") try: - import module_inexistant + import module_inexistant # noqa: F401 - import volontaire : declenche ModuleNotFoundError except ModuleNotFoundError: print(" Ce module n'est pas installe !") diff --git a/09-erreurs-et-debogage/exemples/01_02_exception_groups.py b/09-erreurs-et-debogage/exemples/01_02_exception_groups.py new file mode 100644 index 0000000..60ef72f --- /dev/null +++ b/09-erreurs-et-debogage/exemples/01_02_exception_groups.py @@ -0,0 +1,44 @@ +# ============================================================================ +# Section 9.1 : Hierarchie des exceptions +# Description : Groupes d'exceptions - regrouper plusieurs erreurs dans un +# ExceptionGroup et les traiter (Python 3.11+) +# Fichier source : 01-hierarchie-des-exceptions.md +# ============================================================================ + +import sys + +def valider_formulaire(): + """Leve plusieurs erreurs en meme temps, regroupees dans un ExceptionGroup.""" + raise ExceptionGroup("Le formulaire contient des erreurs", [ # noqa: F821 (builtin Python 3.11+) + ValueError("L'age doit etre positif"), + TypeError("Le nom doit etre une chaine"), + ValueError("L'email est invalide"), + ]) + +# ExceptionGroup (Python 3.11+) regroupe plusieurs exceptions levees ensemble. +# La syntaxe moderne pour les traiter est 'except*' (voir le .md) : +# +# try: +# valider_formulaire() +# except* ValueError as eg: +# ... +# except* TypeError as eg: +# ... +# +# Mais 'except*' est une SYNTAXE de Python 3.11+ : un fichier qui l'utilise ne +# se *parse* meme pas sur Python 3.10. Pour rester executable partout, on montre +# ici l'equivalent par inspection manuelle de l'attribut .exceptions. + +if sys.version_info >= (3, 11): + print("=== Groupes d'exceptions (ExceptionGroup, Python 3.11+) ===\n") + try: + valider_formulaire() + except ExceptionGroup as groupe: # noqa: F821 (builtin Python 3.11+) + print(f" Groupe : {groupe.message}") + valeurs = [str(e) for e in groupe.exceptions if isinstance(e, ValueError)] + types_ = [str(e) for e in groupe.exceptions if isinstance(e, TypeError)] + print(f" {len(valeurs)} ValueError : {valeurs}") + print(f" {len(types_)} TypeError : {types_}") +else: + print("Les groupes d'exceptions (ExceptionGroup / except*) necessitent Python 3.11+") + print(f"Version actuelle : {sys.version_info.major}.{sys.version_info.minor} -> exemple ignore") diff --git a/09-erreurs-et-debogage/exemples/02_03_chainage_exceptions.py b/09-erreurs-et-debogage/exemples/02_03_chainage_exceptions.py new file mode 100644 index 0000000..632a699 --- /dev/null +++ b/09-erreurs-et-debogage/exemples/02_03_chainage_exceptions.py @@ -0,0 +1,49 @@ +# ============================================================================ +# Section 9.2 : Creation d'exceptions personnalisees +# Description : Chainer les exceptions avec 'raise ... from ...' (preserver la +# cause d'origine) et add_note() pour ajouter du contexte (3.11+) +# Fichier source : 02-exceptions-personnalisees.md +# ============================================================================ + +import sys + +class ConfigError(Exception): + """Erreur de configuration de l'application.""" + pass + +# ========================================== +# 1. Chainer une exception (raise ... from ...) +# ========================================== +print("=== Chainage : raise ... from ... ===\n") + +def charger_config(chemin): + try: + with open(chemin, encoding="utf-8") as f: + return int(f.read()) + except FileNotFoundError as e: + # Traduit l'erreur technique en erreur metier, en gardant la cause + raise ConfigError(f"Configuration introuvable : {chemin}") from e + +try: + charger_config("absent.txt") +except ConfigError as e: + print(f" Erreur metier : {e}") + print(f" Cause d'origine (__cause__) : {type(e.__cause__).__name__}: {e.__cause__}") + +# ========================================== +# 2. add_note() - ajouter du contexte (Python 3.11+) +# ========================================== +print("\n=== add_note() (Python 3.11+) ===\n") + +if sys.version_info >= (3, 11): + try: + erreur = ValueError("donnee invalide") + erreur.add_note("Verifier le format du fichier d'entree") + erreur.add_note("Ligne 42 du CSV") + raise erreur + except ValueError as exc: + print(f" Message : {exc}") + print(f" Notes : {exc.__notes__}") +else: + print(" add_note() necessite Python 3.11+") + print(f" Version actuelle : {sys.version_info.major}.{sys.version_info.minor}") diff --git a/09-erreurs-et-debogage/exemples/02_04_hierarchie_personnalisee.py b/09-erreurs-et-debogage/exemples/02_04_hierarchie_personnalisee.py new file mode 100644 index 0000000..9bb70ba --- /dev/null +++ b/09-erreurs-et-debogage/exemples/02_04_hierarchie_personnalisee.py @@ -0,0 +1,75 @@ +# ============================================================================ +# Section 9.2 : Creation d'exceptions personnalisees +# Description : Hierarchie d'exceptions personnalisees a plusieurs niveaux +# (base -> categories -> specifiques) et capture a differents +# niveaux de la hierarchie +# Fichier source : 02-exceptions-personnalisees.md +# ============================================================================ + +# Exception de base de l'application +class ErreurApplication(Exception): + """Classe de base pour toutes les exceptions de l'application.""" + pass + +# Categories +class ErreurBaseDeDonnees(ErreurApplication): + """Erreurs liees a la base de donnees.""" + pass + +class ErreurReseau(ErreurApplication): + """Erreurs liees au reseau.""" + pass + +class ErreurValidation(ErreurApplication): + """Erreurs de validation des donnees.""" + pass + +# Exceptions specifiques +class ConnexionBaseDeDonneesError(ErreurBaseDeDonnees): + """Impossible de se connecter a la base de donnees.""" + pass + +class RequeteEchoueeError(ErreurBaseDeDonnees): + """La requete SQL a echoue.""" + pass + +class TimeoutReseauError(ErreurReseau): + """Le reseau a mis trop de temps a repondre.""" + pass + +# ========================================== +# Capturer a differents niveaux de la hierarchie +# ========================================== +print("=== Hierarchie d'exceptions personnalisees ===\n") + +def operation(scenario): + if scenario == "db_connexion": + raise ConnexionBaseDeDonneesError("Connexion refusee par le serveur") + elif scenario == "db_requete": + raise RequeteEchoueeError("Syntaxe SQL invalide") + elif scenario == "reseau": + raise TimeoutReseauError("Delai depasse apres 30s") + +for scenario in ["db_connexion", "db_requete", "reseau"]: + try: + operation(scenario) + except ConnexionBaseDeDonneesError as e: + # Niveau le plus specifique + print(f" [specifique] Connexion BdD : {e}") + except ErreurBaseDeDonnees as e: + # Niveau categorie : capture les autres erreurs de base de donnees + print(f" [categorie] Erreur BdD : {e}") + except ErreurApplication as e: + # Niveau global : capture toute erreur de l'application + print(f" [global] {type(e).__name__} : {e}") + +# ========================================== +# A retenir : toujours heriter d'Exception, jamais de BaseException +# ========================================== +print("\n=== Verification de la hierarchie ===\n") +print(f" ConnexionBaseDeDonneesError sous ErreurBaseDeDonnees : " + f"{issubclass(ConnexionBaseDeDonneesError, ErreurBaseDeDonnees)}") +print(f" ErreurBaseDeDonnees sous ErreurApplication : " + f"{issubclass(ErreurBaseDeDonnees, ErreurApplication)}") +print(f" ErreurApplication sous Exception (et non BaseException) : " + f"{issubclass(ErreurApplication, Exception)}") diff --git a/09-erreurs-et-debogage/exemples/03_01_print_assert.py b/09-erreurs-et-debogage/exemples/03_01_print_assert.py index 2b5f246..f24dbde 100644 --- a/09-erreurs-et-debogage/exemples/03_01_print_assert.py +++ b/09-erreurs-et-debogage/exemples/03_01_print_assert.py @@ -35,6 +35,13 @@ def calculer_moyenne(notes): ville = "Paris" print(f" Nom: {nom}, Age: {age}, Ville: {ville}") +# f-string auto-documentee (Python 3.8+) : affiche le nom ET la valeur +print("\n--- f-string auto-documentee {x=} ---") +x = 42 +print(f" {x=}") # x=42 +print(f" {nom=}") # nom='Alice' +print(f" {age * 2=}") # age * 2=50 (marche aussi avec une expression) + # Separateurs visuels print("\n--- Separateurs visuels ---") print("=" * 50) diff --git a/09-erreurs-et-debogage/exemples/03_02_logging.py b/09-erreurs-et-debogage/exemples/03_02_logging.py index 44c7605..0254d5d 100644 --- a/09-erreurs-et-debogage/exemples/03_02_logging.py +++ b/09-erreurs-et-debogage/exemples/03_02_logging.py @@ -46,7 +46,22 @@ def diviser(a, b): print(f"diviser(10, 0) = {diviser(10, 0)}") # ========================================== -# 3. Enregistrer les logs dans un fichier +# 3. Logger une exception avec sa trace (logging.exception) +# ========================================== +print("\n=== Logging - logging.exception() ===\n") + +def traiter(donnees): + try: + return sum(donnees) / len(donnees) + except ZeroDivisionError: + # A appeler dans un except : ajoute automatiquement le traceback complet + logging.exception("Echec du traitement (liste vide ?)") + return None + +print(f"traiter([]) = {traiter([])}") + +# ========================================== +# 4. Enregistrer les logs dans un fichier # ========================================== print("\n=== Logging - Ecriture dans un fichier ===\n") diff --git a/09-erreurs-et-debogage/exemples/03_03_traceback_pprint_inspect.py b/09-erreurs-et-debogage/exemples/03_03_traceback_pprint_inspect.py index adbbda4..239a97a 100644 --- a/09-erreurs-et-debogage/exemples/03_03_traceback_pprint_inspect.py +++ b/09-erreurs-et-debogage/exemples/03_03_traceback_pprint_inspect.py @@ -21,11 +21,11 @@ def fonction_b(): fonction_c() def fonction_c(): - x = 1 / 0 # Erreur + return 1 / 0 # Erreur (ZeroDivisionError) try: fonction_a() -except Exception as e: +except Exception: print("Une erreur s'est produite !") traceback.print_exc() diff --git a/09-erreurs-et-debogage/exemples/03_04_erreurs_courantes.py b/09-erreurs-et-debogage/exemples/03_04_erreurs_courantes.py index d7c3fb1..72b5418 100644 --- a/09-erreurs-et-debogage/exemples/03_04_erreurs_courantes.py +++ b/09-erreurs-et-debogage/exemples/03_04_erreurs_courantes.py @@ -60,12 +60,10 @@ print("\n=== NameError ===\n") def afficher_nom(): - if 'prenom' in dir(): - print(prenom) - else: + try: + print(prenom) # noqa: F821 - demo : variable volontairement non definie + except NameError: print(" La variable 'prenom' n'existe pas") - variables = [v for v in dir() if not v.startswith('_')] - print(f" Variables disponibles : {variables}") afficher_nom() diff --git a/09-erreurs-et-debogage/exemples/03_05_warnings.py b/09-erreurs-et-debogage/exemples/03_05_warnings.py new file mode 100644 index 0000000..60cfde9 --- /dev/null +++ b/09-erreurs-et-debogage/exemples/03_05_warnings.py @@ -0,0 +1,49 @@ +# ============================================================================ +# Section 9.3 : Techniques de debogage +# Description : Module warnings - signaler sans planter (warnings.warn), +# deprecier une fonction, transformer les warnings en erreurs +# Fichier source : 03-techniques-de-debogage.md +# ============================================================================ + +import warnings + +# ========================================== +# 1. Emettre un avertissement (UserWarning) +# ========================================== +print("=== warnings.warn (UserWarning) ===\n") + +def diviser(a, b): + if b == 0: + warnings.warn("Division par zero evitee, retour de None", UserWarning) + return None + return a / b + +print(f" diviser(10, 0) = {diviser(10, 0)}") + +# ========================================== +# 2. Deprecier une fonction (DeprecationWarning) +# ========================================== +print("\n=== Deprecation (DeprecationWarning) ===\n") + +def ancienne_api(): + warnings.warn( + "ancienne_api() est obsolete, utilisez nouvelle_api()", + DeprecationWarning, + stacklevel=2, # pointe vers l'appelant, pas vers cette ligne + ) + return 42 + +# DeprecationWarning est masque par defaut ; on le rend visible pour la demo : +warnings.simplefilter("always", DeprecationWarning) +print(f" ancienne_api() = {ancienne_api()}") + +# ========================================== +# 3. Transformer les avertissements en erreurs (debogage / tests) +# ========================================== +print("\n=== simplefilter('error') : un warning devient une exception ===\n") + +warnings.simplefilter("error") +try: + warnings.warn("ceci devient une exception") +except UserWarning as e: + print(f" Capture comme exception : {e}") diff --git a/09-erreurs-et-debogage/exemples/03_06_pdb.py b/09-erreurs-et-debogage/exemples/03_06_pdb.py new file mode 100644 index 0000000..2c50ccd --- /dev/null +++ b/09-erreurs-et-debogage/exemples/03_06_pdb.py @@ -0,0 +1,47 @@ +# ============================================================================ +# Section 9.3 : Techniques de debogage +# Description : Le debogueur pdb - points d'arret avec breakpoint(), +# inspection de variables pas a pas +# Fichier source : 03-techniques-de-debogage.md +# ============================================================================ + +import os + +# Ce fichier contient de VRAIS appels breakpoint() (points d'arret pdb). +# Par defaut, on les NEUTRALISE pour qu'il s'execute d'un seul trait : +os.environ.setdefault("PYTHONBREAKPOINT", "0") +# +# Pour deboguer reellement pas a pas, lancez : +# PYTHONBREAKPOINT=1 python 03_06_pdb.py +# puis utilisez les commandes pdb a l'invite (Pdb) : +# n (next) s (step) c (continue) p variable (print) +# l (list) w (where) q (quit) + +# ========================================== +# 1. Point d'arret avec breakpoint() +# ========================================== +print("=== pdb : breakpoint() ===\n") + +def calculer_prix_total(prix_unitaire, quantite, taux_tva): + breakpoint() # neutralise par defaut ; ouvre pdb si PYTHONBREAKPOINT=1 + prix_ht = prix_unitaire * quantite + montant_tva = prix_ht * taux_tva + prix_ttc = prix_ht + montant_tva + return prix_ttc + +resultat = calculer_prix_total(100, 3, 0.20) +print(f" Prix total : {resultat}EUR") + +# ========================================== +# 2. Inspecter une boucle pas a pas +# ========================================== +print("\n=== pdb : inspecter une boucle ===\n") + +def calculer_factorielle(n): + resultat = 1 + breakpoint() # ex. de commandes : p resultat, n (next), c (continue) + for i in range(1, n + 1): + resultat *= i + return resultat + +print(f" factorielle(5) = {calculer_factorielle(5)}") diff --git a/09-erreurs-et-debogage/exemples/04_01_mesurer_temps.py b/09-erreurs-et-debogage/exemples/04_01_mesurer_temps.py index 7c91d51..c9d0546 100644 --- a/09-erreurs-et-debogage/exemples/04_01_mesurer_temps.py +++ b/09-erreurs-et-debogage/exemples/04_01_mesurer_temps.py @@ -1,6 +1,6 @@ # ============================================================================ # Section 9.4 : Profiling et optimisation -# Description : Mesurer le temps d'execution avec time.time(), fonction +# Description : Mesurer le temps d'execution avec time.perf_counter(), fonction # de chronometrage, gestionnaire de contexte # Fichier source : 04-profiling-et-optimisation.md # ============================================================================ @@ -9,15 +9,15 @@ from contextlib import contextmanager # ========================================== -# 1. time.time() basique +# 1. time.perf_counter() basique # ========================================== -print("=== time.time() ===\n") +print("=== time.perf_counter() ===\n") -debut = time.time() +debut = time.perf_counter() total = 0 for i in range(1000000): total += i -fin = time.time() +fin = time.perf_counter() duree = fin - debut print(f" Temps d'execution : {duree:.4f} secondes") @@ -28,9 +28,9 @@ def chronometrer(fonction, *args, **kwargs): """Chronometre l'execution d'une fonction.""" - debut = time.time() + debut = time.perf_counter() resultat = fonction(*args, **kwargs) - fin = time.time() + fin = time.perf_counter() duree = fin - debut return resultat, duree @@ -50,9 +50,9 @@ def calculer_somme(n): def chronometre(nom="Code"): """Gestionnaire de contexte pour chronometrer un bloc de code.""" print(f" Debut du chronometrage : {nom}") - debut = time.time() + debut = time.perf_counter() yield - fin = time.time() + fin = time.perf_counter() duree = fin - debut print(f" {nom} termine en {duree:.4f} secondes") diff --git a/09-erreurs-et-debogage/exemples/04_04_memoire.py b/09-erreurs-et-debogage/exemples/04_04_memoire.py index 182d1aa..2d413f1 100644 --- a/09-erreurs-et-debogage/exemples/04_04_memoire.py +++ b/09-erreurs-et-debogage/exemples/04_04_memoire.py @@ -7,6 +7,7 @@ # ============================================================================ import sys +import tracemalloc # ========================================== # 1. Mesurer la taille d'objets en memoire @@ -81,3 +82,46 @@ def avec_generateur(n): print(f" Liste : {taille_liste:,} octets") print(f" Generateur : {taille_gen:,} octets") print(f" Le generateur utilise {taille_liste/taille_gen:.0f}x moins de memoire !") + +# ========================================== +# 4. tracemalloc - profiler les allocations (bibliotheque standard) +# ========================================== +print("\n=== tracemalloc (allocations memoire) ===\n") + +tracemalloc.start() # Demarrer le suivi des allocations + +donnees = [i ** 2 for i in range(100000)] +mapping = {i: str(i) for i in range(100000)} + +# Photographier l'etat de la memoire et trier par ligne de code +snapshot = tracemalloc.take_snapshot() +top = snapshot.statistics('lineno') +print("Top 2 des allocations :") +for stat in top[:2]: + print(f" {stat}") + +actuel, pic = tracemalloc.get_traced_memory() +print(f"Memoire actuelle : {actuel / 1024:.1f} Ko ; pic : {pic / 1024:.1f} Ko") +tracemalloc.stop() + +# ========================================== +# 5. memory_profiler - profiling ligne par ligne (outil tiers) +# ========================================== +print("\n=== memory_profiler (optionnel, tiers) ===\n") + +# memory_profiler (pip install memory_profiler) mesure la memoire LIGNE PAR +# LIGNE. C'est un outil tiers ; la bibliotheque standard offre tracemalloc +# (section 4 ci-dessus). On le charge donc de maniere optionnelle. +try: + from memory_profiler import profile + + @profile + def fonction_gourmande(): + liste1 = [i for i in range(1000000)] + liste2 = [i ** 2 for i in range(1000000)] + return sum(liste1) + sum(liste2) + + fonction_gourmande() # @profile imprime un rapport memoire ligne par ligne +except ImportError: + print(" memory_profiler n'est pas installe (pip install memory_profiler).") + print(" La bibliotheque standard fournit tracemalloc, montre ci-dessus.") diff --git a/09-erreurs-et-debogage/exemples/04_05_optimisation_techniques.py b/09-erreurs-et-debogage/exemples/04_05_optimisation_techniques.py index 28b657d..c8eca0e 100644 --- a/09-erreurs-et-debogage/exemples/04_05_optimisation_techniques.py +++ b/09-erreurs-et-debogage/exemples/04_05_optimisation_techniques.py @@ -136,6 +136,17 @@ def fibonacci_avec_cache(n): assert fibonacci_sans_cache(30) == fibonacci_avec_cache(30) == 832040 print(f" Fibonacci(30) = {fibonacci_avec_cache(30)}") +# Depuis Python 3.9, @functools.cache est un raccourci pour @lru_cache(maxsize=None) +from functools import cache + +@cache +def fibonacci_cache_court(n): + if n <= 1: + return n + return fibonacci_cache_court(n-1) + fibonacci_cache_court(n-2) + +print(f" @cache (3.9+, equivalent) : Fibonacci(30) = {fibonacci_cache_court(30)}") + # ========================================== # 6. join() vs + pour concatenation # ========================================== diff --git a/09-erreurs-et-debogage/exemples/04_06_optimisation_numpy.py b/09-erreurs-et-debogage/exemples/04_06_optimisation_numpy.py index dc2e5cd..312a1fe 100644 --- a/09-erreurs-et-debogage/exemples/04_06_optimisation_numpy.py +++ b/09-erreurs-et-debogage/exemples/04_06_optimisation_numpy.py @@ -64,6 +64,6 @@ def calcul_vectorise(n): # Verification que les resultats sont identiques res_boucle = calcul_avec_boucle(10) res_numpy = calcul_vectorise(10) -print(f"\n Verification (10 premiers):") +print("\n Verification (10 premiers):") print(f" Boucle : {[round(x, 2) for x in res_boucle[:5]]}") print(f" NumPy : {[round(x, 2) for x in res_numpy[:5].tolist()]}") diff --git a/09-erreurs-et-debogage/exemples/README.md b/09-erreurs-et-debogage/exemples/README.md index 05bbd70..d64c65c 100644 --- a/09-erreurs-et-debogage/exemples/README.md +++ b/09-erreurs-et-debogage/exemples/README.md @@ -1,5 +1,15 @@ # Chapitre 09 - Erreurs et Debogage : Exemples +Ce dossier contient les exemples exécutables du chapitre 9, un fichier `.py` par thème, numérotés selon la section du cours (`01_*` → 9.1, `02_*` → 9.2, `03_*` → 9.3, `04_*` → 9.4). + +**Exécution** : chaque fichier est autonome. + +```bash +python3 01_01_hierarchie_exceptions.py +``` + +> Les exemples de profiling affichent des **durées et tailles variables** selon la machine ; seules les valeurs déterministes (résultats calculés) sont indiquées comme « Sortie attendue ». + ## Section 9.1 : Hierarchie des exceptions ### 01_01_hierarchie_exceptions.py @@ -12,6 +22,14 @@ - Mauvais ordre : LookupError capture avant IndexError - Bon ordre : IndexError (specifique) capture en premier +### 01_02_exception_groups.py +- **Section** : 9.1 - Hierarchie des exceptions +- **Description** : Groupes d'exceptions (ExceptionGroup) pour regrouper plusieurs erreurs levees ensemble ; la syntaxe `except*` est citee en commentaire (Python 3.11+) +- **Fichier source** : `01-hierarchie-des-exceptions.md` +- **Sortie attendue** : + - Sur Python 3.11+ : groupe de 2 ValueError + 1 TypeError, inspectees via `.exceptions` + - Sur Python < 3.11 : message indiquant que ExceptionGroup necessite 3.11+ + ## Section 9.2 : Creation d'exceptions personnalisees ### 02_01_exceptions_personnalisees.py @@ -37,11 +55,27 @@ - Test 4 : livre inexistant (ValueError) - Test 5 : retard de 5 jours, amende 2.5EUR +### 02_03_chainage_exceptions.py +- **Section** : 9.2 - Creation d'exceptions personnalisees +- **Description** : Chainage d'exceptions (`raise ... from ...`, cause preservee dans `__cause__`) et `add_note()` pour ajouter du contexte (Python 3.11+) +- **Fichier source** : `02-exceptions-personnalisees.md` +- **Sortie attendue** : + - ConfigError leve depuis un FileNotFoundError, avec la cause d'origine affichee + - add_note (3.11+) : notes attachees a l'exception ; sinon message de repli + +### 02_04_hierarchie_personnalisee.py +- **Section** : 9.2 - Creation d'exceptions personnalisees +- **Description** : Hierarchie d'exceptions a plusieurs niveaux (ErreurApplication -> ErreurBaseDeDonnees/ErreurReseau/ErreurValidation -> exceptions specifiques) et capture a differents niveaux de la hierarchie +- **Fichier source** : `02-exceptions-personnalisees.md` +- **Sortie attendue** : + - 3 scenarios captures au bon niveau : specifique, categorie, global + - Verifications de sous-classes : toutes True (chaine d'heritage) + ## Section 9.3 : Techniques de debogage ### 03_01_print_assert.py - **Section** : 9.3 - Techniques de debogage -- **Description** : Debogage avec print() (moyenne, types, separateurs), assertions (racine carree, types, liste vide) +- **Description** : Debogage avec print() (moyenne, types, f-string auto-documentee {x=}, separateurs), assertions (racine carree, types, liste vide) - **Fichier source** : `03-techniques-de-debogage.md` - **Sortie attendue** : - Moyenne de [15, 18, 12, 16] = 15.25 @@ -52,7 +86,7 @@ ### 03_02_logging.py - **Section** : 9.3 - Techniques de debogage -- **Description** : Module logging - 5 niveaux (DEBUG, INFO, WARNING, ERROR, CRITICAL), fonction avec logging, ecriture dans un fichier log +- **Description** : Module logging - 5 niveaux (DEBUG, INFO, WARNING, ERROR, CRITICAL), fonction avec logging, logging.exception (trace complete), ecriture dans un fichier log - **Fichier source** : `03-techniques-de-debogage.md` - **Sortie attendue** : - 5 messages de niveaux differents sur stderr @@ -79,11 +113,28 @@ - KeyError : cle 'ville' n'existe pas, get() retourne "Non specifie" - moyenne([10,20,30]) = 20.0, moyenne_v2([]) = None +### 03_05_warnings.py +- **Section** : 9.3 - Techniques de debogage +- **Description** : Module warnings - `warnings.warn` (UserWarning), depreciation (DeprecationWarning, `stacklevel`), `simplefilter('error')` pour transformer un avertissement en exception +- **Fichier source** : `03-techniques-de-debogage.md` +- **Sortie attendue** : + - UserWarning emis (diviser par zero -> None) + - DeprecationWarning rendu visible (ancienne_api -> 42) + - simplefilter('error') : le warning suivant est capture comme exception + +### 03_06_pdb.py +- **Section** : 9.3 - Techniques de debogage +- **Description** : Debogueur pdb - points d'arret avec breakpoint() (calculer_prix_total, calculer_factorielle). Les breakpoint() sont neutralises par defaut via PYTHONBREAKPOINT=0 pour une execution non interactive +- **Fichier source** : `03-techniques-de-debogage.md` +- **Sortie attendue** : + - Par defaut (breakpoint neutralise) : Prix total = 360.0EUR, factorielle(5) = 120 + - Avec `PYTHONBREAKPOINT=1 python 03_06_pdb.py` : ouvre pdb a chaque breakpoint() + ## Section 9.4 : Profiling et optimisation ### 04_01_mesurer_temps.py - **Section** : 9.4 - Profiling et optimisation -- **Description** : Mesurer le temps d'execution avec time.time(), fonction de chronometrage, gestionnaire de contexte +- **Description** : Mesurer le temps d'execution avec time.perf_counter() (horloge monotone), fonction de chronometrage, gestionnaire de contexte - **Fichier source** : `04-profiling-et-optimisation.md` - **Sortie attendue** : - Temps d'execution de la boucle ~0.05-0.15s @@ -109,16 +160,16 @@ ### 04_04_memoire.py - **Section** : 9.4 - Profiling et optimisation -- **Description** : Profiling memoire - sys.getsizeof, comparaison structures (liste, tuple, set, generateur) +- **Description** : Profiling memoire - sys.getsizeof, comparaison structures (liste, tuple, set, generateur), tracemalloc (allocations), memory_profiler (tiers, optionnel) - **Fichier source** : `04-profiling-et-optimisation.md` - **Sortie attendue** : - - Petite liste : 104 octets + - Petite liste : 104.00 octets - Grande liste : ~7-8 Mo - Generateur : ~200 octets (42000x moins que la liste) ### 04_05_optimisation_techniques.py - **Section** : 9.4 - Profiling et optimisation -- **Description** : Techniques d'optimisation - set vs liste, calculs repetitifs, comprehensions, fonctions built-in, lru_cache (Fibonacci), join() +- **Description** : Techniques d'optimisation - set vs liste, calculs repetitifs, comprehensions, fonctions built-in, lru_cache et functools.cache (Fibonacci), join() - **Fichier source** : `04-profiling-et-optimisation.md` - **Sortie attendue** : - sum() ~4-5x plus rapide que boucle manuelle @@ -151,3 +202,12 @@ - v1 -> v2 : ~1.3x plus rapide - v1 -> v3 : ~3x plus rapide - Moyenne = 4999.50, Variance = 8333333.25 (identiques pour les 3 versions) + +## Notes + +- **Style des fichiers** : les accents français sont conservés (é, è, à…), mais les émojis et le symbole € sont rendus en ASCII (`EUR`…). Les démonstrations d'erreurs (assertions, exceptions) sont **encadrées par `try/except`** pour que les fichiers s'exécutent jusqu'au bout. +- **Mesure du temps** : on utilise `time.perf_counter()` (horloge monotone, adaptée aux durées), comme dans le cours, et non `time.time()`. +- **Dépendances tierces** : `04_06`, `04_07`, `04_08` nécessitent **NumPy** (`pip install numpy`). `04_04` utilise **memory_profiler** s'il est installé, sinon il l'indique et renvoie vers `tracemalloc` (stdlib). Tous les autres exemples n'utilisent que la bibliothèque standard. +- **Débogueur pdb** : `03_06` contient de vrais `breakpoint()`, **neutralisés par défaut** (`PYTHONBREAKPOINT=0`) pour que le fichier s'exécute d'un trait. Relancez-le avec `PYTHONBREAKPOINT=1 python 03_06_pdb.py` pour ouvrir le débogueur et explorer pas à pas. +- **Fonctionnalités Python 3.11+** : `01_02` (ExceptionGroup) et la 2ᵉ partie de `02_03` (`add_note`) sont protégées par un test `sys.version_info` et affichent un message de repli avant 3.11. La syntaxe `except*` (montrée dans le `.md`) est du **3.11+ au niveau de la *syntaxe*** : impossible à inclure dans un fichier exécutable sur 3.10 — `01_02` en montre donc l'équivalent par inspection de l'attribut `.exceptions`. +- **Compatibilité** : hors NumPy, les exemples s'exécutent sur **Python 3.10 à 3.14** (vérifié ; les blocs 3.11+ affichent un repli sur 3.10). diff --git a/10-tests-et-qualite/01-tests-unitaires-unittest-pytest.md b/10-tests-et-qualite/01-tests-unitaires-unittest-pytest.md index b8839a4..b568f26 100644 --- a/10-tests-et-qualite/01-tests-unitaires-unittest-pytest.md +++ b/10-tests-et-qualite/01-tests-unitaires-unittest-pytest.md @@ -395,7 +395,8 @@ def test_assertions_basiques(): # None assert None is None - assert "something" is not None + valeur = "quelque chose" + assert valeur is not None def test_approximation(): """Teste l'égalité approximative.""" @@ -403,6 +404,51 @@ def test_approximation(): assert 3.141592 == pytest.approx(3.14, abs=0.01) ``` +> **Pourquoi un simple `assert` suffit-il ?** En Python pur, un `assert a == b` qui échoue lève un `AssertionError` *sans aucun détail* sur les valeurs comparées. Si pytest parvient malgré tout à les afficher, c'est grâce à la **réécriture des assertions** (*assertion rewriting*) : au moment où il importe un fichier de test, pytest en réécrit le bytecode pour instrumenter chaque `assert` et mémoriser la valeur de ses sous-expressions. En cas d'échec, le rapport devient explicite : +> +> ``` +> > assert additionner(2, 2) == 5 +> E assert 4 == 5 +> E + where 4 = additionner(2, 2) +> ``` +> +> C'est pour cette raison que vous n'avez pas besoin des méthodes `assertEqual`, `assertIn`… de unittest : le `assert` natif, une fois réécrit, fournit déjà un diagnostic détaillé. (La réécriture ne s'applique qu'aux fichiers de test découverts par pytest ; pour instrumenter du code d'assertion situé dans un module utilitaire importé, on l'enregistre explicitement avec `pytest.register_assert_rewrite`.) + +### Tester les exceptions (et leur message) + +On a déjà vu `pytest.raises(...)` pour vérifier qu'une exception est bien levée. Deux compléments très courants méritent d'être connus. + +**Vérifier le message avec `match`** — le paramètre `match` contrôle que le message de l'exception correspond à une **expression régulière**, recherchée avec `re.search` (une simple sous-chaîne suffit donc). Le test échoue si l'exception est bien levée mais que son message ne correspond pas : + +```python +import pytest + +def retirer(solde, montant): + if montant > solde: + raise ValueError("Solde insuffisant pour ce retrait") + return solde - montant + +def test_retrait_trop_grand(): + # match : sous-chaîne (ou motif regex) attendue dans le message d'erreur + with pytest.raises(ValueError, match="insuffisant"): + retirer(100, 500) +``` + +C'est ce paramètre `match` que l'on retrouvera dans la suite du cours (par exemple `match="Email invalide"` ou `match="prix"`) : il rend le test plus précis en s'assurant que c'est *bien la bonne* erreur qui est levée, et pas une autre `ValueError`. + +**Inspecter l'exception avec `as excinfo`** — pour aller plus loin (type exact, message complet, attributs), capturez l'objet `ExceptionInfo` renvoyé par `pytest.raises` : + +```python +def test_details_exception(): + with pytest.raises(ValueError) as excinfo: + retirer(100, 500) + + assert excinfo.type is ValueError # le type effectivement levé + assert "insuffisant" in str(excinfo.value) # excinfo.value = l'exception elle-même +``` + +> ⚠️ `match` est une **expression régulière**, pas une égalité de chaîne : des caractères comme `(`, `)`, `[`, `.`, `+`, `$` y ont une signification spéciale. Pour rechercher un message qui contient littéralement ces caractères, échappez-les (`\\(`) ou passez par `re.escape(...)`. + ### Fixtures : Préparer des données réutilisables Les **fixtures** sont la façon dont pytest gère la préparation des données de test. Elles sont plus flexibles que `setUp()` et `tearDown()` : @@ -432,6 +478,8 @@ def test_avec_utilisateur(utilisateur_test): assert utilisateur_test.actif is True ``` +> **Comment la fixture arrive-t-elle dans le test ?** pytest fait correspondre le **nom du paramètre** de la fonction de test au **nom d'une fixture**. En voyant `def test_avec_liste(liste_nombres)`, il cherche une fixture appelée `liste_nombres`, l'exécute, et **injecte sa valeur de retour** dans le paramètre. C'est de l'*injection de dépendances* : le test déclare ce dont il a besoin (par le nom), pytest se charge de le construire et de le fournir. C'est aussi ce qui les rend plus flexibles que `setUp`/`tearDown` : un test ne reçoit **que** les fixtures qu'il nomme (pas un état partagé monolithique), et les fixtures peuvent elles-mêmes en demander d'autres. Un paramètre sans fixture correspondante déclenche une erreur `fixture '...' not found`. + ### Fixtures avec setup et teardown Vous pouvez créer des fixtures qui font du nettoyage après utilisation : @@ -462,6 +510,15 @@ def test_lecture_fichier(fichier_temporaire): assert contenu == "contenu de test" ``` +> **Astuce (fixture intégrée)** : pour les fichiers et dossiers temporaires, pytest fournit déjà la fixture **`tmp_path`** — un `pathlib.Path` vers un dossier temporaire **unique par test**, supprimé automatiquement. Plus besoin de créer le fichier dans le répertoire courant ni de nettoyer à la main : + +```python +def test_lecture_fichier(tmp_path): + fichier = tmp_path / "test.txt" + fichier.write_text("contenu de test", encoding="utf-8") + assert fichier.read_text(encoding="utf-8") == "contenu de test" +``` + ### Paramétrer vos tests pytest permet de tester facilement plusieurs cas avec le même code : @@ -484,6 +541,33 @@ def test_additionner_plusieurs_cas(a, b, attendu): Ce test sera exécuté 5 fois avec des valeurs différentes ! +### Ignorer ou marquer des tests : skip, skipif, xfail + +En plus du paramétrage, pytest fournit des **marqueurs** pour contrôler l'exécution d'un test selon le contexte : + +```python +import sys +import pytest + +@pytest.mark.skip(reason="fonctionnalité pas encore implémentée") +def test_a_venir(): + ... + +@pytest.mark.skipif(sys.version_info < (3, 10), reason="nécessite Python 3.10+") +def test_syntaxe_recente(): + ... + +@pytest.mark.xfail(reason="bug connu, correctif en cours") +def test_comportement_bugue(): + assert fonction_buguee() == 42 +``` + +- **`skip`** : ignore toujours le test (marqué `s` dans le rapport). +- **`skipif(condition, reason=...)`** : ignore le test si la condition est vraie — pratique pour les différences de version de Python ou de système d'exploitation. +- **`xfail`** : on *s'attend* à ce que le test échoue (marqué `x`). S'il échoue, c'est un `xfail` attendu (pas une erreur) ; s'il réussit quand même, c'est un `xpass` — le signal que le bug est peut-être corrigé et que le marqueur peut être retiré. + +On peut aussi ignorer un test *pendant* son exécution avec `pytest.skip("raison")`, par exemple après avoir détecté qu'une dépendance optionnelle est absente. + ### Exemple complet avec pytest Voici notre classe Utilisateur testée avec pytest : diff --git a/10-tests-et-qualite/02-mocking-et-fixtures.md b/10-tests-et-qualite/02-mocking-et-fixtures.md index 8496138..afd3da7 100644 --- a/10-tests-et-qualite/02-mocking-et-fixtures.md +++ b/10-tests-et-qualite/02-mocking-et-fixtures.md @@ -237,6 +237,30 @@ def test_utilisateur_existe(utilisateur_dans_db, base_de_donnees): assert base_de_donnees["users"][0]["nom"] == "Alice" ``` +### Partager des fixtures entre fichiers : conftest.py + +Une fixture définie dans un fichier de test n'est visible que dans ce fichier. Pour **partager des fixtures entre plusieurs fichiers**, placez-les dans un fichier spécial nommé **`conftest.py`** : pytest le découvre automatiquement et rend ses fixtures disponibles à tous les tests du même dossier (et de ses sous-dossiers), **sans aucun import**. + +```python +# fichier: conftest.py +import pytest +from compte import Compte + +@pytest.fixture +def compte(): + """Fixture partagée par tous les tests du dossier.""" + return Compte("Alice", solde=1000) +``` + +```python +# fichier: test_depot.py - la fixture 'compte' est disponible sans l'importer +def test_deposer(compte): + compte.deposer(500) + assert compte.solde == 1500 +``` + +`conftest.py` est l'emplacement standard de la configuration des tests : on y place aussi les fixtures de portée `session`, les fixtures partagées par tout le projet et les hooks pytest. + ### Fixtures avec unittest Avec unittest, on utilise les méthodes `setUp()` et `tearDown()` : @@ -900,12 +924,38 @@ print(resultats) # [{'id': 1, 'nom': 'Alice'}] print(len(db)) # 10 ``` +> **Pourquoi deux classes (`Mock` et `MagicMock`) ?** Python cherche les méthodes spéciales (`__len__`, `__iter__`, `__enter__`…) sur le **type** de l'objet, jamais sur l'instance. Or un `Mock` crée ses attributs à la volée *sur l'instance* : `len(mock)` lèverait donc `TypeError`, car `__len__` n'est pas trouvé sur la classe. `MagicMock` règle le problème en **pré-configurant ces dunders** sur sa classe (avec des valeurs par défaut sensées : `__len__` → `0`, `__bool__` → `True`, etc.). Règle pratique : prenez `MagicMock` dès que le code testé fait `len(...)`, `for ... in ...`, `with ...`, `mock[clé]` ou une comparaison sur le mock ; un simple `Mock` suffit pour de simples appels de méthodes (`mock.faire(...)`). + --- ## Patch : Remplacer temporairement `patch` est le moyen le plus courant de mocker en Python. Il remplace temporairement un objet dans votre code. +### Où patcher : la règle d'or + +C'est le piège le plus courant du mocking : **on patche un objet là où il est *utilisé*, pas là où il est *défini***. Quand un module fait `from X import fonction`, il crée sa propre référence locale à `fonction` ; patcher `X.fonction` n'a alors plus aucun effet sur cette référence. + +```python +# fichier: service.py +from temps import maintenant # référence locale : service.maintenant + +def horodater(): + return maintenant() +``` + +```python +# ✅ Correct : on patche la référence dans le module qui l'utilise +with patch('service.maintenant', return_value="2024-01-01"): + assert service.horodater() == "2024-01-01" + +# ❌ Sans effet : service.py possède déjà sa propre référence 'maintenant' +with patch('temps.maintenant', return_value="2024-01-01"): + service.horodater() # appelle toujours la vraie fonction +``` + +**Règle** : la cible de `patch` est le chemin de l'objet *dans le module testé*. C'est pourquoi l'exemple météo plus haut patche `meteo.requests.get` (la référence utilisée par `meteo.py`), et non `requests.get`. + ### patch comme décorateur ```python @@ -989,6 +1039,88 @@ def test_patch_object(): assert obj.methode_originale() == "original" ``` +### monkeypatch : le patch natif de pytest + +pytest fournit sa propre fixture **`monkeypatch`** pour remplacer des attributs, des variables d'environnement ou des entrées de dictionnaire — avec **restauration automatique** à la fin du test (ni `with`, ni décorateur) : + +```python +import os + +def lire_cle_api(): + return os.environ["API_KEY"] + +def test_lire_cle_api(monkeypatch): + monkeypatch.setenv("API_KEY", "cle_de_test") + assert lire_cle_api() == "cle_de_test" + # API_KEY est restauree automatiquement apres le test + +def test_patch_attribut(monkeypatch): + import meteo + monkeypatch.setattr(meteo, "obtenir_temperature", lambda ville: 5) + assert meteo.recommander_vetements("Oslo") == "Manteau et écharpe" +``` + +Principales méthodes : `setattr`, `delattr`, `setenv`, `delenv`, `setitem`, `delitem`, `chdir`. En pratique : `monkeypatch` est plus concis pour les **variables d'environnement** et les **attributs** ; `unittest.mock.patch` reste préférable pour les mocks avec `return_value`/`side_effect` et les **assertions d'appel** (`assert_called_once`, etc.). + +### pytest-mock : la fixture `mocker` + +`monkeypatch` ne crée pas de `Mock` et ne vérifie pas les appels ; `unittest.mock.patch` le fait, mais impose un `with` ou un décorateur. Le plugin **`pytest-mock`** combine les deux avantages : il fournit une fixture **`mocker`** qui enveloppe tout `unittest.mock` et **annule automatiquement** les patches à la fin du test — sans gestionnaire de contexte ni décorateur. + +```bash +pip install pytest-mock +``` + +L'API de `mocker` reprend exactement celle de `unittest.mock` (`mocker.patch`, `mocker.patch.object`, `mocker.Mock`, `mocker.MagicMock`...), mais sans imbrication : + +```python +def obtenir_taux_change(): + """Appel réseau coûteux que l'on ne veut pas exécuter dans un test.""" + ... + +def convertir_en_euros(montant_usd): + return round(montant_usd * obtenir_taux_change(), 2) + +def test_conversion(mocker): + # Patch sans 'with' ni décorateur ; restauré automatiquement après le test + faux_taux = mocker.patch("banque.obtenir_taux_change", return_value=0.90) + + assert convertir_en_euros(100) == 90.0 + faux_taux.assert_called_once() +``` + +L'avantage est surtout net quand on patche **plusieurs cibles** : avec `unittest.mock`, les décorateurs s'empilent et leurs paramètres arrivent dans l'ordre **inversé** ; avec `mocker`, chaque patch est une ligne lue de haut en bas. + +```python +# unittest.mock : ordre des décorateurs inversé par rapport aux paramètres +@patch("module.b") +@patch("module.a") +def test_classique(mock_a, mock_b): # attention : a puis b ! + ... + +# pytest-mock : pas d'inversion, une ligne par cible +def test_avec_mocker(mocker): + mock_a = mocker.patch("module.a") + mock_b = mocker.patch("module.b") + ... +``` + +`mocker` ajoute aussi `mocker.spy(objet, "methode")`, qui **espionne** un objet réel (enregistre les appels reçus) tout en **laissant la vraie fonction s'exécuter** — pratique pour vérifier qu'une dépendance est bien appelée sans pour autant la remplacer : + +```python +def test_spy(mocker): + espion = mocker.spy(calculs, "additionner") + assert calculs.additionner(2, 3) == 5 # la vraie fonction tourne + espion.assert_called_once_with(2, 3) +``` + +**Quand utiliser quoi ?** + +- **`monkeypatch`** (natif pytest) : variables d'environnement, attributs simples, entrées de dictionnaire. +- **`unittest.mock.patch`** : disponible partout (y compris hors pytest), standard de la bibliothèque. +- **`mocker`** (pytest-mock) : la puissance complète de `unittest.mock` sans `with` ni décorateur — à privilégier au sein d'une suite pytest. + +> L'exemple exécutable complet (`return_value`, `side_effect`, `spy`) figure dans `exemples/02_15_pytest_mock.py`. + --- ## Bonnes pratiques @@ -1289,6 +1421,7 @@ def test_appel_api_avec_bons_parametres(service_paiement, mock_db): | Simuler une base de données | Mock | | Contrôler le temps | patch sur datetime | | Simuler des fichiers | mock_open | +| Patcher un attribut / variable d'environnement | monkeypatch (pytest) | | Vérifier qu'une fonction a été appelée | Assertions sur mock | ### Commandes pytest utiles diff --git a/10-tests-et-qualite/03-couverture-de-code.md b/10-tests-et-qualite/03-couverture-de-code.md index 7656271..f544bf0 100644 --- a/10-tests-et-qualite/03-couverture-de-code.md +++ b/10-tests-et-qualite/03-couverture-de-code.md @@ -142,12 +142,12 @@ coverage html Sortie typique de `coverage report` : ``` -Name Stmts Miss Cover ------------------------------------------ -calculatrice.py 15 6 60% -test_calculatrice.py 6 0 100% ------------------------------------------ -TOTAL 21 6 71% +Name Stmts Miss Cover +------------------------------------------ +calculatrice.py 14 5 64% +test_calculatrice.py 7 0 100% +------------------------------------------ +TOTAL 21 5 76% ``` **Explication** : @@ -155,7 +155,7 @@ TOTAL 21 6 71% - **Miss** : Nombre de lignes non exécutées - **Cover** : Pourcentage de couverture -Dans notre exemple : 6 lignes de `calculatrice.py` n'ont jamais été exécutées (les fonctions `multiplier()` et `calculer_moyenne()` et leurs branches). +Dans notre exemple : 5 lignes de `calculatrice.py` n'ont jamais été exécutées — le corps de `multiplier()`, celui de `calculer_moyenne()`, et le `raise` de `diviser()` (jamais déclenché par ces tests). ### Rapport HTML détaillé @@ -223,12 +223,12 @@ pytest --cov=module --cov-report=term --cov-config=.coveragerc Avec `--cov-report=term-missing`, vous voyez les lignes manquantes : ``` -Name Stmts Miss Cover Missing ---------------------------------------------------- -calculatrice.py 15 6 60% 12-13, 16-19 -test_calculatrice.py 6 0 100% ---------------------------------------------------- -TOTAL 21 6 71% +Name Stmts Miss Cover Missing +---------------------------------------------------- +calculatrice.py 14 5 64% 12, 17, 22-24 +test_calculatrice.py 7 0 100% +---------------------------------------------------- +TOTAL 21 5 76% ``` Les numéros dans "Missing" indiquent les lignes non couvertes. @@ -294,12 +294,12 @@ pytest --cov=calculatrice --cov-report=term-missing ``` ``` -Name Stmts Miss Cover Missing ---------------------------------------------------- -calculatrice.py 15 0 100% -test_calculatrice.py 20 0 100% ---------------------------------------------------- -TOTAL 35 0 100% +Name Stmts Miss Cover Missing +---------------------------------------------------- +calculatrice.py 14 0 100% +test_calculatrice.py 22 0 100% +---------------------------------------------------- +TOTAL 36 0 100% ``` **Nous avons atteint 100% de couverture ! 🎉** @@ -388,17 +388,17 @@ pytest --cov=validation --cov-branch --cov-report=term-missing ``` ``` -Name Stmts Miss Branch BrPart Cover Missing --------------------------------------------------------------- -validation.py 13 0 8 2 88% 3->5, 10->12 -test_validation.py 6 0 0 0 100% --------------------------------------------------------------- -TOTAL 19 0 8 2 90% +Name Stmts Miss Branch BrPart Cover Missing +---------------------------------------------------------------- +test_validation.py 7 0 0 0 100% +validation.py 16 4 12 4 71% 5, 17, 19, 21 +---------------------------------------------------------------- +TOTAL 23 4 12 4 77% ``` -**BrPart** (Branches Partielles) : 2 branches n'ont pas été testées -- `3->5` : La branche "note invalide" (note < 0 ou note > 20) -- `10->12` : Certaines mentions n'ont pas été testées +Ici, **Miss = 4** : quatre `return` ne sont jamais exécutés, et **BrPart = 4** (Branches Partielles) : quatre conditions `if`/`elif` ne sont empruntées que dans un seul sens. +- Ligne `5` : le cas "Note invalide" (`note < 0 or note > 20`) n'est jamais déclenché +- Lignes `17`, `19`, `21` : les mentions "Passable", "Assez bien" et "Bien" ne sont jamais testées Tests complets avec toutes les branches : @@ -689,15 +689,15 @@ pytest --cov=utilisateur --cov-report=term-missing --cov-branch ``` ``` -Name Stmts Miss Branch BrPart Cover Missing ------------------------------------------------------------------- -test_utilisateur.py 9 0 0 0 100% -utilisateur.py 42 29 12 0 25% 12-15, 18-21, 24-29, 32-35, 38-48, 55-60 ------------------------------------------------------------------- -TOTAL 51 29 12 0 37% +Name Stmts Miss Branch BrPart Cover Missing +----------------------------------------------------------------- +test_utilisateur.py 14 0 0 0 100% +utilisateur.py 45 16 6 0 57% 14, 18, 22, 26-27, 31-32, 36, 40, 58, 62-65, 69, 73 +----------------------------------------------------------------- +TOTAL 59 16 6 0 66% ``` -**Seulement 25% de couverture !** Beaucoup de fonctionnalités ne sont pas testées. +**Seulement 57% de couverture !** Beaucoup de méthodes (gestion des rôles, `obtenir`, `supprimer`, `lister_actifs`, `lister_admins`, etc.) ne sont pas testées. ### Tests complets @@ -894,12 +894,12 @@ pytest --cov=utilisateur --cov-report=term-missing --cov-branch ``` ``` -Name Stmts Miss Branch BrPart Cover --------------------------------------------------------- -test_utilisateur.py 90 0 0 0 100% -utilisateur.py 42 0 12 0 100% --------------------------------------------------------- -TOTAL 132 0 12 0 100% +Name Stmts Miss Branch BrPart Cover +------------------------------------------------------- +test_utilisateur.py 114 0 0 0 100% +utilisateur.py 45 0 6 0 100% +------------------------------------------------------- +TOTAL 159 0 6 0 100% ``` **100% de couverture atteinte ! 🎉** @@ -976,12 +976,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: '3.12' - name: Install dependencies run: | @@ -993,7 +993,7 @@ jobs: pytest --cov=src --cov-report=term --cov-fail-under=80 - name: Upload coverage report - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v4 ``` ### 5. Suivre l'évolution de la couverture @@ -1118,7 +1118,7 @@ Pour tester sur plusieurs versions de Python : ```ini # fichier: tox.ini [tox] -envlist = py310,py311,py312,py313 +envlist = py312,py313 [testenv] deps = diff --git a/10-tests-et-qualite/04-documentation-docstrings.md b/10-tests-et-qualite/04-documentation-docstrings.md index 1d69abb..03ce35a 100644 --- a/10-tests-et-qualite/04-documentation-docstrings.md +++ b/10-tests-et-qualite/04-documentation-docstrings.md @@ -1265,8 +1265,8 @@ class GestionnaireTaches: Example: >>> gestionnaire = GestionnaireTaches() - >>> gestionnaire.creer_tache("Tâche 1") - >>> gestionnaire.creer_tache("Tâche 2") + >>> _ = gestionnaire.creer_tache("Tâche 1") + >>> _ = gestionnaire.creer_tache("Tâche 2") >>> gestionnaire.compter_taches() 2 """ @@ -1421,6 +1421,45 @@ interrogate --generate-badge docs/ --- +## Tester les exemples avec doctest + +Les exemples `>>>` de vos docstrings ne sont pas que de la documentation : le module **`doctest`** (inclus dans Python) peut les **exécuter comme des tests** et vérifier que la sortie correspond. Vos exemples deviennent ainsi une documentation *vérifiée*, qui ne peut pas se désynchroniser du code. + +```python +# fichier: math_simple.py +def est_pair(nombre): + """Vérifie si un nombre est pair. + + Example: + >>> est_pair(4) + True + >>> est_pair(7) + False + """ + return nombre % 2 == 0 +``` + +Pour lancer les doctests : + +```bash +# Directement avec le module doctest (-v pour le détail) +python -m doctest math_simple.py -v + +# Ou via pytest, qui découvre les doctests des modules +pytest --doctest-modules +``` + +Si un exemple ne correspond plus à la sortie réelle, le test **échoue** — ce qui vous oblige à garder la documentation exacte. Vous pouvez activer `--doctest-modules` en permanence via `pyproject.toml` : + +```toml +[tool.pytest.ini_options] +addopts = "--doctest-modules" +``` + +> **Limite** : doctest compare la sortie **caractère par caractère**. Il est donc fragile pour les flottants (`0.1 + 0.2` ne s'affiche pas `0.3`), les `set` (dont l'ordre d'affichage n'est pas garanti), ou les adresses mémoire (`<... at 0x7f...>`). *(Les `dict`, eux, conservent leur ordre d'insertion depuis Python 3.7 : leur affichage est reproductible et convient au doctest.)* Réservez-le aux exemples à sortie simple et déterministe ; pour le reste, écrivez de vrais tests pytest. + +--- + ## Résumé ### Points clés à retenir @@ -1432,6 +1471,7 @@ interrogate --generate-badge docs/ 5. **Sphinx génère** de la documentation HTML automatiquement 6. **help()** accède aux docstrings dans le REPL 7. **Bonnes pratiques** : Être clair, donner des exemples, documenter les cas limites +8. **doctest** : les exemples `>>>` des docstrings peuvent être exécutés comme des tests (`pytest --doctest-modules`) ### Template de docstring Google Style @@ -1501,6 +1541,10 @@ interrogate mon_package/ # Voir la documentation dans le terminal python -m pydoc mon_module + +# Tester les exemples des docstrings (doctest) +python -m doctest mon_module.py -v +pytest --doctest-modules ``` ### Ressources complémentaires diff --git a/10-tests-et-qualite/05-pep8-et-linting.md b/10-tests-et-qualite/05-pep8-et-linting.md index b056d85..b0c49a8 100644 --- a/10-tests-et-qualite/05-pep8-et-linting.md +++ b/10-tests-et-qualite/05-pep8-et-linting.md @@ -679,7 +679,7 @@ Black a très peu d'options (c'est volontaire !). Fichier `pyproject.toml` : ```toml [tool.black] line-length = 88 -target-version = ['py310'] +target-version = ['py312'] include = '\.pyi?$' exclude = ''' /( @@ -792,7 +792,7 @@ def additionner(a: int, b: int) -> int: resultat = additionner(5, 3) # ❌ Erreur détectée par mypy -resultat = additionner("5", "3") # error: Argument 1 has incompatible type "str"; expected "int" +resultat = additionner("5", "3") # error: Argument 1 to "additionner" has incompatible type "str"; expected "int" ``` ### 6. bandit : Sécurité @@ -862,7 +862,7 @@ Dans `pyproject.toml` : ```toml [tool.ruff] line-length = 88 -target-version = "py310" +target-version = "py312" [tool.ruff.lint] select = [ @@ -893,6 +893,8 @@ known-first-party = ["mon_package"] Ruff est compatible avec les règles de flake8 et utilise les mêmes codes d'erreur, ce qui facilite la migration. +> **Pourquoi un tel écart de vitesse ?** Ce n'est pas *seulement* le langage. flake8 orchestre plusieurs outils Python (pycodestyle, pyflakes, mccabe…) qui ré-analysent chacun le fichier de leur côté. Ruff, lui, **parse le fichier une seule fois** en un arbre syntaxique (AST) partagé par *toutes* ses règles — le tout en code natif (Rust, sans interpréteur Python à démarrer), avec parallélisme multi-cœur et mise en cache des résultats. C'est cette architecture « un seul passage » — bien plus que le seul choix de Rust — qui explique le facteur 10-100×. + --- ## Configuration d'un projet complet @@ -921,7 +923,7 @@ mon_projet/ [tool.black] line-length = 88 -target-version = ['py310'] +target-version = ['py312'] include = '\.pyi?$' [tool.isort] @@ -929,7 +931,7 @@ profile = "black" line_length = 88 [tool.mypy] -python_version = "3.10" +python_version = "3.12" warn_return_any = true warn_unused_configs = true disallow_untyped_defs = true @@ -1035,7 +1037,7 @@ repos: rev: 23.3.0 hooks: - id: black - language_version: python3.10 + language_version: python3.12 - repo: https://github.com/pycqa/isort rev: 5.12.0 @@ -1062,6 +1064,26 @@ repos: args: ['-ll'] ``` +> Les versions (`rev`) ci-dessus sont indicatives : lancez `pre-commit autoupdate` pour les mettre à jour. Notez aussi que `types-all` est déprécié — installez plutôt les stubs précis nécessaires (`types-requests`, etc.). + +**Variante moderne avec ruff** — un seul dépôt remplace Black, isort, flake8 et bandit, ce qui réduit la configuration à deux hooks : + +```yaml +# fichier: .pre-commit-config.yaml (version ruff) +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.0 # mettez la dernière version (pre-commit autoupdate) + hooks: + - id: ruff # linting (avec corrections automatiques) + args: [--fix] + - id: ruff-format # formatage (remplace Black) + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.13.0 + hooks: + - id: mypy +``` + #### Activation ```bash @@ -1100,7 +1122,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: '3.12' - name: Install dependencies run: | @@ -1127,7 +1149,7 @@ Créez `.gitlab-ci.yml` : ```yaml lint: stage: test - image: python:3.10 + image: python:3.12 script: - pip install flake8 black isort - black --check . @@ -1241,26 +1263,24 @@ class user: ```bash $ flake8 exemple.py -exemple.py:1:10: E401 multiple imports on one line -exemple.py:2:1: F403 'from datetime import *' used; unable to detect undefined names -exemple.py:3:1: E302 expected 2 blank lines, found 0 -exemple.py:3:15: E231 missing whitespace after ',' -exemple.py:4:1: E111 indentation is not a multiple of 4 -exemple.py:5:2: E225 missing whitespace around operator -exemple.py:5:11: E701 multiple statements on one line (colon) -exemple.py:6:2: E701 multiple statements on one line (colon) -exemple.py:7:1: E302 expected 2 blank lines, found 0 -exemple.py:7:7: E999 SyntaxError: invalid syntax -exemple.py:8:1: E111 indentation is not a multiple of 4 -exemple.py:8:25: E702 multiple statements on one line (semicolon) -exemple.py:9:1: E111 indentation is not a multiple of 4 -exemple.py:9:6: N802 function name should be lowercase -exemple.py:10:2: E111 indentation is not a multiple of 4 -exemple.py:10:18: E701 multiple statements on one line (colon) -exemple.py:11:2: E701 multiple statements on one line (colon) -``` - -**25 erreurs détectées !** +exemple.py:1:1: F401 'sys' imported but unused +exemple.py:1:1: F401 'os' imported but unused +exemple.py:1:11: E401 multiple imports on one line +exemple.py:1:11: E231 missing whitespace after ',' +exemple.py:2:1: F403 'from datetime import *' used; unable to detect undefined names +exemple.py:3:1: E302 expected 2 blank lines, found 0 +exemple.py:3:11: E231 missing whitespace after ',' +exemple.py:4:2: E111 indentation is not a multiple of 4 +exemple.py:4:8: E225 missing whitespace around operator +exemple.py:5:13: E701 multiple statements on one line (colon) +exemple.py:6:6: E701 multiple statements on one line (colon) +exemple.py:8:36: E702 multiple statements on one line (semicolon) +exemple.py:9:2: E301 expected 1 blank line, found 0 +exemple.py:10:14: E225 missing whitespace around operator +... +``` + +**36 violations détectées !** (extrait ci-dessus — la liste complète est plus longue). ### Étape 2 : Appliquer Black @@ -1280,13 +1300,11 @@ Les imports sont organisés. ### Étape 4 : Corrections manuelles +On finalise à la main ce que les outils ne font pas : ajout des docstrings, noms explicites, et **suppression des imports inutilisés** (`os` et `sys`, jamais utilisés, sont signalés en `F401` ; le `from datetime import *` — à proscrire — est quant à lui signalé en `F403`, comme à l'étape 1). + ```python """Module de gestion d'utilisateurs.""" -import os -import sys -from datetime import datetime - def calculer_resultat(valeur_x, valeur_y, seuil): """Calcule un résultat basé sur un seuil. @@ -1303,8 +1321,7 @@ def calculer_resultat(valeur_x, valeur_y, seuil): if resultat > seuil: return resultat - else: - return 0 + return 0 class Utilisateur: @@ -1340,8 +1357,12 @@ class Utilisateur: $ flake8 exemple.py # Aucune erreur ! +$ ruff check exemple.py +# All checks passed! + $ pylint exemple.py -# Note: 10.00/10 +# Note: 9.09/10 (la seule remarque restante, "too-few-public-methods", +# se désactive dans .pylintrc comme vu plus haut, d'où 10.00/10) ``` --- diff --git a/10-tests-et-qualite/06-validation-types-mypy.md b/10-tests-et-qualite/06-validation-types-mypy.md index f34c039..849339b 100644 --- a/10-tests-et-qualite/06-validation-types-mypy.md +++ b/10-tests-et-qualite/06-validation-types-mypy.md @@ -55,6 +55,15 @@ def additionner(a: int, b: int) -> int: resultat = additionner("5", "3") # Pas d'erreur à l'exécution ! ``` +> **« Ignorés », pas « effacés » — le mécanisme.** Python ne *vérifie* pas les annotations, mais il ne les jette pas non plus : il les **évalue et les range** dans l'attribut `__annotations__` de la fonction (ou de la classe/du module). On peut donc les lire à l'exécution : +> +> ```python +> print(additionner.__annotations__) +> # {'a': , 'b': , 'return': } +> ``` +> +> Python lui-même n'en fait **rien** (fidèle à son typage dynamique) — d'où la nécessité d'un outil **externe** comme mypy pour les contrôler. Mais d'autres bibliothèques les *exploitent* bel et bien à l'exécution : `dataclasses` génère `__init__` à partir des annotations, `pydantic` valide les données entrantes avec, etc. Une annotation n'est donc pas un commentaire inerte. + ### Qu'est-ce que mypy ? **mypy** est un outil qui analyse votre code et vérifie que les types sont utilisés correctement, **avant** l'exécution. @@ -104,7 +113,7 @@ Exécutons mypy : ```bash $ mypy exemple.py -exemple.py:10: error: Argument 1 to "saluer" has incompatible type "int"; expected "str" +exemple.py:11: error: Argument 1 to "saluer" has incompatible type "int"; expected "str" Found 1 error in 1 file (checked 1 source file) ``` @@ -233,7 +242,7 @@ personne: Tuple[str, int, bool] = ("Alice", 25, True) coordonnees: Tuple[int, ...] = (10, 20, 30, 40) ``` -**Note importante** : En Python 3.9+, vous pouvez utiliser la syntaxe simplifiée : +**Note importante** : Depuis Python 3.9 (PEP 585), ces types s'écrivent directement avec les types natifs. Les alias `typing.List`, `typing.Dict`, `typing.Set`, `typing.Tuple` sont **dépréciés** depuis la 3.9 — ils restent utilisables sans avertissement à l'exécution, mais les outils modernes comme `ruff` les signalent (règle `UP035`). Préférez la forme native : ```python # Python 3.9+ @@ -523,6 +532,15 @@ def obtenir_utilisateur( return utilisateurs.get(user_id) ``` +**Note (Python 3.12+)** : depuis Python 3.12, le mot-clé `type` (PEP 695) crée des alias de manière plus concise, et `typing.TypeAlias` est **déprécié** : + +```python +# Python 3.12+ (PEP 695) +type UserId = int +type Email = str +type Utilisateurs = dict[UserId, Utilisateur] +``` + ### Générics : types paramétrés ```python @@ -569,6 +587,22 @@ boite_int.remplacer(100) # ✅ OK boite_int.remplacer("texte") # ❌ Erreur: expected int, got str ``` +**Note (Python 3.12+)** : la PEP 695 introduit une syntaxe allégée pour les génériques, sans déclaration explicite de `TypeVar` : + +```python +# Python 3.12+ (PEP 695) +class Boite[T]: + def __init__(self, contenu: T) -> None: + self.contenu = contenu + + def obtenir(self) -> T: + return self.contenu + +# Fonction générique +def premier[T](elements: list[T]) -> T: + return elements[0] +``` + --- ## Fonctionnalités avancées @@ -734,7 +768,7 @@ Créez un fichier `mypy.ini` à la racine du projet : # fichier: mypy.ini [mypy] # Version Python ciblée -python_version = 3.10 +python_version = 3.12 # Fichiers à vérifier files = src/ @@ -774,7 +808,7 @@ Alternative moderne avec `pyproject.toml` : ```toml # fichier: pyproject.toml [tool.mypy] -python_version = "3.10" +python_version = "3.12" files = ["src"] exclude = ["tests", "docs", "build"] @@ -839,13 +873,15 @@ def fonction_legacy(data: Any) -> Any: # Ignorer cette ligne return data.process() # type: ignore -# Ignorer avec raison +# Ignorer UNIQUEMENT un code d'erreur précis (les autres restent vérifiés) resultat = fonction_externe() # type: ignore[no-untyped-call] # Ignorer avec commentaire explicatif valeur = calcul_complexe() # type: ignore # TODO: Ajouter les types ``` +Le `[code]` entre crochets restreint l'ignore à **un seul type d'erreur** : `# type: ignore[no-untyped-call]` ne masque que cette erreur-là ; si une *autre* erreur apparaît sur la même ligne, mypy la signalera quand même. C'est bien plus sûr qu'un `# type: ignore` nu, qui masque **toutes** les erreurs de la ligne (y compris une future erreur introduite par mégarde). Le code à placer entre crochets est celui qu'affiche mypy avec l'option `--show-error-codes` (par exemple `[import]`, `[arg-type]`, `[return-value]`). + ### Ignorer un fichier entier ```python @@ -876,6 +912,23 @@ def obtenir_donnees() -> Any: donnees = cast(dict[str, Any], obtenir_donnees()) ``` +### reveal_type : inspecter ce que mypy infère + +Pour comprendre quel type mypy attribue à une expression, insérez un appel **`reveal_type(...)`** : lors de la vérification, mypy affiche le type inféré. Cette fonction n'existe pas à l'exécution — c'est un outil purement statique, à retirer une fois le diagnostic terminé. + +```python +def trouver(valeurs: list[int]) -> int | None: + for v in valeurs: + if v > 0: + return v + return None + +resultat = trouver([1, 2, 3]) +reveal_type(resultat) # note: Revealed type is "int | None" +``` + +C'est l'outil idéal pour déboguer un type trop large ou inattendu. (`reveal_locals()` affiche de même le type de toutes les variables locales à cet endroit.) + --- ## Cas pratique : API de gestion de tâches @@ -1103,8 +1156,8 @@ def exemple_utilisation() -> None: # Créer des tâches tache1 = gestionnaire.creer_tache("Faire les courses", Priorite.HAUTE) - tache2 = gestionnaire.creer_tache("Lire un livre") - tache3 = gestionnaire.creer_tache("Faire du sport", Priorite.BASSE) + gestionnaire.creer_tache("Lire un livre") + gestionnaire.creer_tache("Faire du sport", Priorite.BASSE) # Marquer une tâche comme terminée tache1.marquer_terminee() @@ -1176,7 +1229,9 @@ repos: rev: v1.5.0 hooks: - id: mypy - additional_dependencies: [types-all] + # Listez ici les stubs nécessaires à votre projet (le méta-paquet + # types-all n'est plus maintenu : ajoutez les stubs un par un). + additional_dependencies: [types-requests, types-PyYAML] args: [--ignore-missing-imports] ``` @@ -1200,7 +1255,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: '3.12' - name: Install dependencies run: | @@ -1476,7 +1531,7 @@ mypy --config-file mypy.ini src/ - [ ] Installer mypy : `pip install mypy` - [ ] Annoter les fonctions publiques -- [ ] Utiliser Optional pour les valeurs nullables +- [ ] Utiliser `X | None` (ou `Optional[X]`) pour les valeurs nullables - [ ] Configurer mypy.ini ou pyproject.toml - [ ] Intégrer dans l'IDE (VS Code, PyCharm) - [ ] Ajouter à pre-commit hooks @@ -1517,11 +1572,26 @@ def ma_fonction( --- +## Au-delà de mypy : le paysage des vérificateurs de types (2026) + +mypy reste la **référence** pour la vérification de types en Python : c'est l'implémentation la plus mature, dotée d'un riche écosystème de plugins (Django, Pydantic, SQLAlchemy…). C'est l'outil à apprendre en premier. + +Mais l'écosystème évolue vite — comme `ruff` l'a fait pour le linting (voir 10.5), des vérificateurs écrits en **Rust**, bien plus rapides, ont émergé : + +- **ty** — par Astral (les auteurs de `ruff` et `uv`). 10 à 60× plus rapide que mypy ; en **bêta** en 2026 (version 1.0 visée courant 2026). +- **pyrefly** — par Meta (issu de leur outil Pyre). Passé en **1.0 stable** en 2026 ; utilisé par défaut sur la base de code Python d'Instagram. + +Ils comprennent les mêmes annotations de types standard (PEP 484 et suivantes), mais n'ont **pas encore** le système de plugins de mypy ; beaucoup d'équipes les exécutent donc *en complément* de mypy plutôt qu'en remplacement. Un mouvement à surveiller de près. + +--- + ## Ressources complémentaires - **Documentation officielle mypy** : https://mypy.readthedocs.io/ - **PEP 484** (Type Hints) : https://peps.python.org/pep-0484/ -- **PEP 585** (Syntaxe moderne) : https://peps.python.org/pep-0585/ +- **PEP 585** (génériques natifs `list[int]`) : https://peps.python.org/pep-0585/ +- **PEP 604** (syntaxe `X | Y`) : https://peps.python.org/pep-0604/ +- **PEP 695** (mot-clé `type` et génériques, 3.12+) : https://peps.python.org/pep-0695/ - **typing module** : https://docs.python.org/3/library/typing.html - **Real Python - Type Checking** : https://realpython.com/python-type-checking/ - **mypy cheat sheet** : https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html diff --git a/10-tests-et-qualite/README.md b/10-tests-et-qualite/README.md index a83571b..fba1375 100644 --- a/10-tests-et-qualite/README.md +++ b/10-tests-et-qualite/README.md @@ -319,7 +319,7 @@ def test_exemple(): # 3. ASSERT (Vérifier) # Vérifier que le résultat est correct - assert resultat == True + assert resultat is True ``` ### Qu'est-ce qu'une assertion ? @@ -410,13 +410,13 @@ def test_utilisateur_nom(): def test_utilisateur_actif_par_defaut(): user = Utilisateur("Alice") - assert user.actif == True + assert user.actif is True # ❌ Mauvais - teste trop de choses def test_utilisateur(): user = Utilisateur("Alice") assert user.nom == "Alice" - assert user.actif == True + assert user.actif is True assert user.email is None assert user.age is None # ... trop de vérifications @@ -480,17 +480,17 @@ Un test doit être facile à comprendre, même sans connaître le code testé. def test_utilisateur_peut_se_connecter_avec_bon_mot_de_passe(): user = Utilisateur("alice@test.com", "motdepasse123") resultat = user.se_connecter("motdepasse123") - assert resultat == True + assert resultat is True def test_utilisateur_ne_peut_pas_se_connecter_avec_mauvais_mot_de_passe(): user = Utilisateur("alice@test.com", "motdepasse123") resultat = user.se_connecter("mauvais") - assert resultat == False + assert resultat is False # ❌ Mauvais - test cryptique def test_login(): u = User("a@t.c", "p") - assert u.l("p") == True + assert u.l("p") is True ``` #### 5. Testez les cas limites @@ -599,6 +599,7 @@ Pour un projet Python professionnel : | **unittest** | Tests unitaires (standard) | 10.1 | | **pytest-cov** | Couverture de code | 10.3 | | **coverage.py** | Couverture de code | 10.3 | +| **ruff** | Linter + formateur ultra-rapide (remplace flake8, isort, et `black`) | 10.5 | | **flake8** | Linting (style) | 10.5 | | **black** | Formatage automatique | 10.5 | | **isort** | Organisation des imports | 10.5 | @@ -606,6 +607,8 @@ Pour un projet Python professionnel : | **pylint** | Analyse approfondie | 10.5 | | **pre-commit** | Automatisation | Toutes | +> **Note (2024+)** : `ruff` (écrit en Rust) est devenu l'outil de référence pour le style. Il fait à lui seul le travail de **flake8 + isort** et une grande partie de **pylint**, et embarque un formateur compatible Black (`ruff format`) — le tout des dizaines de fois plus vite. Les outils classiques ci-dessus restent valables (et largement utilisés) ; ils sont détaillés en 10.5, où `ruff` est présenté comme l'option moderne recommandée. + ### Pipeline de qualité ``` @@ -638,6 +641,8 @@ Pour un projet Python professionnel : └──────────────────────────────────────┘ ``` +> Avec `ruff`, les étapes 2 et 3 fusionnent : `ruff format` (formatage, étape 2) puis `ruff check` (linting, étape 3) — un seul outil au lieu de trois. + --- ## Mentalité de qualité @@ -722,7 +727,8 @@ Avant de commencer les sections suivantes, installez les outils essentiels : pip install pytest pytest-cov # Linting et formatage -pip install flake8 black isort +pip install ruff # moderne, tout-en-un (recommandé) +pip install flake8 black isort # approche classique (alternative) # Vérification de types pip install mypy diff --git a/10-tests-et-qualite/exemples/01_05_skip_xfail_pytest.py b/10-tests-et-qualite/exemples/01_05_skip_xfail_pytest.py new file mode 100644 index 0000000..6c658c4 --- /dev/null +++ b/10-tests-et-qualite/exemples/01_05_skip_xfail_pytest.py @@ -0,0 +1,46 @@ +# ============================================================================ +# Section 10.1 : Tests unitaires avec unittest et pytest +# Description : Marqueurs pytest pour controler l'execution des tests +# (skip, skipif, xfail, xpass) +# Fichier source : 01-tests-unitaires-unittest-pytest.md +# Execution : pytest 01_05_skip_xfail_pytest.py -v -o "addopts=" +# ============================================================================ + +import sys + +import pytest + + +def fonction_existante(): + """Fonction de demonstration.""" + return 42 + + +@pytest.mark.skip(reason="fonctionnalite pas encore implementee") +def test_a_venir(): + # Jamais execute : le corps peut meme appeler du code inexistant. + assert fonction_pas_encore_ecrite() == 0 # noqa: F821 + + +@pytest.mark.skipif(sys.version_info < (3, 10), reason="reserve a Python 3.10+") +def test_reserve_aux_versions_recentes(): + # 'int | str' (operateur | sur les types) est une fonctionnalite 3.10+. + union_type = int | str + assert isinstance(5, union_type) + + +@pytest.mark.xfail(reason="bug connu, correctif en cours") +def test_bug_connu(): + # On s'attend a un echec : marque 'xfail' (et non une erreur). + assert fonction_existante() == 0 + + +@pytest.mark.xfail(reason="devrait deja etre corrige") +def test_deja_corrige(): + # Reussit malgre xfail : marque 'xpass' (signal a verifier). + assert fonction_existante() == 42 + + +def test_normal(): + """Test classique, sans marqueur.""" + assert fonction_existante() == 42 diff --git a/10-tests-et-qualite/exemples/01_06_pytest_raises.py b/10-tests-et-qualite/exemples/01_06_pytest_raises.py new file mode 100644 index 0000000..a233e57 --- /dev/null +++ b/10-tests-et-qualite/exemples/01_06_pytest_raises.py @@ -0,0 +1,43 @@ +# ============================================================================ +# Section 10.1 : Tests unitaires avec unittest et pytest +# Description : Tester les exceptions avec pytest.raises -- verifier qu'une +# exception est levee, controler son message avec `match` +# (expression reguliere), et inspecter l'objet via `as excinfo`. +# Fichier source : 01-tests-unitaires-unittest-pytest.md +# Execution : pytest 01_06_pytest_raises.py -v -o "addopts=" +# ============================================================================ + +import pytest + + +def retirer(solde, montant): + """Retire un montant d'un solde ; leve ValueError si fonds insuffisants.""" + if montant > solde: + raise ValueError("Solde insuffisant pour ce retrait") + return solde - montant + + +def test_raises_simple(): + """pytest.raises : verifie qu'une exception est bien levee.""" + with pytest.raises(ValueError): + retirer(100, 500) + + +def test_raises_match(): + """match : le message doit correspondre (sous-chaine / regex, via re.search).""" + with pytest.raises(ValueError, match="insuffisant"): + retirer(100, 500) + + +def test_raises_excinfo(): + """as excinfo : inspecter le type et le message de l'exception capturee.""" + with pytest.raises(ValueError) as excinfo: + retirer(100, 500) + + assert excinfo.type is ValueError + assert "insuffisant" in str(excinfo.value) + + +def test_pas_d_exception_si_solde_suffisant(): + """Cas nominal : aucune exception levee, le retrait est effectue.""" + assert retirer(100, 30) == 70 diff --git a/10-tests-et-qualite/exemples/02_05_mock_assertions.py b/10-tests-et-qualite/exemples/02_05_mock_assertions.py index 0a07e3c..0c6f985 100644 --- a/10-tests-et-qualite/exemples/02_05_mock_assertions.py +++ b/10-tests-et-qualite/exemples/02_05_mock_assertions.py @@ -60,4 +60,4 @@ def methode_existante(self): try: mock_avec_spec.methode_qui_nexiste_pas() except AttributeError as e: - print(f"Avec spec : methode_qui_nexiste_pas() -> AttributeError") + print(f"Avec spec : methode_qui_nexiste_pas() -> AttributeError: {e}") diff --git a/10-tests-et-qualite/exemples/02_06_patch.py b/10-tests-et-qualite/exemples/02_06_patch.py index d17a086..0349f2f 100644 --- a/10-tests-et-qualite/exemples/02_06_patch.py +++ b/10-tests-et-qualite/exemples/02_06_patch.py @@ -5,7 +5,8 @@ # Fichier source : 02-mocking-et-fixtures.md # ============================================================================ -from unittest.mock import patch, Mock +import os +from unittest.mock import patch # --- Classe de demonstration --- @@ -34,7 +35,6 @@ def methode_originale(self): print("\n=== patch comme context manager ===") # Patcher os.path.exists -import os with patch('os.path.exists', return_value=True) as mock_exists: resultat = os.path.exists("/fichier/inexistant") print(f"os.path.exists mocke : {resultat}") # True diff --git a/10-tests-et-qualite/exemples/02_09_test_evenements.py b/10-tests-et-qualite/exemples/02_09_test_evenements.py index c2ee618..b954714 100644 --- a/10-tests-et-qualite/exemples/02_09_test_evenements.py +++ b/10-tests-et-qualite/exemples/02_09_test_evenements.py @@ -5,9 +5,8 @@ # Fichier source : 02-mocking-et-fixtures.md # ============================================================================ -import pytest from unittest.mock import patch -from datetime import datetime, timedelta +from datetime import datetime from evenements import Evenement diff --git a/10-tests-et-qualite/exemples/02_10_test_config.py b/10-tests-et-qualite/exemples/02_10_test_config.py index 69d1126..59f00a3 100644 --- a/10-tests-et-qualite/exemples/02_10_test_config.py +++ b/10-tests-et-qualite/exemples/02_10_test_config.py @@ -6,7 +6,6 @@ # Fichier source : 02-mocking-et-fixtures.md # ============================================================================ -import pytest from unittest.mock import mock_open, patch import json from config import Configuration diff --git a/10-tests-et-qualite/exemples/02_11_test_notification.py b/10-tests-et-qualite/exemples/02_11_test_notification.py index 9086781..9a6d445 100644 --- a/10-tests-et-qualite/exemples/02_11_test_notification.py +++ b/10-tests-et-qualite/exemples/02_11_test_notification.py @@ -6,7 +6,7 @@ # ============================================================================ import pytest -from unittest.mock import Mock, patch, MagicMock +from unittest.mock import patch, MagicMock from notification import ServiceNotification diff --git a/10-tests-et-qualite/exemples/02_13_monkeypatch.py b/10-tests-et-qualite/exemples/02_13_monkeypatch.py new file mode 100644 index 0000000..3e59eab --- /dev/null +++ b/10-tests-et-qualite/exemples/02_13_monkeypatch.py @@ -0,0 +1,43 @@ +# ============================================================================ +# Section 10.2 : Mocking et fixtures +# Description : monkeypatch, la fixture native de pytest pour remplacer +# attributs, variables d'environnement et entrees de dict. +# Toutes les modifications sont annulees apres chaque test. +# Fichier source : 02-mocking-et-fixtures.md +# Execution : pytest 02_13_monkeypatch.py -v -o "addopts=" +# ============================================================================ + +import os + + +def lire_cle_api(): + """Lit la cle API depuis l'environnement.""" + return os.environ.get("API_KEY", "absente") + + +PARAMETRES = {"timeout": 30} + + +def test_setenv(monkeypatch): + """setenv : definit une variable d'environnement (annulee apres le test).""" + monkeypatch.setenv("API_KEY", "cle_de_test") + assert lire_cle_api() == "cle_de_test" + + +def test_delenv(monkeypatch): + """delenv : supprime une variable d'environnement.""" + monkeypatch.setenv("API_KEY", "valeur") + monkeypatch.delenv("API_KEY") + assert lire_cle_api() == "absente" + + +def test_setattr(monkeypatch): + """setattr : remplace un attribut (ici une fonction d'un module).""" + monkeypatch.setattr(os, "getcwd", lambda: "/chemin/simule") + assert os.getcwd() == "/chemin/simule" + + +def test_setitem(monkeypatch): + """setitem : modifie une entree de dictionnaire (annulee apres le test).""" + monkeypatch.setitem(PARAMETRES, "timeout", 60) + assert PARAMETRES["timeout"] == 60 diff --git a/10-tests-et-qualite/exemples/02_14_conftest_demo/compte.py b/10-tests-et-qualite/exemples/02_14_conftest_demo/compte.py new file mode 100644 index 0000000..b8ac3cf --- /dev/null +++ b/10-tests-et-qualite/exemples/02_14_conftest_demo/compte.py @@ -0,0 +1,18 @@ +# ============================================================================ +# Section 10.2 : Mocking et fixtures - demonstration de conftest.py +# Module source : classe Compte utilisee par la fixture partagee. +# ============================================================================ + + +class Compte: + """Represente un compte bancaire simple.""" + + def __init__(self, titulaire, solde=0): + self.titulaire = titulaire + self.solde = solde + + def deposer(self, montant): + """Depose de l'argent sur le compte.""" + if montant <= 0: + raise ValueError("Le montant doit etre positif") + self.solde += montant diff --git a/10-tests-et-qualite/exemples/02_14_conftest_demo/conftest.py b/10-tests-et-qualite/exemples/02_14_conftest_demo/conftest.py new file mode 100644 index 0000000..d2d5c4b --- /dev/null +++ b/10-tests-et-qualite/exemples/02_14_conftest_demo/conftest.py @@ -0,0 +1,14 @@ +# ============================================================================ +# conftest.py : fixtures partagees, decouvertes automatiquement par pytest +# pour tous les tests de ce dossier (sans import). +# ============================================================================ + +import pytest + +from compte import Compte + + +@pytest.fixture +def compte(): + """Fixture partagee : disponible dans tous les tests du dossier.""" + return Compte("Alice", solde=1000) diff --git a/10-tests-et-qualite/exemples/02_14_conftest_demo/test_depot.py b/10-tests-et-qualite/exemples/02_14_conftest_demo/test_depot.py new file mode 100644 index 0000000..adbe439 --- /dev/null +++ b/10-tests-et-qualite/exemples/02_14_conftest_demo/test_depot.py @@ -0,0 +1,16 @@ +# ============================================================================ +# Tests utilisant la fixture 'compte' definie dans conftest.py. +# Remarquez : aucun import de la fixture n'est necessaire. +# Execution : depuis ce dossier -> pytest -v -o "addopts=" +# ============================================================================ + + +def test_solde_initial(compte): + """La fixture fournit un compte avec un solde de 1000.""" + assert compte.solde == 1000 + + +def test_deposer(compte): + """Chaque test recoit une instance neuve de la fixture.""" + compte.deposer(500) + assert compte.solde == 1500 diff --git a/10-tests-et-qualite/exemples/02_15_pytest_mock.py b/10-tests-et-qualite/exemples/02_15_pytest_mock.py new file mode 100644 index 0000000..2a2faea --- /dev/null +++ b/10-tests-et-qualite/exemples/02_15_pytest_mock.py @@ -0,0 +1,65 @@ +# ============================================================================ +# Section 10.2 : Mocking et fixtures +# Description : pytest-mock - la fixture `mocker`, wrapper de unittest.mock +# avec restauration automatique (sans 'with' ni decorateur). +# Illustre patch (return_value, side_effect), les assertions +# d'appel, et spy (espionner sans empecher l'execution reelle). +# Fichier source : 02-mocking-et-fixtures.md +# Necessite : pip install pytest-mock +# Execution : pytest 02_15_pytest_mock.py -v -o "addopts=" +# ============================================================================ + +import sys + + +# ---- Code applicatif (normalement dans un module separe) ------------------- + +def obtenir_taux_change(): + """Simule un appel reseau couteux (interdit pendant les tests).""" + raise RuntimeError("Vrai appel réseau interdit dans les tests") + + +def convertir_en_euros(montant_usd): + """Convertit un montant USD en EUR via le taux de change courant.""" + taux = obtenir_taux_change() + return round(montant_usd * taux, 2) + + +def additionner(a, b): + """Addition simple, utilisee pour illustrer mocker.spy().""" + return a + b + + +# Le nom de ce fichier commence par un chiffre : il n'est pas importable par +# un chemin texte ("02_15_...obtenir_taux_change"). On passe donc l'OBJET +# module a patch.object. Dans un vrai projet, on ecrirait simplement : +# mocker.patch("banque.obtenir_taux_change", return_value=0.90) +_module = sys.modules[__name__] + + +# ---- Tests utilisant la fixture `mocker` (fournie par pytest-mock) ---------- + +def test_patch_return_value(mocker): + """patch + return_value : ni 'with' ni decorateur, restaure apres le test.""" + faux_taux = mocker.patch.object( + _module, "obtenir_taux_change", return_value=0.90 + ) + assert convertir_en_euros(100) == 90.0 + faux_taux.assert_called_once() + + +def test_patch_side_effect(mocker): + """side_effect : une valeur differente a chaque appel successif.""" + mocker.patch.object( + _module, "obtenir_taux_change", side_effect=[0.90, 0.80] + ) + assert convertir_en_euros(100) == 90.0 # 1er appel -> taux 0.90 + assert convertir_en_euros(100) == 80.0 # 2e appel -> taux 0.80 + + +def test_spy(mocker): + """spy : on espionne `additionner` SANS empecher son execution reelle.""" + espion = mocker.spy(_module, "additionner") + resultat = additionner(2, 3) + assert resultat == 5 # la vraie fonction a bien tourne + espion.assert_called_once_with(2, 3) diff --git a/10-tests-et-qualite/exemples/04_03_fonctions_documentees.py b/10-tests-et-qualite/exemples/04_03_fonctions_documentees.py index 93dbd08..f23d460 100644 --- a/10-tests-et-qualite/exemples/04_03_fonctions_documentees.py +++ b/10-tests-et-qualite/exemples/04_03_fonctions_documentees.py @@ -99,7 +99,7 @@ def formater_prix(prix, devise="EUR"): >>> formater_prix(100, "USD") '100.00 $' """ - symboles = {"EUR": "EUR", "USD": "$", "GBP": "£"} + symboles = {"EUR": "EUR", "USD": "$", "GBP": "GBP"} symbole = symboles.get(devise, devise) return f"{prix:.2f} {symbole}" diff --git a/10-tests-et-qualite/exemples/04_05_api_taches.py b/10-tests-et-qualite/exemples/04_05_api_taches.py index dd36a1b..e9c4508 100644 --- a/10-tests-et-qualite/exemples/04_05_api_taches.py +++ b/10-tests-et-qualite/exemples/04_05_api_taches.py @@ -13,7 +13,6 @@ suppression (CRUD). Example: - >>> from api_taches import GestionnaireTaches, Tache >>> gestionnaire = GestionnaireTaches() >>> tache = gestionnaire.creer_tache("Acheter du pain") >>> gestionnaire.lister_taches() diff --git a/10-tests-et-qualite/exemples/05_02_refactorisation.py b/10-tests-et-qualite/exemples/05_02_refactorisation.py index 7e1102c..9a0335c 100644 --- a/10-tests-et-qualite/exemples/05_02_refactorisation.py +++ b/10-tests-et-qualite/exemples/05_02_refactorisation.py @@ -22,10 +22,6 @@ def isAdult(self): else:return False """ -import os -import sys -from datetime import datetime - def calculer_resultat(valeur_x, valeur_y, seuil): """Calcule un resultat base sur un seuil. @@ -42,8 +38,7 @@ def calculer_resultat(valeur_x, valeur_y, seuil): if resultat > seuil: return resultat - else: - return 0 + return 0 class Utilisateur: @@ -86,8 +81,3 @@ def est_majeur(self): print(f"\n{alice.nom} (age: {alice.age}) est majeur : {alice.est_majeur()}") print(f"{bob.nom} (age: {bob.age}) est majeur : {bob.est_majeur()}") - -# Utilisation correcte de datetime (au lieu de from datetime import *) -print(f"\nDate actuelle : {datetime.now().strftime('%Y-%m-%d %H:%M')}") -print(f"Python path : {sys.executable}") -print(f"Repertoire courant : {os.getcwd()}") diff --git a/10-tests-et-qualite/exemples/06_03_classes_typees.py b/10-tests-et-qualite/exemples/06_03_classes_typees.py index 4d5dc9c..ff062de 100644 --- a/10-tests-et-qualite/exemples/06_03_classes_typees.py +++ b/10-tests-et-qualite/exemples/06_03_classes_typees.py @@ -1,10 +1,11 @@ # ============================================================================ # Section 10.6 : Validation de types avec mypy # Description : Classes typees - annotations de classe, TypeAlias, -# Generic[T], classe comme type de parametre +# Generic[T], et syntaxe PEP 695 (type, class C[T]) # Fichier source : 06-validation-types-mypy.md # ============================================================================ +import sys from typing import TypeAlias, TypeVar, Generic # --- Classe avec annotations --- @@ -115,3 +116,45 @@ def remplacer(self, nouveau: T) -> None: boite_liste: Boite[list[int]] = Boite([1, 2, 3]) print(f"boite_liste.obtenir() = {boite_liste.obtenir()}") + + +# --- PEP 695 : syntaxe moderne (Python 3.12+) --- +print("\n=== PEP 695 (Python 3.12+) ===") + +# La syntaxe PEP 695 (mot-cle 'type', 'class C[T]', 'def f[T]') est une +# nouveaute SYNTAXIQUE : sur Python < 3.12 elle provoquerait une SyntaxError +# des le chargement du fichier. On l'isole donc dans une chaine executee +# seulement si l'interpreteur est assez recent ; le reste du fichier reste +# ainsi portable (3.10+). Le .md, lui, montre cette syntaxe directement +# (bloc illustratif non execute). +_demo_pep695 = ''' +type Identifiant = int + + +class BoiteMod[T]: + """Boite generique en syntaxe PEP 695.""" + + def __init__(self, contenu: T) -> None: + self.contenu = contenu + + def obtenir(self) -> T: + """Retourne le contenu.""" + return self.contenu + + +def premier[T](elements: list[T]) -> T: + """Retourne le premier element (fonction generique PEP 695).""" + return elements[0] + + +identifiant: Identifiant = 1 +boite_mod: BoiteMod[str] = BoiteMod("PEP 695") +print(f"identifiant (type Identifiant = int) = {identifiant}") +print(f"BoiteMod('PEP 695').obtenir() = {boite_mod.obtenir()}") +print(f"premier([10, 20, 30]) = {premier([10, 20, 30])}") +''' + +if sys.version_info >= (3, 12): + exec(_demo_pep695) +else: + print(" (syntaxe PEP 695 ignoree : necessite Python 3.12+)") diff --git a/10-tests-et-qualite/exemples/06_05_api_taches_typee.py b/10-tests-et-qualite/exemples/06_05_api_taches_typee.py index 9a49635..abc92b1 100644 --- a/10-tests-et-qualite/exemples/06_05_api_taches_typee.py +++ b/10-tests-et-qualite/exemples/06_05_api_taches_typee.py @@ -144,7 +144,7 @@ def exemple_utilisation() -> None: # Creer des taches tache1 = gestionnaire.creer_tache("Faire les courses", Priorite.HAUTE) - tache2 = gestionnaire.creer_tache("Lire un livre") + gestionnaire.creer_tache("Lire un livre") tache3 = gestionnaire.creer_tache("Faire du sport", Priorite.BASSE) print("=== Toutes les taches ===") diff --git a/10-tests-et-qualite/exemples/README.md b/10-tests-et-qualite/exemples/README.md index bc5350a..9cacb5a 100644 --- a/10-tests-et-qualite/exemples/README.md +++ b/10-tests-et-qualite/exemples/README.md @@ -1,18 +1,24 @@ -# Chapitre 10 - Tests et qualite : Exemples +# Chapitre 10 - Tests et qualité du code : Exemples -## Modules source (utilises par les tests) +Ce dossier contient les exemples exécutables du chapitre 10, numérotés selon la section du cours (`01_*` → 10.1, `02_*` → 10.2, … `06_*` → 10.6). Chaque fichier reprend un exemple du `.md` de la section correspondante. + +**Exécution** : selon le fichier, on utilise `unittest`, `pytest` ou l'exécution directe `python3` (voir la commande dans chaque tableau). Les tests `pytest` ajoutent `-o "addopts="` pour ignorer une éventuelle configuration globale du dépôt. + +> Les sorties annoncées (nombres de tests, pourcentages de couverture) ont été vérifiées avec pytest 9, coverage 7 et mypy sur Python 3.12. Tous les exemples s'exécutent de Python 3.10 à 3.14 (la démo PEP 695 de `06_03` n'apparaît qu'à partir de 3.12 ; elle est ignorée avec un message en deçà). Un seul exemple (`02_15_pytest_mock.py`) requiert le plugin `pytest-mock` (`pip install pytest-mock`). + +## Modules source (utilisés par les tests) | Fichier | Description | Section | |---------|-------------|---------| | `calculatrice.py` | Fonctions additionner, soustraire, diviser | 10.1 | | `utilisateur.py` | Classe Utilisateur (nom, email, actif) | 10.1 | | `panier.py` | Classe Panier d'achat (ajouter, total, vider) | 10.1 | -| `meteo.py` | Obtenir temperature et recommander vetements (API) | 10.2 | +| `meteo.py` | Obtenir température et recommander vêtements (API) | 10.2 | | `utilisateurs_db.py` | BaseDeDonnees et ServiceUtilisateur | 10.2 | | `evenements.py` | Classe Evenement avec gestion du temps | 10.2 | | `config.py` | Classe Configuration (chargement JSON) | 10.2 | | `notification.py` | ServiceNotification (envoi email SMTP) | 10.2 | -| `paiement.py` | ServicePaiement (API + base de donnees) | 10.2 | +| `paiement.py` | ServicePaiement (API + base de données) | 10.2 | --- @@ -20,12 +26,14 @@ Source : `01-tests-unitaires-unittest-pytest.md` -| Fichier | Description | Execution | Sortie attendue | +| Fichier | Description | Exécution | Sortie attendue | |---------|-------------|-----------|-----------------| -| `01_01_test_calculatrice_unittest.py` | Tests unittest pour la calculatrice (addition, soustraction, division, division par zero) | `python3 -m unittest 01_01_test_calculatrice_unittest -v` | 4 tests OK | +| `01_01_test_calculatrice_unittest.py` | Tests unittest pour la calculatrice (addition, soustraction, division, division par zéro) | `python3 -m unittest 01_01_test_calculatrice_unittest -v` | 4 tests OK | | `01_02_test_assertions_unittest.py` | Assertions unittest (assertEqual, assertNotEqual, assertTrue, assertIsNone, assertIn, assertGreater, assertAlmostEqual, setUp/tearDown) | `python3 -m unittest 01_02_test_assertions_unittest -v` | 9 tests OK | -| `01_03_test_utilisateur_unittest.py` | Tests unittest pour Utilisateur (creation, activation, desactivation, changement email, representation) | `python3 -m unittest 01_03_test_utilisateur_unittest -v` | 6 tests OK | -| `01_04_test_panier_unittest.py` | Tests unittest pour Panier (panier vide, ajout, total, prix negatif, quantite zero, vidage, totaux parametres) | `python3 -m unittest 01_04_test_panier_unittest -v` | 9 tests OK | +| `01_03_test_utilisateur_unittest.py` | Tests unittest pour Utilisateur (création, activation, désactivation, changement d'email, représentation) | `python3 -m unittest 01_03_test_utilisateur_unittest -v` | 6 tests OK | +| `01_04_test_panier_unittest.py` | Tests unittest pour Panier (panier vide, ajout, total, prix négatif, quantité zéro, vidage, totaux paramétrés) | `python3 -m unittest 01_04_test_panier_unittest -v` | 9 tests OK | +| `01_05_skip_xfail_pytest.py` | Marqueurs pytest pour contrôler l'exécution : `skip`, `skipif`, `xfail` (et `xpass`) | `pytest 01_05_skip_xfail_pytest.py -v -o "addopts="` | 2 passed, 1 skipped, 1 xfailed, 1 xpassed | +| `01_06_pytest_raises.py` | Tester les exceptions avec `pytest.raises` : exception levée, message via `match` (regex), inspection avec `as excinfo` | `pytest 01_06_pytest_raises.py -v -o "addopts="` | 4 tests OK | --- @@ -33,20 +41,23 @@ Source : `01-tests-unitaires-unittest-pytest.md` Source : `02-mocking-et-fixtures.md` -| Fichier | Description | Execution | Sortie attendue | +| Fichier | Description | Exécution | Sortie attendue | |---------|-------------|-----------|-----------------| -| `02_01_fixtures_pytest.py` | Fixtures pytest (basique, objet Compte, yield/teardown, scope, parametrees, dependantes) | `pytest 02_01_fixtures_pytest.py -v -o "addopts="` | 12 tests OK | +| `02_01_fixtures_pytest.py` | Fixtures pytest (basique, objet Compte, yield/teardown, scope, paramétrées, dépendantes) | `pytest 02_01_fixtures_pytest.py -v -o "addopts="` | 12 tests OK | | `02_02_fixtures_unittest.py` | Fixtures unittest (setUp/tearDown, setUpClass/tearDownClass) | `python3 -m unittest 02_02_fixtures_unittest -v` | 4 tests OK | -| `02_03_mock_bases.py` | Mock bases (creation, return_value, side_effect, exceptions) | `python3 02_03_mock_bases.py` | Affichage des appels mock, valeurs successives, exception capturee | -| `02_04_magicmock.py` | MagicMock (\_\_len\_\_, \_\_iter\_\_, \_\_eq\_\_, simulation de BDD) | `python3 02_04_magicmock.py` | len=5, iteration 1 2 3, eq=True, query=[{id:1}], len=10 | -| `02_05_mock_assertions.py` | Assertions sur mocks (called, call_count, assert_called_with, assert_has_calls, spec) | `python3 02_05_mock_assertions.py` | Verifications OK, spec AttributeError | -| `02_06_patch.py` | Patch (decorateur, context manager, patch.object, multi-patch) | `python3 02_06_patch.py` | Demonstrations de patch avec restauration | -| `02_07_test_meteo.py` | Tests mocking API meteo (requests.get mocke) | `pytest 02_07_test_meteo.py -v -o "addopts="` | 5 tests OK | -| `02_08_test_utilisateurs_db.py` | Tests mocking base de donnees (Mock spec, creer/obtenir utilisateur) | `pytest 02_08_test_utilisateurs_db.py -v -o "addopts="` | 3 tests OK | +| `02_03_mock_bases.py` | Mock : bases (création, return_value, side_effect, exceptions) | `python3 02_03_mock_bases.py` | Affichage des appels mock, valeurs successives, exception capturée | +| `02_04_magicmock.py` | MagicMock (\_\_len\_\_, \_\_iter\_\_, \_\_eq\_\_, simulation de BDD) | `python3 02_04_magicmock.py` | len=5, itération 1 2 3, eq=True, query=[{id:1}], len=10 | +| `02_05_mock_assertions.py` | Assertions sur mocks (called, call_count, assert_called_with, assert_has_calls, spec) | `python3 02_05_mock_assertions.py` | Vérifications OK, spec AttributeError | +| `02_06_patch.py` | Patch (décorateur, context manager, patch.object, multi-patch) | `python3 02_06_patch.py` | Démonstrations de patch avec restauration | +| `02_07_test_meteo.py` | Tests mocking API météo (requests.get mocké) | `pytest 02_07_test_meteo.py -v -o "addopts="` | 5 tests OK | +| `02_08_test_utilisateurs_db.py` | Tests mocking base de données (Mock spec, créer/obtenir utilisateur) | `pytest 02_08_test_utilisateurs_db.py -v -o "addopts="` | 3 tests OK | | `02_09_test_evenements.py` | Tests mocking du temps (patch datetime.now) | `pytest 02_09_test_evenements.py -v -o "addopts="` | 3 tests OK | | `02_10_test_config.py` | Tests mocking fichiers (mock_open, configuration JSON) | `pytest 02_10_test_config.py -v -o "addopts="` | 3 tests OK | -| `02_11_test_notification.py` | Tests mocking SMTP (envoi email, notification inscription) | `pytest 02_11_test_notification.py -v -o "addopts="` | 2 tests OK | -| `02_12_test_paiement.py` | Cas pratique complet : service de paiement (API + DB + datetime mockes) | `pytest 02_12_test_paiement.py -v -o "addopts="` | 5 tests OK | +| `02_11_test_notification.py` | Tests mocking SMTP (envoi email, notification d'inscription) | `pytest 02_11_test_notification.py -v -o "addopts="` | 2 tests OK | +| `02_12_test_paiement.py` | Cas pratique complet : service de paiement (API + DB + datetime mockés) | `pytest 02_12_test_paiement.py -v -o "addopts="` | 5 tests OK | +| `02_13_monkeypatch.py` | `monkeypatch` (setenv, delenv, setattr, setitem) avec restauration automatique | `pytest 02_13_monkeypatch.py -v -o "addopts="` | 4 tests OK | +| `02_14_conftest_demo/` | Partage de fixtures via `conftest.py` (sous-dossier : `compte.py`, `conftest.py`, `test_depot.py`) | `cd 02_14_conftest_demo && pytest -v -o "addopts="` | 2 tests OK (fixture trouvée sans import) | +| `02_15_pytest_mock.py` | `pytest-mock` : la fixture `mocker` (patch sans `with`/décorateur, `return_value`, `side_effect`, `spy`) — **nécessite `pip install pytest-mock`** | `pytest 02_15_pytest_mock.py -v -o "addopts="` | 3 tests OK | --- @@ -54,14 +65,14 @@ Source : `02-mocking-et-fixtures.md` Source : `03-couverture-de-code.md` -| Fichier | Description | Execution | Sortie attendue | +| Fichier | Description | Exécution | Sortie attendue | |---------|-------------|-----------|-----------------| -| `03_01_calculatrice_cov.py` | Calculatrice etendue (additionner, soustraire, multiplier, diviser, calculer_moyenne) | Module source | - | -| `03_02_test_calculatrice_cov.py` | Tests complets pour 100% de couverture (7 tests) | `pytest 03_02_test_calculatrice_cov.py --cov=03_01_calculatrice_cov --cov-branch -o "addopts="` | 7 tests OK, 100% couverture | +| `03_01_calculatrice_cov.py` | Calculatrice étendue (additionner, soustraire, multiplier, diviser, calculer_moyenne) | Module source | - | +| `03_02_test_calculatrice_cov.py` | Tests complets pour 100% de couverture (7 tests) | `pytest 03_02_test_calculatrice_cov.py --cov=03_01_calculatrice_cov --cov-branch -o "addopts="` | 7 tests OK, 100% de couverture | | `03_03_validation.py` | Module validation (valider_note, calculer_mention) avec branches | Module source | - | -| `03_04_test_validation.py` | Tests complets avec toutes les branches (9 tests) | `pytest 03_04_test_validation.py --cov=03_03_validation --cov-branch -o "addopts="` | 9 tests OK, 100% couverture branches | -| `03_05_utilisateur_cov.py` | Utilisateur etendu avec roles + GestionnaireUtilisateurs | Module source | - | -| `03_06_test_utilisateur_cov.py` | Tests complets (22 tests, toutes methodes et branches) | `pytest 03_06_test_utilisateur_cov.py --cov=03_05_utilisateur_cov --cov-branch -o "addopts="` | 22 tests OK, 100% couverture | +| `03_04_test_validation.py` | Tests complets avec toutes les branches (9 tests) | `pytest 03_04_test_validation.py --cov=03_03_validation --cov-branch -o "addopts="` | 9 tests OK, 100% de couverture des branches | +| `03_05_utilisateur_cov.py` | Utilisateur étendu avec rôles + GestionnaireUtilisateurs | Module source | - | +| `03_06_test_utilisateur_cov.py` | Tests complets (22 tests, toutes méthodes et branches) | `pytest 03_06_test_utilisateur_cov.py --cov=03_05_utilisateur_cov --cov-branch -o "addopts="` | 22 tests OK, 100% de couverture | --- @@ -69,13 +80,15 @@ Source : `03-couverture-de-code.md` Source : `04-documentation-docstrings.md` -| Fichier | Description | Execution | Sortie attendue | +| Fichier | Description | Exécution | Sortie attendue | |---------|-------------|-----------|-----------------| | `04_01_docstrings_bases.py` | Bases des docstrings (\_\_doc\_\_, help(), inspect.signature) | `python3 04_01_docstrings_bases.py` | Docstrings, signatures, help() | | `04_02_styles_docstrings.py` | 3 styles de docstrings (Google, NumPy, Sphinx) avec exemples fonctionnels | `python3 04_02_styles_docstrings.py` | diviser, calculer_statistiques, creer_utilisateur | -| `04_03_fonctions_documentees.py` | Fonctions documentees (est_pair, calculer_prix_total, analyser_texte, formater_prix) | `python3 04_03_fonctions_documentees.py` | Resultats de chaque fonction | -| `04_04_classes_documentees.py` | Classes documentees (CompteBancaire, Vehicule, Voiture avec heritage) | `python3 04_04_classes_documentees.py` | Depot/retrait, descriptions vehicules | -| `04_05_api_taches.py` | Cas pratique complet : API Tache + GestionnaireTaches | `python3 04_05_api_taches.py` | CRUD taches, priorites, compteurs | +| `04_03_fonctions_documentees.py` | Fonctions documentées (est_pair, calculer_prix_total, analyser_texte, formater_prix) | `python3 04_03_fonctions_documentees.py` | Résultats de chaque fonction | +| `04_04_classes_documentees.py` | Classes documentées (CompteBancaire, Vehicule, Voiture avec héritage) | `python3 04_04_classes_documentees.py` | Dépôt/retrait, descriptions des véhicules | +| `04_05_api_taches.py` | Cas pratique complet : API Tache + GestionnaireTaches (doctests inclus) | `python3 04_05_api_taches.py` | CRUD tâches, priorités, compteurs | + +> Astuce : les docstrings de `04_05_api_taches.py` contiennent des exemples `>>>`. On peut les exécuter comme des tests avec `python3 -m doctest 04_05_api_taches.py -v`. --- @@ -83,10 +96,12 @@ Source : `04-documentation-docstrings.md` Source : `05-pep8-et-linting.md` -| Fichier | Description | Execution | Sortie attendue | +| Fichier | Description | Exécution | Sortie attendue | |---------|-------------|-----------|-----------------| -| `05_01_pep8_regles.py` | Demonstration des regles PEP 8 (indentation, nommage, espaces, comparaisons) | `python3 05_01_pep8_regles.py` | Exemples de bon style PEP 8 | -| `05_02_refactorisation.py` | Cas pratique de refactorisation (code corrige selon PEP 8) | `python3 05_02_refactorisation.py` | Fonctions et classes refactorisees | +| `05_01_pep8_regles.py` | Démonstration des règles PEP 8 (indentation, nommage, espaces, comparaisons) | `python3 05_01_pep8_regles.py` | Exemples de bon style PEP 8 | +| `05_02_refactorisation.py` | Cas pratique de refactorisation (code corrigé selon PEP 8 : sans import inutilisé, sans `else` après `return`) | `python3 05_02_refactorisation.py` | Fonctions et classes refactorisées | + +> `05_02_refactorisation.py` est volontairement « propre » : `flake8` et `ruff check` ne signalent aucune erreur. --- @@ -94,34 +109,43 @@ Source : `05-pep8-et-linting.md` Source : `06-validation-types-mypy.md` -| Fichier | Description | Execution | Sortie attendue | +| Fichier | Description | Exécution | Sortie attendue | |---------|-------------|-----------|-----------------| -| `06_01_types_bases.py` | Types de base (typage dynamique, type hints, variables annotees) | `python3 06_01_types_bases.py` | Types, fonctions typees, TypeError | -| `06_02_types_complexes.py` | Types complexes (list, dict, Optional, Union, Callable, Iterable, Sequence, Mapping) | `python3 06_02_types_complexes.py` | Collections typees, fonctions generiques | -| `06_03_classes_typees.py` | Classes typees (annotations, TypeAlias, Generic[T]) | `python3 06_03_classes_typees.py` | Utilisateur, Boite generique | -| `06_04_types_avances.py` | Types avances (Literal, TypedDict, Final, Protocol) | `python3 06_04_types_avances.py` | Demonstrations de chaque type avance | -| `06_05_api_taches_typee.py` | Cas pratique complet : API taches avec types (Enum, TypedDict) | `python3 06_05_api_taches_typee.py` | CRUD taches typees, statistiques | +| `06_01_types_bases.py` | Types de base (typage dynamique, type hints, variables annotées) | `python3 06_01_types_bases.py` | Types, fonctions typées, TypeError | +| `06_02_types_complexes.py` | Types complexes (list, dict, Optional, Union, Callable, Iterable, Sequence, Mapping) | `python3 06_02_types_complexes.py` | Collections typées, fonctions génériques | +| `06_03_classes_typees.py` | Classes typées (annotations, TypeAlias, Generic[T]) **et syntaxe PEP 695** (`type`, `class C[T]`, 3.12+) | `python3 06_03_classes_typees.py` | Utilisateur, Boite générique, variante PEP 695 (exécutée sur Python 3.12+ ; ignorée avec un message en deçà) | +| `06_04_types_avances.py` | Types avancés (Literal, TypedDict, Final, Protocol) | `python3 06_04_types_avances.py` | Démonstrations de chaque type avancé | +| `06_05_api_taches_typee.py` | Cas pratique complet : API tâches avec types (Enum, TypedDict) | `python3 06_05_api_taches_typee.py` | CRUD tâches typées, statistiques | + +> Les fichiers `06_02` à `06_05` passent `mypy` sans erreur. `06_01_types_bases.py` contient **volontairement** une fonction non typée (`additionner_dynamique`) pour illustrer le typage dynamique : `mypy` la signale sous `disallow_untyped_defs` (configuration du dépôt), ce qui est attendu. --- -## Execution +## Exécution groupée ```bash -# Executer tous les tests unittest (section 01) -python3 -m unittest discover -p "01_*" -v +# Tous les tests des sections 01 à 03 en une fois. +# (pytest collecte aussi les classes unittest ; `unittest discover` ne peut +# pas importer ces fichiers à cause de leur préfixe numérique — utiliser +# alors la commande explicite par fichier indiquée dans les tableaux.) +pytest 01_*.py 02_*.py 03_*.py -v -o "addopts=" -# Executer tous les tests pytest (sections 02-03) -pytest 02_*.py 03_*.py -v -o "addopts=" +# Démonstration conftest.py (sous-dossier isolé) +(cd 02_14_conftest_demo && pytest -v -o "addopts=") -# Executer les demos (sections 02-06) +# Démos en exécution directe (sections 02 à 06) for f in 02_03*.py 02_04*.py 02_05*.py 02_06*.py 04_*.py 05_*.py 06_*.py; do echo "--- $f ---" python3 "$f" echo done -# Couverture complete +# Couverture complète (sections 03) pytest 03_02_test_calculatrice_cov.py 03_04_test_validation.py 03_06_test_utilisateur_cov.py \ --cov=03_01_calculatrice_cov --cov=03_03_validation --cov=03_05_utilisateur_cov \ --cov-branch --cov-report=term-missing -o "addopts=" + +# Vérification de types (section 06) +mypy 06_01_types_bases.py 06_02_types_complexes.py 06_03_classes_typees.py \ + 06_04_types_avances.py 06_05_api_taches_typee.py ``` diff --git a/11-developpement-web-et-apis/02-fastapi-framework-moderne.md b/11-developpement-web-et-apis/02-fastapi-framework-moderne.md index 12d9950..25d8996 100644 --- a/11-developpement-web-et-apis/02-fastapi-framework-moderne.md +++ b/11-developpement-web-et-apis/02-fastapi-framework-moderne.md @@ -46,9 +46,11 @@ FastAPI génère deux interfaces de documentation : - **Swagger UI** (accessible via `/docs`) - Interface moderne et interactive - **ReDoc** (accessible via `/redoc`) - Documentation alternative plus épurée +> **Comment cette « magie » fonctionne-t-elle ?** FastAPI lit les **annotations de type** de vos routes et de vos modèles Pydantic, et en déduit un document **OpenAPI** : un fichier JSON standard (servi sur `/openapi.json`) qui décrit toute votre API — chemins, paramètres, formats d'entrée/sortie, codes d'erreur. Ce document unique alimente ensuite les deux interfaces `/docs` (Swagger UI) et `/redoc`, qui ne sont que des **visualiseurs** de ce schéma. Vous n'écrivez donc jamais la documentation : elle est *déduite* de vos types. Changez une annotation, et la doc se met à jour instantanément. + ### 4. Type hints Python natifs -FastAPI exploite pleinement les **annotations de type** Python (type hints) introduites dans Python 3.6+. Cela signifie que vous utilisez la syntaxe Python standard, et FastAPI fait le reste : +FastAPI exploite pleinement les **annotations de type** Python (type hints) standardisées par la PEP 484 (Python 3.5). Cela signifie que vous utilisez la syntaxe Python standard, et FastAPI fait le reste : ```python @app.get("/items/{item_id}") @@ -104,7 +106,7 @@ Si vous avez déjà de l'expérience, FastAPI offre : - ⚡ **Performances de production** : Prêt pour des applications à haute charge - 🔧 **Flexibilité** : Architecture modulaire et extensible - 🛡️ **Sécurité** : Mécanismes de sécurité intégrés -- 📊 **Type safety** : Détection des erreurs à la compilation +- 📊 **Type safety** : Détection des erreurs en amont, *avant* l'exécution (analyse statique des types, par exemple avec mypy) — Python n'a pas de phase de compilation au sens de C ou Java - 🔄 **Async/await** : Support complet de la programmation asynchrone ### Pour les projets professionnels @@ -309,7 +311,7 @@ FastAPI est idéal comme backend pour applications iOS/Android : ### Popularité croissante FastAPI a connu une croissance explosive depuis sa sortie : -- ⭐ Plus de 70 000 étoiles sur GitHub +- ⭐ Près de 100 000 étoiles sur GitHub (il rivalise désormais avec Django, le framework historique) - 📈 Adoption rapide par les entreprises - 📚 Documentation traduite en plusieurs langues - 🎓 Nombreux tutoriels et cours @@ -481,7 +483,7 @@ Tout ce dont vous avez besoin sera expliqué au fur et à mesure ! Pour aller plus loin, voici les ressources officielles de FastAPI : - **Documentation officielle :** https://fastapi.tiangolo.com/ -- **Code source (GitHub) :** https://github.com/tiangolo/fastapi +- **Code source (GitHub) :** https://github.com/fastapi/fastapi - **Tutoriel officiel :** https://fastapi.tiangolo.com/tutorial/ - **Guide utilisateur :** https://fastapi.tiangolo.com/tutorial/ - **Référence API :** https://fastapi.tiangolo.com/reference/ diff --git a/11-developpement-web-et-apis/02.1-installation-premier-projet-fastapi.md b/11-developpement-web-et-apis/02.1-installation-premier-projet-fastapi.md index b530fb1..8bb7b8a 100644 --- a/11-developpement-web-et-apis/02.1-installation-premier-projet-fastapi.md +++ b/11-developpement-web-et-apis/02.1-installation-premier-projet-fastapi.md @@ -93,12 +93,16 @@ Pensez à Uvicorn comme le moteur qui fait fonctionner votre voiture (FastAPI). Installez FastAPI et Uvicorn avec pip : ```bash -pip install fastapi uvicorn[standard] +pip install "fastapi[standard]" ``` -Cette commande installe : -- `fastapi` : Le framework FastAPI lui-même -- `uvicorn[standard]` : Le serveur ASGI avec des fonctionnalités supplémentaires optimales +Cette commande (la **méthode recommandée** aujourd'hui) installe FastAPI et ses dépendances *standard* : +- le **framework FastAPI** lui-même ; +- **Uvicorn**, le serveur ASGI qui fera tourner votre application ; +- la **commande `fastapi`** en ligne de commande (la *FastAPI CLI*, voir l'étape 4) ; +- quelques outils pratiques (validation d'emails, support des formulaires, etc.). + +> Les guillemets `"..."` autour de `fastapi[standard]` sont recommandés : sans eux, certains terminaux (comme zsh sur macOS) interprètent les crochets `[ ]` et l'installation échoue. L'installation peut prendre quelques instants. Une fois terminée, vous êtes prêt à créer votre première application ! @@ -209,6 +213,23 @@ INFO: Application startup complete. Félicitations ! 🎉 Votre serveur est lancé et écoute sur `http://127.0.0.1:8000`. +### Alternative moderne : la commande `fastapi` + +Depuis 2024, FastAPI fournit sa propre **interface en ligne de commande** (la *FastAPI CLI*, installée avec `fastapi[standard]`). Elle détecte automatiquement votre objet `app` et lance Uvicorn pour vous — plus besoin de préciser `main:app` : + +```bash +# Mode développement (équivalent de `uvicorn main:app --reload`) +fastapi dev main.py + +# Mode production (sans rechargement, écoute sur toutes les interfaces) +fastapi run main.py +``` + +- **`fastapi dev`** : pour *développer*. Le rechargement automatique est activé et le serveur n'écoute que sur `127.0.0.1` (votre machine uniquement). +- **`fastapi run`** : pour la *production*. Pas de rechargement, et le serveur écoute sur `0.0.0.0` (accessible depuis l'extérieur, par exemple dans un conteneur Docker). + +C'est désormais la méthode mise en avant par la documentation officielle. Sous le capot, ces deux commandes utilisent **toujours Uvicorn** : `uvicorn main:app --reload` reste donc parfaitement valide — et utile à connaître pour contrôler finement les options du serveur (port, hôte, nombre de *workers*, etc.). + ## Étape 5 : Tester votre API Il y a plusieurs façons de tester votre API. Voyons les principales. diff --git a/11-developpement-web-et-apis/02.2-routes-et-validation-pydantic.md b/11-developpement-web-et-apis/02.2-routes-et-validation-pydantic.md index 30e0539..c515e85 100644 --- a/11-developpement-web-et-apis/02.2-routes-et-validation-pydantic.md +++ b/11-developpement-web-et-apis/02.2-routes-et-validation-pydantic.md @@ -264,7 +264,7 @@ pip install email-validator ### Autres types spéciaux Pydantic ```python -from pydantic import BaseModel, HttpUrl, conint, constr +from pydantic import BaseModel, EmailStr, HttpUrl, conint, constr class Profil(BaseModel): nom: constr(min_length=2, max_length=50) # String avec contraintes @@ -285,8 +285,8 @@ class ModificationUtilisateur(BaseModel): @app.put("/utilisateurs/{utilisateur_id}") def modifier_utilisateur( utilisateur_id: int, # Paramètre de chemin - notifier: bool = False, # Paramètre de requête - utilisateur: ModificationUtilisateur # Corps de requête + utilisateur: ModificationUtilisateur, # Corps de requête + notifier: bool = False # Paramètre de requête ): return { "utilisateur_id": utilisateur_id, @@ -475,28 +475,36 @@ FastAPI renverra : { "detail": [ { + "type": "string_too_short", "loc": ["body", "nom"], - "msg": "ensure this value has at least 2 characters", - "type": "value_error.any_str.min_length" + "msg": "String should have at least 2 characters", + "input": "A", + "ctx": {"min_length": 2} }, { + "type": "greater_than_equal", "loc": ["body", "age"], - "msg": "ensure this value is greater than or equal to 0", - "type": "value_error.number.not_ge" + "msg": "Input should be greater than or equal to 0", + "input": -5, + "ctx": {"ge": 0} }, { + "type": "value_error", "loc": ["body", "email"], - "msg": "value is not a valid email address", - "type": "value_error.email" + "msg": "value is not a valid email address: An email address must have an @-sign.", + "input": "pas-un-email", + "ctx": {"reason": "An email address must have an @-sign."} } ] } ``` Chaque erreur indique : +- `type` : Le type d'erreur (ex. `string_too_short`, `greater_than_equal`) - `loc` : L'emplacement du champ erroné -- `msg` : Le message d'erreur -- `type` : Le type d'erreur +- `msg` : Le message d'erreur lisible +- `input` : La valeur reçue qui a échoué la validation +- `ctx` : Le contexte de la contrainte (ex. la longueur minimale attendue) ## Paramètres de requête avec validation diff --git a/11-developpement-web-et-apis/02.3-endpoints-asynchrones.md b/11-developpement-web-et-apis/02.3-endpoints-asynchrones.md index 7ff10c3..7f78f3a 100644 --- a/11-developpement-web-et-apis/02.3-endpoints-asynchrones.md +++ b/11-developpement-web-et-apis/02.3-endpoints-asynchrones.md @@ -96,6 +96,12 @@ Si 3 clients font une requête en même temps : C'est beaucoup plus rapide ! 🚀 +> **Nuance importante (à lire !).** L'exemple ci-dessus est volontairement simplifié. En réalité, FastAPI **n'exécute pas les fonctions `def` synchrones de façon séquentielle** : il les délègue à un **pool de threads**. Les trois requêtes vers la version `def` tourneraient donc, elles aussi, en parallèle (≈ 2 secondes, et non 6), chacune dans son propre thread. +> +> La vraie différence est ailleurs : ce pool de threads est **limité** (40 threads par défaut), alors qu'un endpoint `async def` bien écrit gère des **milliers** de connexions simultanées sur un **seul** thread. Pour des opérations d'entrée/sortie à forte concurrence, `async` passe donc bien mieux à l'échelle. +> +> Le piège à *vraiment* éviter est l'inverse : placer du code **bloquant** (comme `time.sleep` ou `requests`) dans un `async def`. Là, l'event loop est figé pour toutes les requêtes, et on retombe — pour de bon cette fois — sur les 6 secondes (voir la section « Pièges courants » plus bas). + ## Les mots-clés async et await La programmation asynchrone en Python utilise deux mots-clés principaux : diff --git a/11-developpement-web-et-apis/03-flask-micro-framework.md b/11-developpement-web-et-apis/03-flask-micro-framework.md index 2b16277..677f7a1 100644 --- a/11-developpement-web-et-apis/03-flask-micro-framework.md +++ b/11-developpement-web-et-apis/03-flask-micro-framework.md @@ -84,10 +84,10 @@ C'est tout ! Flask est installé et prêt à l'emploi. Le package est léger et ### Vérifier l'installation ```bash -python -c "import flask; print(flask.__version__)" +pip show flask ``` -Vous devriez voir la version de Flask s'afficher (par exemple, 3.0.0). +La version installée de Flask apparaîtra sur la ligne `Version:` (par exemple, `Version: 3.1.0`). On évite `flask.__version__`, **déprécié depuis Flask 3.1** (et retiré en 3.2) ; pour récupérer la version depuis du code Python, la méthode pérenne est `importlib.metadata.version("flask")`. ## Votre première application Flask @@ -1248,7 +1248,7 @@ gunicorn -w 4 -b 0.0.0.0:8000 app:app ### Avec Docker ```dockerfile -FROM python:3.11-slim +FROM python:3.12-slim WORKDIR /app @@ -1268,9 +1268,11 @@ Créez un fichier `.env` : ``` SECRET_KEY=votre-clé-secrète DATABASE_URL=postgresql://user:pass@localhost/db -FLASK_ENV=production +FLASK_DEBUG=0 ``` +> **Note** : la variable `FLASK_ENV` (ex. `FLASK_ENV=production`) a été **supprimée dans Flask 2.3**. Le mode debug se contrôle désormais via `FLASK_DEBUG` (`1` pour activer, `0` ou absent en production) ou l'option `--debug` de la commande `flask run`. + Chargez-le avec `python-dotenv` : ```python from dotenv import load_dotenv diff --git a/11-developpement-web-et-apis/04-requetes-http-requests.md b/11-developpement-web-et-apis/04-requetes-http-requests.md index 695cc4f..12b242f 100644 --- a/11-developpement-web-et-apis/04-requetes-http-requests.md +++ b/11-developpement-web-et-apis/04-requetes-http-requests.md @@ -617,6 +617,8 @@ response3 = session.get('https://api.example.com/comments') 2. **Persistance des cookies** : Maintient automatiquement les cookies 3. **Configuration partagée** : Headers, auth, etc. définis une fois +> **Pourquoi est-ce « plus rapide » ?** Sans session, chaque appel à `requests.get(...)` ouvre une **nouvelle connexion** TCP (et, en HTTPS, refait toute la poignée de main TLS), puis la referme — un coût non négligeable, surtout répété. Une `Session` garde la connexion **ouverte** et la **réutilise** pour les requêtes suivantes vers le même hôte (mécanisme *keep-alive*, ou *connection pooling*). Sur une série d'appels à la même API, le gain de temps est très net. + ### Exemple complet avec session ```python diff --git a/11-developpement-web-et-apis/05-creation-consommation-apis-rest.md b/11-developpement-web-et-apis/05-creation-consommation-apis-rest.md index dc7b512..f8d8604 100644 --- a/11-developpement-web-et-apis/05-creation-consommation-apis-rest.md +++ b/11-developpement-web-et-apis/05-creation-consommation-apis-rest.md @@ -311,8 +311,10 @@ class Commentaire(CommentaireBase): ### Configuration base de données (database.py) ```python -from sqlalchemy import create_engine -from sqlalchemy.orm import declarative_base, sessionmaker +from sqlalchemy import ( + create_engine, Column, Integer, String, Text, DateTime, ForeignKey, JSON +) +from sqlalchemy.orm import declarative_base, relationship, sessionmaker # URL de la base de données SQLite SQLALCHEMY_DATABASE_URL = "sqlite:///./blog.db" @@ -329,6 +331,41 @@ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) # Base pour les modèles Base = declarative_base() + +# Modèles SQLAlchemy (les tables de la base de données) +class User(Base): + __tablename__ = "utilisateurs" + id = Column(Integer, primary_key=True) + nom = Column(String(100), nullable=False) + email = Column(String(100), unique=True, nullable=False) + bio = Column(Text, nullable=True) + mot_de_passe = Column(String(255), nullable=False) + date_inscription = Column(DateTime, nullable=False) + + +class Article(Base): + __tablename__ = "articles" + id = Column(Integer, primary_key=True) + titre = Column(String(200), nullable=False) + contenu = Column(Text, nullable=False) + categorie = Column(String(100), nullable=False) + tags = Column(JSON, default=list) # liste de chaînes stockée en JSON + auteur_id = Column(Integer, ForeignKey("utilisateurs.id"), nullable=False) + date_publication = Column(DateTime, nullable=False) + nombre_vues = Column(Integer, default=0) + + auteur = relationship("User") + + +class Commentaire(Base): + __tablename__ = "commentaires" + id = Column(Integer, primary_key=True) + contenu = Column(String(500), nullable=False) + article_id = Column(Integer, ForeignKey("articles.id"), nullable=False) + auteur_id = Column(Integer, ForeignKey("utilisateurs.id"), nullable=False) + date_creation = Column(DateTime, nullable=False) + + # Dépendance pour obtenir la session de base de données def get_db(): db = SessionLocal() @@ -338,6 +375,32 @@ def get_db(): db.close() ``` +> **Où est créé le fichier `blog.db` ?** L'appel `create_engine(...)` ne crée **aucun fichier** : la connexion SQLite est *paresseuse*. Le fichier `blog.db` n'apparaît qu'à la **première connexion**, déclenchée par `Base.metadata.create_all(...)` au démarrage de `main.py` (voir plus bas). Son chemin `./blog.db` est **relatif au répertoire de travail** d'où vous lancez l'application (par exemple `uvicorn main:app`), et non au dossier des fichiers sources. + +> **Important** : ne confondez pas les **modèles SQLAlchemy** (`User`, `Article`, `Commentaire` ci-dessus — les *tables* de la base) et les **modèles Pydantic** (`Utilisateur`, `Article`… définis dans `models.py` — la *validation* et la *sérialisation* des requêtes/réponses). Ils portent des noms voisins mais vivent dans des modules différents. FastAPI fait le pont entre les deux grâce à `from_attributes=True` (côté Pydantic), qui permet de renvoyer directement un objet SQLAlchemy comme `response_model`. + +### Le système de dépendances (`Depends`) + +Dans l'application ci-dessous, chaque endpoint reçoit un paramètre `db: Session = Depends(get_db)`. C'est le **système de dépendances** de FastAPI, l'un de ses mécanismes les plus puissants — il mérite qu'on s'y arrête avant d'aller plus loin. + +Une **dépendance** est une fonction dont FastAPI **exécute le résultat à votre place** avant d'appeler votre endpoint, puis **injecte** ce résultat dans le paramètre correspondant. Concrètement, à chaque requête sur un endpoint qui déclare `db: Session = Depends(get_db)` : + +1. FastAPI **appelle `get_db()`** ; +2. il récupère la valeur **fournie par `yield`** (ici, la session SQLAlchemy) ; +3. il la passe à votre fonction via le paramètre `db` ; +4. une fois la réponse renvoyée, il **reprend `get_db()` après le `yield`** pour exécuter le nettoyage (`finally: db.close()`). + +Le `yield` (plutôt qu'un simple `return`) est la clé : ce qui le précède est du **code de préparation** (ouvrir la session), ce qui le suit est du **code de nettoyage** (fermer la session), garanti même si l'endpoint lève une exception — exactement comme un gestionnaire de contexte (`with`). + +**Pourquoi est-ce si utile ?** +- **Pas de répétition** : la logique « ouvrir puis fermer une session » est écrite **une seule fois** dans `get_db`, et réutilisée par tous les endpoints. +- **Testabilité** : on peut remplacer une dépendance dans les tests via `app.dependency_overrides[get_db] = ...` (par exemple pour brancher une base de test). +- **Composable** : une dépendance peut elle-même dépendre d'autres dépendances (authentification, autorisations, pagination…). C'est ainsi qu'est construit `verifier_token`, plus loin dans ce chapitre, qui sert à protéger des endpoints. + +> **Au passage**, deux autres paramètres apparaissent dans le décorateur `@app.post(...)` : +> - `status_code=status.HTTP_201_CREATED` : fixe le code de statut renvoyé en cas de succès. L'objet `status` (importé de `fastapi`) n'est qu'un ensemble de **constantes lisibles** — `status.HTTP_201_CREATED` vaut simplement `201`. +> - `tags=["Utilisateurs"]` : **regroupe** les endpoints par thème dans la documentation interactive `/docs`, pour s'y retrouver plus facilement. + ### Application principale (main.py) ```python @@ -346,6 +409,7 @@ from sqlalchemy.orm import Session from datetime import datetime import models import database +from database import User, Article, Commentaire app = FastAPI( title="Blog API", @@ -353,7 +417,9 @@ app = FastAPI( version="1.0.0" ) -# Créer les tables +# Créer les tables au démarrage de l'application. C'est précisément ici, +# à la première connexion, que SQLite crée le fichier blog.db, dans le +# répertoire courant (celui d'où l'application est lancée). database.Base.metadata.create_all(bind=database.engine) # ==================== UTILISATEURS ==================== @@ -492,7 +558,7 @@ def creer_article( titre=article.titre, contenu=article.contenu, categorie=article.categorie, - tags=",".join(article.tags), # Stocker comme chaîne + tags=article.tags, auteur_id=auteur_id, date_publication=datetime.now(), nombre_vues=0 @@ -1167,13 +1233,15 @@ origins = [ app.add_middleware( CORSMiddleware, - allow_origins=origins, # Ou ["*"] pour tout autoriser (développement uniquement) + allow_origins=origins, # listez explicitement les origines — pas "*" (voir l'avertissement ci-dessous) allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) ``` +> ⚠️ **Piège classique : `"*"` et identifiants ne font pas bon ménage.** La spécification CORS **interdit** de combiner `allow_origins=["*"]` (tout autoriser) avec `allow_credentials=True`. Si vous le faites, les navigateurs **rejettent** toute requête transportant des identifiants — cookies de session, ou en-tête `Authorization` (**donc votre token JWT !**). Votre frontend recevrait alors une erreur CORS dès qu'il tente de s'authentifier. Pour autoriser les requêtes authentifiées, vous **devez** lister les origines explicitement (comme `origins` ci-dessus) ; le joker `"*"` n'est acceptable que pour une API **publique, sans authentification**. + ## Rate Limiting Limiter le nombre de requêtes pour éviter les abus : @@ -1227,7 +1295,7 @@ def verifier_token(credentials: HTTPAuthorizationCredentials = Depends(security) status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expiré" ) - except jwt.JWTError: + except jwt.InvalidTokenError: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Token invalide" @@ -1437,6 +1505,8 @@ class UtilisateurReponse(BaseModel): # Loggez les accès ``` +> 🔒 **Ne stockez jamais un mot de passe en clair.** Dans l'exemple `creer_utilisateur` plus haut, le mot de passe était enregistré tel quel (commentaire « À hasher ! ») — en production, c'est **dangereux** : une fuite de la base compromettrait immédiatement tous vos utilisateurs. On stocke donc un **hash** : une empreinte *irréversible* calculée avec un algorithme **lent et salé**, conçu pour les mots de passe — `bcrypt`, `argon2` ou `scrypt` (surtout pas un simple `md5`/`sha256`, bien trop rapides à forcer). En pratique, la bibliothèque `passlib` (`pip install "passlib[bcrypt]"`) fournit `CryptContext.hash(mot_de_passe)` à l'inscription et `CryptContext.verify(saisi, hash_stocké)` à la connexion. Le hash ainsi obtenu (60 caractères pour bcrypt) remplace `mot_de_passe` dans la table. + ## Récapitulatif Dans cette section, vous avez appris : diff --git a/11-developpement-web-et-apis/06.1-introduction-sqlalchemy.md b/11-developpement-web-et-apis/06.1-introduction-sqlalchemy.md index 6cef14d..26a7e34 100644 --- a/11-developpement-web-et-apis/06.1-introduction-sqlalchemy.md +++ b/11-developpement-web-et-apis/06.1-introduction-sqlalchemy.md @@ -30,6 +30,8 @@ users = session.query(User).filter(User.age > 18).all() - Validation automatique des données - Gestion simplifiée des relations entre tables +> **Pourquoi l'ORM protège-t-il des injections SQL ?** Une **injection SQL** survient quand on **concatène** une valeur fournie par l'utilisateur directement dans une requête : `f"SELECT * FROM users WHERE nom = '{nom}'"`. Si un attaquant saisit `nom = "'; DROP TABLE users; --"`, la chaîne devient une *autre* requête, exécutée telle quelle. La parade s'appelle les **requêtes paramétrées** : la valeur est transmise **séparément** du texte SQL (via un emplacement `?` ou `:nom`), si bien que le moteur la traite toujours comme une simple **donnée**, jamais comme du code. SQLAlchemy construit **systématiquement** des requêtes paramétrées sous le capot — `filter(User.nom == nom)` devient `WHERE nom = ?` avec la valeur passée à part. Comme vous n'écrivez jamais de chaîne SQL vous-même, l'injection devient *structurellement* impossible : c'est cela, la « protection automatique ». + ## Installation Pour installer SQLAlchemy, utilisez pip : @@ -150,6 +152,35 @@ Base.metadata.create_all(engine) - `unique=True` : Chaque valeur doit être unique dans la table - `__repr__` : Méthode spéciale pour afficher l'objet de façon lisible +### Aller plus loin : le style moderne de SQLAlchemy 2.0 (optionnel) + +Ce cours utilise la syntaxe `Column(...)` avec `declarative_base()`, qui reste **pleinement supportée** par SQLAlchemy 2.0 (aucun avertissement de dépréciation) et la plus simple pour débuter. Depuis SQLAlchemy 2.0 (2023), une syntaxe **typée** est toutefois recommandée pour les nouveaux projets, basée sur `DeclarativeBase`, `Mapped[...]` et `mapped_column(...)` : + +```python +from sqlalchemy import String, select +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column + +class Base(DeclarativeBase): + pass + +class User(Base): + __tablename__ = 'users' + id: Mapped[int] = mapped_column(primary_key=True) + nom: Mapped[str] = mapped_column(String(50)) + email: Mapped[str | None] = mapped_column(String(100), unique=True) +``` + +Les annotations `Mapped[...]` rendent les modèles compatibles avec les vérificateurs de types (mypy). Côté requêtes, le style 2.0 privilégie `select()` exécuté via `session.execute(...)` plutôt que `session.query(...)` (qui demeure disponible) : + +```python +# Style moderne 2.0 +users = session.execute( + select(User).where(User.nom == "Alice") +).scalars().all() +``` + +Les deux styles fonctionnent avec SQLAlchemy 2.0 ; ce chapitre conserve `Column` et `session.query()` pour leur simplicité pédagogique. + ## Types de colonnes courants SQLAlchemy offre de nombreux types de colonnes : diff --git a/11-developpement-web-et-apis/06.3-requetes-et-migrations.md b/11-developpement-web-et-apis/06.3-requetes-et-migrations.md index e4a77d5..a0e71af 100644 --- a/11-developpement-web-et-apis/06.3-requetes-et-migrations.md +++ b/11-developpement-web-et-apis/06.3-requetes-et-migrations.md @@ -214,12 +214,14 @@ livres = session.query(Livre).limit(5).all() livres = session.query(Livre).offset(10).limit(5).all() # Pagination : page 3, avec 10 résultats par page -page = 3 -par_page = 10 -livres = session.query(Livre)\ - .offset((page - 1) * par_page)\ - .limit(par_page)\ +page = 3 +par_page = 10 +livres = ( + session.query(Livre) + .offset((page - 1) * par_page) + .limit(par_page) .all() +) # Récupérer un seul résultat # .first() : retourne le premier ou None @@ -310,7 +312,7 @@ livres = session.query(Livre)\ ### Agrégations et fonctions -SQLAlchemy permet d'utiliser des fonctions d'agrégation SQL. +SQLAlchemy permet d'utiliser des fonctions d'agrégation SQL. Comme ces fonctions ne renvoient qu'**une seule valeur** (un total, une moyenne…), on termine la requête par **`.scalar()`** : cette méthode extrait la valeur unique (première colonne de la première ligne) au lieu de retourner une liste de lignes. ```python from sqlalchemy import func diff --git a/11-developpement-web-et-apis/exemples/02_03_routes_validation_pydantic.py b/11-developpement-web-et-apis/exemples/02_03_routes_validation_pydantic.py index 6d79088..bbf5589 100644 --- a/11-developpement-web-et-apis/exemples/02_03_routes_validation_pydantic.py +++ b/11-developpement-web-et-apis/exemples/02_03_routes_validation_pydantic.py @@ -149,12 +149,12 @@ def rechercher( @app.put("/utilisateurs/{utilisateur_id}") def modifier_utilisateur( utilisateur_id: int, - notifier: bool = False, - utilisateur: ModificationUtilisateur = None + utilisateur: ModificationUtilisateur, + notifier: bool = False ): return { "utilisateur_id": utilisateur_id, - "modifications": utilisateur.model_dump() if utilisateur else {}, + "modifications": utilisateur.model_dump(), "notification_envoyee": notifier } diff --git a/11-developpement-web-et-apis/exemples/04_01_requetes_http_requests.py b/11-developpement-web-et-apis/exemples/04_01_requetes_http_requests.py index 0ccf08b..7738767 100644 --- a/11-developpement-web-et-apis/exemples/04_01_requetes_http_requests.py +++ b/11-developpement-web-et-apis/exemples/04_01_requetes_http_requests.py @@ -86,8 +86,6 @@ def slow_endpoint(): @api.get("/echo-headers") def echo_headers(request_obj=None): - from starlette.requests import Request - from fastapi import Request as FRequest # Access via dependency return {"note": "headers echoes"} @@ -178,7 +176,7 @@ def start_server(): } response = requests.get(f"{BASE}/users", headers=headers) print(f" Status: {response.status_code}") - print(f" Request headers envoyes: User-Agent, Accept, X-Custom-Header") + print(" Request headers envoyes: User-Agent, Accept, X-Custom-Header") # === Session (partage headers/cookies) === print("\n=== Session requests ===") diff --git a/11-developpement-web-et-apis/exemples/05_01_api_rest_complete.py b/11-developpement-web-et-apis/exemples/05_01_api_rest_complete.py index 0aeefbf..694c365 100644 --- a/11-developpement-web-et-apis/exemples/05_01_api_rest_complete.py +++ b/11-developpement-web-et-apis/exemples/05_01_api_rest_complete.py @@ -8,7 +8,7 @@ """API REST complete pour un blog avec client de consommation.""" -from fastapi import FastAPI, HTTPException, Query, Path, status +from fastapi import FastAPI, HTTPException, Query, status from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel, Field, field_validator from fastapi.testclient import TestClient diff --git a/11-developpement-web-et-apis/exemples/05_02_api_rest_sqlalchemy.py b/11-developpement-web-et-apis/exemples/05_02_api_rest_sqlalchemy.py new file mode 100644 index 0000000..a9f3412 --- /dev/null +++ b/11-developpement-web-et-apis/exemples/05_02_api_rest_sqlalchemy.py @@ -0,0 +1,622 @@ +# ============================================================================ +# Section 11.5 : Creation et consommation d'APIs REST (version SQLAlchemy) +# Description : API REST complete pour un blog, adossee a une base de +# donnees via SQLAlchemy ORM (utilisateurs, articles, +# commentaires) : CRUD, pagination, filtrage, tri, CORS, +# erreurs 404/409/422, client de consommation. +# Fichier source : 05-creation-consommation-apis-rest.md +# ============================================================================ + +"""API REST complete pour un blog, adossee a SQLAlchemy (version persistante). + +Cet exemple reproduit en UN SEUL fichier l'API SQLAlchemy decrite dans le +chapitre. Le cours repartit ce code en plusieurs modules : + + models.py -> les modeles Pydantic (validation / serialisation) + database.py -> le moteur, la session et les modeles SQLAlchemy (tables) + main.py -> l'application FastAPI et les routes + +et utilise une base fichier "sqlite:///./blog.db". Ici, on emploie une base +SQLite EN MEMOIRE avec StaticPool : l'unique connexion est partagee entre les +threads de TestClient, l'exemple reste auto-suffisant, testable, et ne laisse +aucun fichier sur le disque. + +A comparer avec 05_01_api_rest_complete.py, qui implemente la MEME API mais +EN MEMOIRE (dictionnaires Python, sans base de donnees ni ORM). + +Note : les modeles SQLAlchemy (tables) sont ici suffixes "Table" +(UserTable, ArticleTable, CommentaireTable) pour eviter toute collision de +noms avec les modeles Pydantic (Utilisateur, Article, Commentaire) dans ce +fichier unique. Dans le cours, ils vivent dans deux modules separes. +""" + +from datetime import datetime + +from fastapi import Depends, FastAPI, HTTPException, Query, status +from fastapi.middleware.cors import CORSMiddleware +from fastapi.testclient import TestClient +from pydantic import BaseModel, ConfigDict, EmailStr, Field +from sqlalchemy import ( + JSON, Column, DateTime, ForeignKey, Integer, String, Text, create_engine +) +from sqlalchemy.orm import Session, declarative_base, relationship, sessionmaker +from sqlalchemy.pool import StaticPool + + +# ============================================================================ +# Modeles Pydantic (models.py dans le cours) +# Role : valider les requetes et serialiser les reponses. +# ============================================================================ + +class UtilisateurBase(BaseModel): + nom: str = Field(..., min_length=2, max_length=100) + email: EmailStr + bio: str | None = None + + +class UtilisateurCreate(UtilisateurBase): + mot_de_passe: str = Field(..., min_length=8) + + +class Utilisateur(UtilisateurBase): + model_config = ConfigDict(from_attributes=True) + + id: int + date_inscription: datetime + + +class ArticleBase(BaseModel): + titre: str = Field(..., min_length=5, max_length=200) + contenu: str = Field(..., min_length=10) + categorie: str + tags: list[str] = [] + + +class ArticleCreate(ArticleBase): + pass + + +class Article(ArticleBase): + model_config = ConfigDict(from_attributes=True) + + id: int + auteur_id: int + date_publication: datetime + nombre_vues: int = 0 + + +class ArticleAvecAuteur(Article): + auteur: Utilisateur + + +class CommentaireBase(BaseModel): + contenu: str = Field(..., min_length=1, max_length=500) + + +class CommentaireCreate(CommentaireBase): + pass + + +class Commentaire(CommentaireBase): + model_config = ConfigDict(from_attributes=True) + + id: int + article_id: int + auteur_id: int + date_creation: datetime + + +# ============================================================================ +# Base de donnees SQLAlchemy (database.py dans le cours) +# Role : le moteur, la session, et les modeles = les TABLES. +# ============================================================================ + +# Le cours utilise une base fichier : "sqlite:///./blog.db". +# Ici : base en memoire + StaticPool pour partager l'unique connexion entre +# les threads de TestClient et ne rien ecrire sur le disque. +engine = create_engine( + "sqlite://", + connect_args={"check_same_thread": False}, + poolclass=StaticPool, +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() + + +class UserTable(Base): + __tablename__ = "utilisateurs" + id = Column(Integer, primary_key=True) + nom = Column(String(100), nullable=False) + email = Column(String(100), unique=True, nullable=False) + bio = Column(Text, nullable=True) + mot_de_passe = Column(String(255), nullable=False) + date_inscription = Column(DateTime, nullable=False) + + +class ArticleTable(Base): + __tablename__ = "articles" + id = Column(Integer, primary_key=True) + titre = Column(String(200), nullable=False) + contenu = Column(Text, nullable=False) + categorie = Column(String(100), nullable=False) + tags = Column(JSON, default=list) # liste de chaines stockee en JSON + auteur_id = Column(Integer, ForeignKey("utilisateurs.id"), nullable=False) + date_publication = Column(DateTime, nullable=False) + nombre_vues = Column(Integer, default=0) + + auteur = relationship("UserTable") + + +class CommentaireTable(Base): + __tablename__ = "commentaires" + id = Column(Integer, primary_key=True) + contenu = Column(String(500), nullable=False) + article_id = Column(Integer, ForeignKey("articles.id"), nullable=False) + auteur_id = Column(Integer, ForeignKey("utilisateurs.id"), nullable=False) + date_creation = Column(DateTime, nullable=False) + + +def get_db(): + """Dependance FastAPI : fournit une session, fermee en fin de requete.""" + db = SessionLocal() + try: + yield db + finally: + db.close() + + +# ============================================================================ +# Application FastAPI (main.py dans le cours) +# ============================================================================ + +app = FastAPI( + title="Blog API", + description="Une API REST complete pour gerer un blog (SQLAlchemy)", + version="1.0.0", +) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Creer les tables +Base.metadata.create_all(bind=engine) + + +# ==================== UTILISATEURS ==================== + +@app.post("/api/utilisateurs", response_model=Utilisateur, + status_code=status.HTTP_201_CREATED, tags=["Utilisateurs"]) +def creer_utilisateur(utilisateur: UtilisateurCreate, + db: Session = Depends(get_db)): + """Creer un nouvel utilisateur (email unique).""" + db_user = db.query(UserTable).filter( + UserTable.email == utilisateur.email + ).first() + if db_user: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="Cet email est deja utilise" + ) + + db_utilisateur = UserTable( + nom=utilisateur.nom, + email=utilisateur.email, + bio=utilisateur.bio, + mot_de_passe=utilisateur.mot_de_passe, # A hasher en production ! + date_inscription=datetime.now(), + ) + db.add(db_utilisateur) + db.commit() + db.refresh(db_utilisateur) + return db_utilisateur + + +@app.get("/api/utilisateurs", response_model=list[Utilisateur], + tags=["Utilisateurs"]) +def lire_utilisateurs(skip: int = Query(0, ge=0), + limit: int = Query(10, ge=1, le=100), + db: Session = Depends(get_db)): + """Lister les utilisateurs avec pagination.""" + return db.query(UserTable).offset(skip).limit(limit).all() + + +@app.get("/api/utilisateurs/{utilisateur_id}", response_model=Utilisateur, + tags=["Utilisateurs"]) +def lire_utilisateur(utilisateur_id: int, db: Session = Depends(get_db)): + """Recuperer un utilisateur par son ID.""" + utilisateur = db.query(UserTable).filter( + UserTable.id == utilisateur_id + ).first() + if not utilisateur: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Utilisateur non trouve" + ) + return utilisateur + + +@app.put("/api/utilisateurs/{utilisateur_id}", response_model=Utilisateur, + tags=["Utilisateurs"]) +def modifier_utilisateur(utilisateur_id: int, utilisateur: UtilisateurCreate, + db: Session = Depends(get_db)): + """Mettre a jour un utilisateur.""" + db_utilisateur = db.query(UserTable).filter( + UserTable.id == utilisateur_id + ).first() + if not db_utilisateur: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Utilisateur non trouve" + ) + db_utilisateur.nom = utilisateur.nom + db_utilisateur.email = utilisateur.email + db_utilisateur.bio = utilisateur.bio + db.commit() + db.refresh(db_utilisateur) + return db_utilisateur + + +@app.delete("/api/utilisateurs/{utilisateur_id}", + status_code=status.HTTP_204_NO_CONTENT, tags=["Utilisateurs"]) +def supprimer_utilisateur(utilisateur_id: int, db: Session = Depends(get_db)): + """Supprimer un utilisateur.""" + db_utilisateur = db.query(UserTable).filter( + UserTable.id == utilisateur_id + ).first() + if not db_utilisateur: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Utilisateur non trouve" + ) + db.delete(db_utilisateur) + db.commit() + return None + + +# ==================== ARTICLES ==================== + +@app.post("/api/articles", response_model=Article, + status_code=status.HTTP_201_CREATED, tags=["Articles"]) +def creer_article(article: ArticleCreate, auteur_id: int, + db: Session = Depends(get_db)): + """Creer un article (l'auteur doit exister).""" + auteur = db.query(UserTable).filter(UserTable.id == auteur_id).first() + if not auteur: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Auteur non trouve" + ) + db_article = ArticleTable( + titre=article.titre, + contenu=article.contenu, + categorie=article.categorie, + tags=article.tags, + auteur_id=auteur_id, + date_publication=datetime.now(), + nombre_vues=0, + ) + db.add(db_article) + db.commit() + db.refresh(db_article) + return db_article + + +@app.get("/api/articles", response_model=list[Article], tags=["Articles"]) +def lire_articles( + skip: int = Query(0, ge=0), + limit: int = Query(10, ge=1, le=100), + categorie: str | None = None, + auteur_id: int | None = None, + sort: str = Query("date_publication", + pattern="^(date_publication|nombre_vues|titre)$"), + order: str = Query("desc", pattern="^(asc|desc)$"), + db: Session = Depends(get_db), +): + """Lister les articles avec filtrage, tri et pagination.""" + query = db.query(ArticleTable) + + if categorie: + query = query.filter(ArticleTable.categorie == categorie) + if auteur_id: + query = query.filter(ArticleTable.auteur_id == auteur_id) + + if order == "desc": + query = query.order_by(getattr(ArticleTable, sort).desc()) + else: + query = query.order_by(getattr(ArticleTable, sort).asc()) + + return query.offset(skip).limit(limit).all() + + +@app.get("/api/articles/{article_id}", response_model=ArticleAvecAuteur, + tags=["Articles"]) +def lire_article(article_id: int, db: Session = Depends(get_db)): + """Recuperer un article avec son auteur (incremente les vues).""" + article = db.query(ArticleTable).filter( + ArticleTable.id == article_id + ).first() + if not article: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Article non trouve" + ) + article.nombre_vues += 1 + db.commit() + return article + + +@app.patch("/api/articles/{article_id}", response_model=Article, + tags=["Articles"]) +def modifier_article_partiel(article_id: int, + titre: str | None = None, + contenu: str | None = None, + categorie: str | None = None, + db: Session = Depends(get_db)): + """Modifier partiellement un article (seuls les champs fournis).""" + db_article = db.query(ArticleTable).filter( + ArticleTable.id == article_id + ).first() + if not db_article: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Article non trouve" + ) + if titre is not None: + db_article.titre = titre + if contenu is not None: + db_article.contenu = contenu + if categorie is not None: + db_article.categorie = categorie + db.commit() + db.refresh(db_article) + return db_article + + +@app.delete("/api/articles/{article_id}", + status_code=status.HTTP_204_NO_CONTENT, tags=["Articles"]) +def supprimer_article(article_id: int, db: Session = Depends(get_db)): + """Supprimer un article.""" + db_article = db.query(ArticleTable).filter( + ArticleTable.id == article_id + ).first() + if not db_article: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Article non trouve" + ) + db.delete(db_article) + db.commit() + return None + + +# ==================== COMMENTAIRES ==================== + +@app.post("/api/articles/{article_id}/commentaires", + response_model=Commentaire, + status_code=status.HTTP_201_CREATED, tags=["Commentaires"]) +def creer_commentaire(article_id: int, commentaire: CommentaireCreate, + auteur_id: int, db: Session = Depends(get_db)): + """Ajouter un commentaire a un article.""" + article = db.query(ArticleTable).filter( + ArticleTable.id == article_id + ).first() + if not article: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Article non trouve" + ) + db_commentaire = CommentaireTable( + contenu=commentaire.contenu, + article_id=article_id, + auteur_id=auteur_id, + date_creation=datetime.now(), + ) + db.add(db_commentaire) + db.commit() + db.refresh(db_commentaire) + return db_commentaire + + +@app.get("/api/articles/{article_id}/commentaires", + response_model=list[Commentaire], tags=["Commentaires"]) +def lire_commentaires_article(article_id: int, db: Session = Depends(get_db)): + """Recuperer tous les commentaires d'un article.""" + article = db.query(ArticleTable).filter( + ArticleTable.id == article_id + ).first() + if not article: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Article non trouve" + ) + return db.query(CommentaireTable).filter( + CommentaireTable.article_id == article_id + ).all() + + +@app.delete("/api/commentaires/{commentaire_id}", + status_code=status.HTTP_204_NO_CONTENT, tags=["Commentaires"]) +def supprimer_commentaire(commentaire_id: int, db: Session = Depends(get_db)): + """Supprimer un commentaire.""" + db_commentaire = db.query(CommentaireTable).filter( + CommentaireTable.id == commentaire_id + ).first() + if not db_commentaire: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Commentaire non trouve" + ) + db.delete(db_commentaire) + db.commit() + return None + + +# ==================== STATISTIQUES / RACINE ==================== + +@app.get("/api/stats", tags=["Statistiques"]) +def obtenir_statistiques(db: Session = Depends(get_db)): + """Statistiques globales du blog.""" + return { + "nombre_utilisateurs": db.query(UserTable).count(), + "nombre_articles": db.query(ArticleTable).count(), + "nombre_commentaires": db.query(CommentaireTable).count(), + "article_plus_vu": db.query(ArticleTable).order_by( + ArticleTable.nombre_vues.desc() + ).first(), + } + + +@app.get("/", tags=["Root"]) +def root(): + """Endpoint racine de l'API.""" + return { + "message": "Bienvenue sur l'API Blog", + "version": "1.0.0", + "documentation": "/docs", + } + + +# ============================================================================ +# Client de consommation (BlogAPIClient dans le cours) +# Le cours s'appuie sur `requests` contre un serveur en cours d'execution ; +# ici on enveloppe un TestClient pour rester auto-suffisant et testable. +# ============================================================================ + +class BlogAPIClient: + """Petit client pour consommer l'API Blog.""" + + def __init__(self, client): + self.client = client + + def creer_utilisateur(self, nom, email, mot_de_passe, bio=None): + r = self.client.post("/api/utilisateurs", json={ + "nom": nom, "email": email, + "mot_de_passe": mot_de_passe, "bio": bio, + }) + r.raise_for_status() + return r.json() + + def creer_article(self, titre, contenu, categorie, auteur_id, tags=None): + r = self.client.post("/api/articles", params={"auteur_id": auteur_id}, + json={"titre": titre, "contenu": contenu, + "categorie": categorie, "tags": tags or []}) + r.raise_for_status() + return r.json() + + def obtenir_article(self, article_id): + r = self.client.get(f"/api/articles/{article_id}") + r.raise_for_status() + return r.json() + + def obtenir_articles(self, **params): + r = self.client.get("/api/articles", params=params) + r.raise_for_status() + return r.json() + + def creer_commentaire(self, article_id, contenu, auteur_id): + r = self.client.post(f"/api/articles/{article_id}/commentaires", + params={"auteur_id": auteur_id}, + json={"contenu": contenu}) + r.raise_for_status() + return r.json() + + def obtenir_statistiques(self): + r = self.client.get("/api/stats") + r.raise_for_status() + return r.json() + + +# ============================================================================ +# Demonstration via TestClient (aucun serveur a lancer) +# ============================================================================ + +if __name__ == "__main__": + test_client = TestClient(app) + api = BlogAPIClient(test_client) + + print("=== Endpoint racine ===") + print(f" {test_client.get('/').json()}") + + print("\n=== Creer des utilisateurs ===") + u1 = api.creer_utilisateur("Alice Dupont", "alice@example.com", + "motdepasse1", "Developpeuse Python") + print(f" Cree: {u1['nom']} (id={u1['id']})") + u2 = api.creer_utilisateur("Bob Martin", "bob@example.com", "motdepasse2") + print(f" Cree: {u2['nom']} (id={u2['id']})") + + print("\n=== Email en double (409 Conflict) ===") + r = test_client.post("/api/utilisateurs", json={ + "nom": "Alice Clone", "email": "alice@example.com", + "mot_de_passe": "12345678", + }) + print(f" {r.status_code}: {r.json()['detail']}") + + print("\n=== Creer des articles ===") + a1 = api.creer_article("Introduction a REST", + "REST est un style d'architecture pour les APIs web...", + "Tech", u1["id"], ["REST", "API", "Python"]) + print(f" Cree: '{a1['titre']}' (auteur {a1['auteur_id']})") + a2 = api.creer_article("Guide Python avance", + "Les decorateurs et metaclasses en Python...", + "Tech", u1["id"], ["Python", "avance"]) + print(f" Cree: '{a2['titre']}' (auteur {a2['auteur_id']})") + a3 = api.creer_article("Decouverte de la science", + "Les dernieres avancees scientifiques nous montrent...", + "Science", u2["id"], ["Science"]) + print(f" Cree: '{a3['titre']}' (auteur {a3['auteur_id']})") + + print("\n=== Lire un article avec auteur (incremente les vues) ===") + art = api.obtenir_article(a1["id"]) + print(f" '{art['titre']}' par {art['auteur']['nom']} - vues={art['nombre_vues']}") + art = api.obtenir_article(a1["id"]) + print(f" 2eme lecture - vues={art['nombre_vues']}") + + print("\n=== Filtrer par categorie / trier par vues ===") + tech = api.obtenir_articles(categorie="Tech") + print(f" Tech: {len(tech)} articles") + par_vues = api.obtenir_articles(sort="nombre_vues", order="desc") + print(f" Plus vu en premier: '{par_vues[0]['titre']}' ({par_vues[0]['nombre_vues']} vues)") + + print("\n=== Modification partielle (PATCH) ===") + r = test_client.patch(f"/api/articles/{a2['id']}", + params={"titre": "Guide Python (mis a jour)"}) + print(f" {r.status_code}: nouveau titre = '{r.json()['titre']}'") + + print("\n=== Commentaires ===") + api.creer_commentaire(a1["id"], "Excellent article sur REST !", u2["id"]) + api.creer_commentaire(a1["id"], "Merci pour le partage", u1["id"]) + coms = test_client.get(f"/api/articles/{a1['id']}/commentaires").json() + print(f" Commentaires de l'article {a1['id']}: {len(coms)}") + + print("\n=== Article inexistant (404) ===") + r = test_client.get("/api/articles/999") + print(f" {r.status_code}: {r.json()['detail']}") + + print("\n=== Validation : titre trop court (422) ===") + r = test_client.post("/api/articles", params={"auteur_id": u1["id"]}, json={ + "titre": "ABC", "contenu": "Contenu valide suffisamment long", + "categorie": "Tech", + }) + print(f" {r.status_code}: validation erreur (titre trop court)") + + print("\n=== Statistiques ===") + stats = api.obtenir_statistiques() + print(f" Utilisateurs: {stats['nombre_utilisateurs']}") + print(f" Articles: {stats['nombre_articles']}") + print(f" Commentaires: {stats['nombre_commentaires']}") + plus_vu = stats["article_plus_vu"] + print(f" Article le plus vu: '{plus_vu['titre']}' ({plus_vu['nombre_vues']} vues)") + + print("\n=== Suppressions ===") + r = test_client.delete(f"/api/articles/{a3['id']}") + print(f" DELETE article {a3['id']}: {r.status_code}") + r = test_client.delete(f"/api/utilisateurs/{u2['id']}") + print(f" DELETE utilisateur {u2['id']}: {r.status_code}") + stats = api.obtenir_statistiques() + print(f" Articles restants: {stats['nombre_articles']}, " + f"utilisateurs restants: {stats['nombre_utilisateurs']}") diff --git a/11-developpement-web-et-apis/exemples/06_02_sqlalchemy_crud.py b/11-developpement-web-et-apis/exemples/06_02_sqlalchemy_crud.py index 4629974..c1dd257 100644 --- a/11-developpement-web-et-apis/exemples/06_02_sqlalchemy_crud.py +++ b/11-developpement-web-et-apis/exemples/06_02_sqlalchemy_crud.py @@ -95,7 +95,7 @@ def get_session(): ] session.add_all(produits) -print(f" 3 produits crees") +print(" 3 produits crees") # === READ === @@ -172,7 +172,7 @@ def get_session(): session.add(User(nom="Test2", email="test@example.com", age=25)) except Exception as e: print(f" Erreur capturee: {type(e).__name__}") - print(f" Transaction annulee (rollback automatique)") + print(" Transaction annulee (rollback automatique)") with get_session() as session: test_user = session.query(User).filter(User.nom == "Test").first() diff --git a/11-developpement-web-et-apis/exemples/06_03_sqlalchemy_relations.py b/11-developpement-web-et-apis/exemples/06_03_sqlalchemy_relations.py index 390b337..60e30a0 100644 --- a/11-developpement-web-et-apis/exemples/06_03_sqlalchemy_relations.py +++ b/11-developpement-web-et-apis/exemples/06_03_sqlalchemy_relations.py @@ -457,7 +457,7 @@ class Emprunt(Base): # Verification d'appartenance if cours2 in etudiant2.cours: - print(f" Lucas est inscrit au cours de Python") + print(" Lucas est inscrit au cours de Python") session.close() @@ -578,7 +578,7 @@ class Emprunt(Base): session.add_all([etudiant_v2, cours_v2, insc]) session.commit() -print(f"\n Inscription creee:") +print("\n Inscription creee:") for i in etudiant_v2.inscriptions: print(f" {etudiant_v2.prenom} a eu {i.note}/20 en {i.cours.intitule}") print(f" Date: {i.date_inscription}, Statut: {i.statut}") @@ -616,14 +616,14 @@ class Emprunt(Base): print(f"\n === {bibli.nom} ===") print(f" Nombre de livres : {len(bibli.livres)}") -print(f"\n Livres de Victor Hugo :") +print("\n Livres de Victor Hugo :") for livre in hugo.livres: print(f" - {livre.titre}") print(f"\n Emprunts de {membre.nom} :") for emp in membre.emprunts: print(f" Date : {emp.date_emprunt}") - print(f" Livres empruntes :") + print(" Livres empruntes :") for livre in emp.livres: print(f" - {livre.titre} par {livre.auteur.nom}") diff --git a/11-developpement-web-et-apis/exemples/06_04_sqlalchemy_requetes.py b/11-developpement-web-et-apis/exemples/06_04_sqlalchemy_requetes.py index 1ed6678..27c606b 100644 --- a/11-developpement-web-et-apis/exemples/06_04_sqlalchemy_requetes.py +++ b/11-developpement-web-et-apis/exemples/06_04_sqlalchemy_requetes.py @@ -245,7 +245,7 @@ def __repr__(self): # Tri croissant par titre livres_tri = session.query(Livre).order_by(Livre.titre).all() -print(f"\n Tri par titre (croissant) :") +print("\n Tri par titre (croissant) :") for l in livres_tri[:3]: print(f" {l.titre}") print(f" ... ({len(livres_tri)} au total)") @@ -255,7 +255,7 @@ def __repr__(self): .filter(Livre.prix.is_not(None))\ .order_by(Livre.prix.desc())\ .all() -print(f"\n Tri par prix (decroissant) :") +print("\n Tri par prix (decroissant) :") for l in livres_tri_desc[:3]: print(f" {l.titre} - {l.prix} EUR") @@ -263,7 +263,7 @@ def __repr__(self): auteurs_tri = session.query(Auteur)\ .order_by(Auteur.nationalite, Auteur.nom)\ .all() -print(f"\n Tri par nationalite puis nom :") +print("\n Tri par nationalite puis nom :") for a in auteurs_tri: print(f" {a.nom} ({a.nationalite})") @@ -311,7 +311,7 @@ def __repr__(self): .order_by(Livre.prix)\ .limit(4)\ .all() -print(f"\n Titre et prix (top 4 moins chers) :") +print("\n Titre et prix (top 4 moins chers) :") for titre, prix in resultats: print(f" {titre} : {prix} EUR") @@ -320,7 +320,7 @@ def __repr__(self): .join(Auteur)\ .limit(5)\ .all() -print(f"\n Titre + Auteur (via JOIN) :") +print("\n Titre + Auteur (via JOIN) :") for titre_livre, nom_auteur in resultats_join: print(f" {titre_livre} par {nom_auteur}") @@ -329,7 +329,7 @@ def __repr__(self): Livre.titre.label('titre_livre'), Auteur.nom.label('nom_auteur') ).join(Auteur).limit(3).all() -print(f"\n Avec labels :") +print("\n Avec labels :") for row in resultats_label: print(f" {row.titre_livre} par {row.nom_auteur}") @@ -397,7 +397,7 @@ def __repr__(self): print(f" MIN(prix) : {prix_min} EUR, MAX(prix) : {prix_max} EUR") # GROUP BY -print(f"\n GROUP BY auteur :") +print("\n GROUP BY auteur :") resultats_group = session.query( Auteur.nom, func.count(Livre.id).label('nombre_livres') @@ -409,7 +409,7 @@ def __repr__(self): print(f" {nom} a ecrit {nb} livre(s)") # HAVING -print(f"\n HAVING count > 2 :") +print("\n HAVING count > 2 :") auteurs_prolif = session.query( Auteur.nom, func.count(Livre.id).label('nombre_livres') @@ -458,7 +458,7 @@ def __repr__(self): Auteur.nom, livre_subq.label('nb_livres') ).all() -print(f"\n Sous-requete correlee (auteur + nb livres) :") +print("\n Sous-requete correlee (auteur + nb livres) :") for nom, nb in auteurs_avec_compte: print(f" {nom} : {nb} livre(s)") @@ -474,7 +474,7 @@ def __repr__(self): text("SELECT titre, prix FROM livres WHERE prix > :prix ORDER BY prix DESC"), {"prix": 20} ) -print(f"\n SQL brut (prix > 20) :") +print("\n SQL brut (prix > 20) :") for row in resultat: print(f" {row[0]} - {row[1]} EUR") @@ -482,7 +482,7 @@ def __repr__(self): text("SELECT titre, annee_publication FROM livres WHERE annee_publication > :annee"), {"annee": 2000} ) -print(f"\n SQL brut (annee > 2000) :") +print("\n SQL brut (annee > 2000) :") for row in resultat2: print(f" {row[0]} ({row[1]})") @@ -584,27 +584,27 @@ def rechercher_livres( # Recherche 1 : livres francais -print(f"\n Recherche : auteurs francais, tri par annee desc") +print("\n Recherche : auteurs francais, tri par annee desc") res = rechercher_livres(nationalite="Francaise", tri_par='annee', ordre='desc') print(f" Trouve {res['total']} livre(s), page {res['page']}/{res['total_pages']}") for l in res['livres']: print(f" {l.titre} ({l.annee_publication}) - {l.prix} EUR") # Recherche 2 : Python -print(f"\n Recherche : titre contient 'Python'") +print("\n Recherche : titre contient 'Python'") res2 = rechercher_livres(titre='Python', tri_par='annee', ordre='desc') print(f" Trouve {res2['total']} livre(s)") for l in res2['livres']: print(f" {l.titre} ({l.annee_publication}) - {l.prix} EUR") # Recherche 3 : prix max 12, pagination -print(f"\n Recherche : prix <= 12, page 1, 2 par page") +print("\n Recherche : prix <= 12, page 1, 2 par page") res3 = rechercher_livres(prix_max=12, par_page=2, page=1, tri_par='prix') print(f" Trouve {res3['total']} livre(s), page {res3['page']}/{res3['total_pages']}") for l in res3['livres']: print(f" {l.titre} - {l.prix} EUR") -print(f"\n Page 2 :") +print("\n Page 2 :") res4 = rechercher_livres(prix_max=12, par_page=2, page=2, tri_par='prix') print(f" page {res4['page']}/{res4['total_pages']}") for l in res4['livres']: diff --git a/11-developpement-web-et-apis/exemples/README.md b/11-developpement-web-et-apis/exemples/README.md index fce5abd..b06f3de 100644 --- a/11-developpement-web-et-apis/exemples/README.md +++ b/11-developpement-web-et-apis/exemples/README.md @@ -1,40 +1,44 @@ -# Chapitre 11 - Developpement web et APIs : Exemples +# Chapitre 11 - Développement web et APIs : Exemples + +Ce dossier contient les exemples exécutables du chapitre 11, un fichier `.py` par thème, numérotés selon la section du cours (`01_*` → 11.1, `02_*` → 11.2, … `06_*` → 11.6). + +**Chaque fichier est autonome.** Les applications FastAPI et Flask se démontrent elles-mêmes via `TestClient` / `test_client` (aucun serveur à lancer manuellement) ; `04_01` démarre un petit serveur FastAPI local dans un thread pour illustrer `requests` sans dépendre d'Internet ; les exemples SQLAlchemy utilisent une base SQLite en mémoire ou temporaire. Sorties vérifiées avec FastAPI, Pydantic v2, SQLAlchemy 2.0, Flask et requests ; tous les exemples s'exécutent de Python 3.10 à 3.14. ## Fichiers d'exemples ### 01_01_concepts_web.py - **Section** : 11.1 - Introduction aux frameworks web - **Fichier source** : `01-introduction-frameworks-web.md` -- **Description** : Concepts fondamentaux du web - methodes HTTP, parsing d'URLs avec urllib.parse, simulation de routeur avec decorateurs, cycle requete/reponse, comparaison des frameworks Python +- **Description** : Concepts fondamentaux du web - méthodes HTTP, parsing d'URLs avec urllib.parse, simulation de routeur avec décorateurs, cycle requête/réponse, comparaison des frameworks Python - **Sortie attendue** : - - Methodes HTTP : GET, POST, PUT, DELETE avec descriptions - - Decomposition d'URL (scheme, netloc, path, query params) - - Routeur simulant l'association chemin/methode -> fonction - - Cycle requete/reponse avec dictionnaires + - Méthodes HTTP : GET, POST, PUT, DELETE avec descriptions + - Décomposition d'URL (scheme, netloc, path, query params) + - Routeur simulant l'association chemin/méthode -> fonction + - Cycle requête/réponse avec dictionnaires - Tableau comparatif Flask vs Django vs FastAPI ### 02_01_fastapi_concepts.py - **Section** : 11.2 - FastAPI framework moderne - **Fichier source** : `02-fastapi-framework-moderne.md` -- **Description** : Concepts Pydantic - BaseModel, validation (valide, conversion auto, erreurs), modeles imbriques (Adresse dans UtilisateurComplet), serialisation (model_dump, model_dump_json) +- **Description** : Concepts Pydantic - BaseModel, validation (valide, conversion auto, erreurs), modèles imbriqués (Adresse dans UtilisateurComplet), sérialisation (model_dump, model_dump_json) - **Sortie attendue** : - Validation d'un utilisateur valide - Conversion automatique de types (str "28" -> int 28) - Erreur de validation sur email invalide - - Modele imbrique avec Adresse - - Serialisation dict et JSON + - Modèle imbriqué avec Adresse + - Sérialisation dict et JSON ### 02_02_premier_projet_fastapi.py - **Section** : 11.2.1 - Installation et premier projet FastAPI - **Fichier source** : `02.1-installation-premier-projet-fastapi.md` -- **Description** : Application FastAPI avec TestClient - routes GET (/, /bonjour, /info, /utilisateur/{nom}, /age/{age}, /articles), POST/PUT/DELETE avec modele Article, erreur de validation (422 sur /age/abc) +- **Description** : Application FastAPI avec TestClient - routes GET (/, /bonjour, /info, /utilisateur/{nom}, /age/{age}, /articles), POST/PUT/DELETE avec modèle Article, erreur de validation (422 sur /age/abc) - **Sortie attendue** : - GET / : message de bienvenue - GET /bonjour : salutation simple - GET /info : nom et version de l'API - - GET /utilisateur/Alice : salutation personnalisee - - GET /age/25 : age valide accepte - - POST /articles : creation d'article (201) + - GET /utilisateur/Alice : salutation personnalisée + - GET /age/25 : âge valide accepté + - POST /articles : création d'article (201) - PUT /articles/1 : modification d'article - DELETE /articles/1 : suppression (204) - GET /age/abc : erreur validation 422 @@ -42,82 +46,82 @@ ### 02_03_routes_validation_pydantic.py - **Section** : 11.2.2 - Routes et validation Pydantic - **Fichier source** : `02.2-routes-et-validation-pydantic.md` -- **Description** : Blog API avec validation avancee - Field (min_length, max_length), ArticleCreation avec Auteur imbrique, Query params avec contraintes, path params, field_validator personnalise (nom avec espace, force du mot de passe), endpoint de recherche, ModificationUtilisateur +- **Description** : Blog API avec validation avancée - Field (min_length, max_length), ArticleCreation avec Auteur imbriqué, Query params avec contraintes, path params, field_validator personnalisé (nom avec espace, force du mot de passe), endpoint de recherche, ModificationUtilisateur - **Sortie attendue** : - - Creation d'article valide (201) + - Création d'article valide (201) - Erreur validation titre trop court (422) - Article par ID, liste avec pagination - - Recherche par mot-cle et categorie - - Creation utilisateur avec validation mot de passe fort - - Modification partielle utilisateur (PATCH) + - Recherche par mot-clé et catégorie + - Création utilisateur avec validation mot de passe fort + - Modification d'utilisateur (chemin + corps + query) ### 02_04_endpoints_asynchrones.py - **Section** : 11.2.3 - Endpoints asynchrones - **Fichier source** : `02.3-endpoints-asynchrones.md` -- **Description** : Endpoints async FastAPI - middleware de timing (X-Process-Time), asyncio.gather pour fetch parallele, calcul Fibonacci synchrone, asyncio.wait_for timeout, Semaphore limiteur de concurrence, BackgroundTasks pour email, cache simple avec /meteo/{ville} +- **Description** : Endpoints async FastAPI - middleware de timing (X-Process-Time), asyncio.gather pour fetch parallèle, calcul Fibonacci synchrone, asyncio.wait_for timeout, Semaphore limiteur de concurrence, BackgroundTasks pour email, cache simple avec /meteo/{ville} - **Sortie attendue** : - - Header X-Process-Time present dans les reponses - - Recuperation parallele de 3 sources - - Calcul Fibonacci synchrone (resultat correct) - - Timeout sur operation longue + - Header X-Process-Time présent dans les réponses + - Récupération parallèle de 3 sources + - Calcul Fibonacci synchrone (résultat correct) + - Timeout sur opération longue - Semaphore limitant la concurrence - - BackgroundTask programmee pour envoi email - - Cache meteo : miss puis hit + - BackgroundTask programmée pour envoi email + - Cache météo : miss puis hit ### 03_01_flask_bases.py - **Section** : 11.3 - Flask micro-framework - **Fichier source** : `03-flask-micro-framework.md` -- **Description** : Flask bases - routes (/, /about, /contact), routes parametrees (/user/, /post/), url_for, sessions (login/dashboard/logout), inspection de l'objet request, gestionnaires d'erreurs (404 HTML et JSON) +- **Description** : Flask bases - routes (/, /about, /contact), routes paramétrées (/user/, /post/), url_for, sessions (login/dashboard/logout), inspection de l'objet request, gestionnaires d'erreurs (404 HTML et JSON) - **Sortie attendue** : - Pages d'accueil, about, contact (200) - - Route parametree avec username et post_id - - url_for generant les bons chemins + - Route paramétrée avec username et post_id + - url_for générant les bons chemins - Session : login -> dashboard avec nom -> logout -> redirect - - Objet request : methode, path, headers + - Objet request : méthode, path, headers - Erreur 404 HTML et JSON ### 03_02_flask_api_rest.py - **Section** : 11.3 - Flask micro-framework - **Fichier source** : `03-flask-micro-framework.md` -- **Description** : API REST complete avec Flask - CRUD taches (GET all, GET by id, POST create, PUT update, DELETE), jsonify, request.get_json, codes HTTP +- **Description** : API REST complète avec Flask - CRUD tâches (GET all, GET by id, POST create, PUT update, DELETE), jsonify, request.get_json, codes HTTP - **Sortie attendue** : - - GET /api/tasks : 2 taches initiales - - GET /api/tasks/1 : tache specifique + - GET /api/tasks : 2 tâches initiales + - GET /api/tasks/1 : tâche spécifique - GET /api/tasks/999 : 404 - - POST : creation d'une 3eme tache (201) + - POST : création d'une 3e tâche (201) - PUT /api/tasks/1 : marquer done=True - DELETE /api/tasks/2 : suppression - - Etat final : 2 taches restantes + - État final : 2 tâches restantes ### 04_01_requetes_http_requests.py -- **Section** : 11.4 - Requetes HTTP avec requests +- **Section** : 11.4 - Requêtes HTTP avec requests - **Fichier source** : `04-requetes-http-requests.md` -- **Description** : Bibliotheque requests - serveur FastAPI local (port 9999) avec uvicorn en thread, GET avec params, POST json, PUT, DELETE, headers personnalises, Session requests, timeout (ReadTimeout sur /slow), gestion complete des erreurs (Timeout, ConnectionError, HTTPError), proprietes de Response +- **Description** : Bibliothèque requests - serveur FastAPI local (port 9999) avec uvicorn en thread, GET avec params, POST json, PUT, DELETE, headers personnalisés, Session requests, timeout (ReadTimeout sur /slow), gestion complète des erreurs (Timeout, ConnectionError, HTTPError), propriétés de Response - **Sortie attendue** : - - GET /users : liste JSON, status 200, Content-Type, temps de reponse + - GET /users : liste JSON, status 200, Content-Type, temps de réponse - GET avec params : URL construite avec query string - - GET /users/1 : utilisateur specifique - - GET /users/999 : 404 avec detail + - GET /users/1 : utilisateur spécifique + - GET /users/999 : 404 avec détail - response.ok et raise_for_status - - POST /users : creation (201) - - PUT /users/1 : mise a jour + - POST /users : création (201) + - PUT /users/1 : mise à jour - DELETE /users/2 : suppression - - Headers personnalises envoyes - - Session partageant headers - - Timeout apres 1s sur /slow + - Headers personnalisés envoyés + - Session partageant les headers + - Timeout après 1s sur /slow - Gestion erreurs : HTTPError 404, ConnectionError ### 05_01_api_rest_complete.py -- **Section** : 11.5 - Creation et consommation d'APIs REST +- **Section** : 11.5 - Création et consommation d'APIs REST - **Fichier source** : `05-creation-consommation-apis-rest.md` -- **Description** : API REST complete blog - modeles Pydantic (UtilisateurCreate, Article, Commentaire), CRUD utilisateurs/articles/commentaires, ResourceNotFound, CORS, pagination (skip/limit), filtrage (categorie, auteur_id), compteur de vues, BlogAPIClient, field_validator +- **Description** : API REST complète de blog **en mémoire** (modèles Pydantic + dictionnaires, sans base de données) - CRUD utilisateurs/articles/commentaires, ResourceNotFound, CORS, pagination (skip/limit), filtrage (catégorie, auteur_id), compteur de vues, BlogAPIClient, field_validator. *Note : version **en mémoire** (modèles Pydantic + dictionnaires, aucune base à configurer). Voir `05_02_api_rest_sqlalchemy.py` pour la **même API avec persistance SQLAlchemy**, fidèle au `.md`.* - **Sortie attendue** : - - Endpoint racine avec message bienvenue - - Creation 2 utilisateurs (201) + - Endpoint racine avec message de bienvenue + - Création 2 utilisateurs (201) - Email en double : 409 Conflict - - Creation 3 articles avec tags et categories - - Lecture incrementant les vues - - Filtrage par categorie (Tech: 2, Science: 1) + - Création 3 articles avec tags et catégories + - Lecture incrémentant les vues + - Filtrage par catégorie (Tech: 2, Science: 1) - Filtrage par auteur - Pagination (skip/limit) - Commentaires sur article @@ -126,71 +130,89 @@ - Statistiques (users, articles, commentaires) - Suppression article et utilisateur +### 05_02_api_rest_sqlalchemy.py +- **Section** : 11.5 - Création et consommation d'APIs REST +- **Fichier source** : `05-creation-consommation-apis-rest.md` +- **Description** : La **même** API REST de blog, mais avec **persistance SQLAlchemy** (modèles ORM `UserTable`/`ArticleTable`/`CommentaireTable` + schémas Pydantic), fidèle au `.md`. Le cours répartit ce code en `models.py`/`database.py`/`main.py` ; ici tout est réuni dans un fichier unique. CRUD complet, filtrage + tri (`sort`/`order`), PATCH partiel, relation `article.auteur` (modèle `ArticleAvecAuteur`), statistiques (article le plus vu). Base SQLite **en mémoire** (`StaticPool`) : auto-suffisant, testable via `TestClient`, sans fichier laissé sur le disque. *Pendant persistant de `05_01` (version en mémoire).* +- **Sortie attendue** : + - Endpoint racine avec message de bienvenue + - Création 2 utilisateurs (201), email en double : 409 Conflict + - Création 3 articles (tags stockés en JSON, catégories) + - Lecture avec auteur (`auteur.nom`) incrémentant les vues (1 -> 2) + - Filtrage par catégorie (Tech: 2), tri par vues (plus vu en premier) + - Modification partielle PATCH (200, nouveau titre) + - 2 commentaires sur l'article + - 404 article inexistant, 422 titre trop court + - Statistiques (2 users, 3 articles, 2 commentaires, article le plus vu) + - Suppressions (204) : 2 articles et 1 utilisateur restants + ### 06_01_intro_bdd_orm.py -- **Section** : 11.6 - Bases de donnees et ORM +- **Section** : 11.6 - Bases de données et ORM - **Fichier source** : `06-bases-de-donnees-orm-sqlalchemy.md` - **Description** : Comparaison SQL brut (sqlite3) vs ORM (SQLAlchemy) - CREATE TABLE, INSERT, SELECT avec filtre, UPDATE, DELETE. Tableau comparatif final - **Sortie attendue** : - - SQL brut : 3 utilisateurs, filtre age>30 (2), mise a jour Alice (29 ans), suppression (2 restants) - - ORM : memes operations avec objets Python - - Tableau comparatif : 6 operations comparees + - SQL brut : 3 utilisateurs, filtre age>30 (2), mise à jour Alice (29 ans), suppression (2 restants) + - ORM : mêmes opérations avec objets Python + - Tableau comparatif : 6 opérations comparées ### 06_02_sqlalchemy_crud.py -- **Section** : 11.6.1 - Introduction a SQLAlchemy +- **Section** : 11.6.1 - Introduction à SQLAlchemy - **Fichier source** : `06.1-introduction-sqlalchemy.md` -- **Description** : CRUD complet SQLAlchemy - declarative_base, modeles User et Produit (Integer, String, Float, Boolean, Text), context manager get_session() avec commit/rollback, CREATE (add, add_all, flush), READ (all, get, filter, count), UPDATE, DELETE, transaction rollback sur IntegrityError +- **Description** : CRUD complet SQLAlchemy - declarative_base, modèles User et Produit (Integer, String, Float, Boolean, Text), context manager get_session() avec commit/rollback, CREATE (add, add_all, flush), READ (all, get, filter, count), UPDATE, DELETE, transaction rollback sur IntegrityError - **Sortie attendue** : - CREATE : 4 utilisateurs, 3 produits - READ : tous les users, par ID, filtre age>30 (2), par email, count, produits en stock (2) - - UPDATE : Alice age 28->29, email modifie - - DELETE : Bob supprime, 3 restants - - TRANSACTION : rollback sur email duplique, User 'Test' n'existe pas + - UPDATE : Alice age 28->29, email modifié + - DELETE : Bob supprimé, 3 restants + - TRANSACTION : rollback sur email dupliqué, User 'Test' n'existe pas - Types de colonnes : String, Text, Float, Boolean, Integer - - Etat final : 3 utilisateurs, 3 produits + - État final : 3 utilisateurs, 3 produits ### 06_03_sqlalchemy_relations.py -- **Section** : 11.6.2 - Modeles et relations +- **Section** : 11.6.2 - Modèles et relations - **Fichier source** : `06.2-modeles-et-relations.md` -- **Description** : Relations SQLAlchemy - One-to-Many (Auteur/Livre), Many-to-One (Article/Commentaire), Many-to-Many (Etudiant/Cours avec table d'association), One-to-One (Utilisateur/Profil avec uselist=False), cascade delete-orphan, auto-referentiel (Employe/Manager), Association Object (Inscription avec notes), exemple complet Bibliotheque, eager loading (joinedload/selectinload) +- **Description** : Relations SQLAlchemy - One-to-Many (Auteur/Livre), Many-to-One (Article/Commentaire), Many-to-Many (Etudiant/Cours avec table d'association), One-to-One (Utilisateur/Profil avec uselist=False), cascade delete-orphan, auto-référentiel (Employe/Manager), Association Object (Inscription avec notes), exemple complet Bibliothèque, eager loading (joinedload/selectinload) - **Sortie attendue** : - - One-to-Many : Hugo 3 livres, Moliere 2 livres + - One-to-Many : Hugo 3 livres, Molière 2 livres - Many-to-One : article avec 3 commentaires - - Many-to-Many : 3 etudiants, 3 cours, inscriptions croisees, ajout/retrait dynamique + - Many-to-Many : 3 étudiants, 3 cours, inscriptions croisées, ajout/retrait dynamique - One-to-One : utilisateur avec profil (type Profil, pas liste) - - Cascade : orphelin supprime, cascade sur delete auteur (0 livres) - - Auto-referentiel : hierarchie CEO -> Managers -> Employes + - Cascade : orphelin supprimé, cascade sur delete auteur (0 livres) + - Auto-référentiel : hiérarchie CEO -> Managers -> Employés - Association Object : inscription avec note 15/20, date, statut - - Bibliotheque : 3 livres, emprunt de 2 livres par un membre - - Eager loading : joinedload et selectinload (4 auteurs charges) + - Bibliothèque : 3 livres, emprunt de 2 livres par un membre + - Eager loading : joinedload et selectinload (4 auteurs chargés) ### 06_04_sqlalchemy_requetes.py -- **Section** : 11.6.3 - Requetes et migrations +- **Section** : 11.6.3 - Requêtes et migrations - **Fichier source** : `06.3-requetes-et-migrations.md` -- **Description** : Requetes avancees SQLAlchemy - filtres multiples (chainage, multi-conditions), operateurs logiques (and_, or_, not_, combinaison complexe), comparaisons (LIKE, IN, BETWEEN, IS NULL), tri (ORDER BY asc/desc, multi-colonnes), pagination (LIMIT/OFFSET, first, one_or_none), selection de colonnes (labels), jointures (JOIN, OUTERJOIN), agregations (COUNT, SUM, AVG, MIN, MAX, GROUP BY, HAVING), sous-requetes (scalar_subquery, correlee), SQL brut (text), eager loading, fonction de recherche avancee avec pagination +- **Description** : Requêtes avancées SQLAlchemy - filtres multiples (chaînage, multi-conditions), opérateurs logiques (and_, or_, not_, combinaison complexe), comparaisons (LIKE, IN, BETWEEN, IS NULL), tri (ORDER BY asc/desc, multi-colonnes), pagination (LIMIT/OFFSET, first, one_or_none), sélection de colonnes (labels), jointures (JOIN, OUTERJOIN), agrégations (COUNT, SUM, AVG, MIN, MAX, GROUP BY, HAVING), sous-requêtes (scalar_subquery, corrélée), SQL brut (text), eager loading, fonction de recherche avancée avec pagination - **Sortie attendue** : - - Filtres multiples : 5 livres (annee>1840 ET prix<15) - - or_ : 5 livres (avant 1840 OU apres 2000) + - Filtres multiples : 5 livres (année>1840 ET prix<15) + - or_ : 5 livres (avant 1840 OU après 2000) - Combinaison complexe : 5 livres - LIKE 'Le%' : 4 livres, '%python%' insensible : 2 - IN : 3 livres, BETWEEN 1840-1860 : 4, IS NULL : 1 - - Tri par titre, prix desc, nationalite+nom + - Tri par titre, prix desc, nationalité+nom - Pagination : page 2 avec 3 par page - - JOIN : 9 livres, Francais : 5, OUTERJOIN : 10 (avec anonyme) + - JOIN : 9 livres, Français : 5, OUTERJOIN : 10 (avec anonyme) - COUNT:10, SUM:153.30, AVG:17.03, MIN:8.50, MAX:42.00 - GROUP BY : 4 auteurs, HAVING >2 : Victor Hugo (3) - - Sous-requetes : 2 livres chers, 1 auteur moderne + - Sous-requêtes : 2 livres chers, 1 auteur moderne - SQL brut : 2 livres > 20 EUR - - Recherche avancee : francais (5), Python (2), pagination prix<=12 + - Recherche avancée : français (5), Python (2), pagination prix<=12 -## Dependances +## Dépendances ```bash -pip install fastapi uvicorn httpx pydantic flask requests sqlalchemy +pip install fastapi uvicorn httpx pydantic flask requests sqlalchemy email-validator ``` -## Execution +> `email-validator` n'est requis que par `05_02_api_rest_sqlalchemy.py` (champ `EmailStr`, comme dans le `.md`). Les autres exemples emploient `email: str` et n'en ont pas besoin. + +## Exécution -Chaque fichier est autonome et peut etre execute independamment : +Chaque fichier est autonome et peut être exécuté indépendamment : ```bash python3 01_01_concepts_web.py @@ -199,4 +221,4 @@ python3 02_01_fastapi_concepts.py python3 06_04_sqlalchemy_requetes.py ``` -Les fichiers de base de donnees temporaires sont automatiquement crees dans `/tmp/` et nettoyes apres execution. +Les fichiers de base de données temporaires sont automatiquement créés dans `/tmp/` et nettoyés après exécution. diff --git a/12-projets-et-bonnes-pratiques/01-architecture-projet-outils-modernes.md b/12-projets-et-bonnes-pratiques/01-architecture-projet-outils-modernes.md index 407c944..1378371 100644 --- a/12-projets-et-bonnes-pratiques/01-architecture-projet-outils-modernes.md +++ b/12-projets-et-bonnes-pratiques/01-architecture-projet-outils-modernes.md @@ -56,7 +56,12 @@ mon_projet/ Le dossier `src/` (source) contient tout votre code source. C'est là que vit votre application. **Pourquoi `src/mon_projet/` et pas juste `mon_projet/` ?** -Cette structure en deux niveaux est une bonne pratique moderne car elle évite certains problèmes d'importation et force l'installation du package avant de l'utiliser. +Cette structure en deux niveaux (appelée *src layout*) est la bonne pratique moderne recommandée par la documentation officielle d'empaquetage Python. La raison tient à un détail du fonctionnement des imports : **Python ajoute automatiquement le répertoire courant à son chemin de recherche** (`sys.path`). + +- **Sans `src/`** (le package `mon_projet/` est posé à la racine) : quand vous lancez `python` ou `pytest` depuis la racine du projet, ce dossier est dans `sys.path`, donc `import mon_projet` trouve le code **directement**, sans même que le package soit installé. Vous testez alors le code *du dossier de travail*, pas le package *tel qu'il sera distribué* — et un fichier oublié dans la configuration d'empaquetage passe inaperçu (il est là localement, mais absent une fois le package installé chez l'utilisateur). +- **Avec `src/`** : le package est rangé sous `src/`, qui n'est **pas** dans `sys.path`. `import mon_projet` ne fonctionne donc qu'**après installation** du package. On l'installe une seule fois en mode « éditable » avec `pip install -e .` (ou `uv pip install -e .`) : vos tests s'exécutent alors contre le package réellement installé, dans les mêmes conditions que vos utilisateurs, et les erreurs d'empaquetage sont détectées immédiatement. + +En résumé, le *src layout* force une séparation nette entre « le code que j'écris » et « le package que j'installe », ce qui rend les tests bien plus fiables. #### Le fichier `__init__.py` @@ -122,9 +127,9 @@ config.local.py Liste toutes les dépendances (bibliothèques externes) de votre projet avec leurs versions : ``` -requests==2.31.0 -pandas>=2.0.0 -numpy==1.24.3 +requests==2.32.5 +pandas>=2.2.0 +numpy==2.4.6 ``` Installation des dépendances : @@ -261,19 +266,29 @@ description = "Description de mon projet" authors = ["Votre Nom "] [tool.poetry.dependencies] -python = "^3.10" -requests = "^2.31.0" -pandas = "^2.0.0" +python = "^3.12" +requests = "^2.32.0" +pandas = "^2.2.0" [tool.poetry.group.dev.dependencies] -pytest = "^7.4.0" -black = "^23.0.0" +pytest = "^9.0.0" +black = "^26.0.0" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" ``` +> **Poetry 2.0+** : vous pouvez déclarer les métadonnées dans le tableau standard `[project]` (PEP 621), partagé avec pip/uv/hatch, plutôt que dans `[tool.poetry]`. L'ancienne syntaxe reste prise en charge. + +> **Comprendre les contraintes de version (`^`, `>=`, `==`).** Les symboles placés devant les numéros précisent *quelles* versions d'une dépendance sont acceptées lors de l'installation : +> +> - `^2.32.0` (caret, syntaxe Poetry) autorise toutes les mises à jour qui **ne changent pas le premier chiffre non nul** — ici, tout ce qui va de `2.32.0` (inclus) à `3.0.0` (exclu). L'idée : en versionnage sémantique (détaillé en [12.5](/12-projets-et-bonnes-pratiques/05-deploiement-et-distribution.md)), un changement de numéro **majeur** (`2.x` → `3.0`) signale une rupture de compatibilité ; le caret s'en protège tout en laissant passer corrections de bugs et ajouts mineurs ; +> - `>=2.32.0` (syntaxe standard, utilisée par uv et pip) demande **au minimum** cette version, sans borne supérieure ; +> - `==2.32.0` fige une version **exacte**. +> +> Le caret est donc un compromis : profiter des correctifs sans risquer une montée de version majeure potentiellement cassante. + #### Commandes Poetry essentielles ```bash @@ -290,16 +305,109 @@ poetry install poetry run python mon_script.py # Activer l'environnement virtuel -poetry shell +# (Poetry 2.0+ : la commande `poetry shell` a été retirée du cœur. +# `poetry env activate` affiche la commande d'activation ; exécutez-la, +# ou installez le plugin `poetry-plugin-shell` pour retrouver `poetry shell`.) +poetry env activate # Mettre à jour les dépendances poetry update -# Générer requirements.txt (pour compatibilité) +# Générer requirements.txt (depuis Poetry 2.0, fourni par le plugin +# poetry-plugin-export) poetry export -f requirements.txt --output requirements.txt ``` -### 2. Black - Formateur de code automatique +### 2. uv - le gestionnaire de projet tout-en-un (le plus rapide) + +**uv**, développé par Astral (les créateurs de Ruff), est un gestionnaire de paquets et de projets écrit en Rust. Apparu en 2024, il s'est imposé comme **l'outil le plus moderne** : il réunit en un seul binaire ce que faisaient `pip`, `virtualenv`, `pyenv` et `pip-tools`, ainsi qu'une grande partie de Poetry — le tout 10 à 100 fois plus vite. + +#### Installation + +```bash +# Script officiel (Linux / macOS) +curl -LsSf https://astral.sh/uv/install.sh | sh + +# Windows (PowerShell) +powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" + +# Ou via pip / pipx +pip install uv +``` + +#### Créer et gérer un projet + +```bash +# Initialiser un nouveau projet (crée pyproject.toml + structure de base) +uv init mon_projet +cd mon_projet + +# Ajouter une dépendance (résout, installe et verrouille en une seule étape) +uv add requests + +# Ajouter des dépendances de développement +uv add --dev pytest ruff mypy + +# Lancer un script ou un outil dans l'environnement du projet +# (inutile d'activer le venv : uv run s'en charge) +uv run python mon_script.py +uv run pytest + +# Synchroniser l'environnement avec le fichier de verrouillage +uv sync + +# Mettre à jour le fichier de verrouillage +uv lock +``` + +uv crée et gère automatiquement l'environnement virtuel (`.venv/`) et un fichier de verrouillage **`uv.lock`** qui garantit des installations reproductibles d'une machine à l'autre. + +#### Gérer les versions de Python + +Contrairement à Poetry, uv sait aussi **installer et sélectionner les versions de Python** : + +```bash +uv python install 3.12 # Installer Python 3.12 +uv python pin 3.12 # Fixer la version utilisée par le projet +``` + +#### Le fichier `pyproject.toml` avec uv + +uv s'appuie sur le tableau **`[project]`** standard (PEP 621), partagé par tout l'écosystème (pip, build, hatch...), et sur les groupes de dépendances standards (PEP 735) : + +```toml +[project] +name = "mon_projet" +version = "0.1.0" +description = "Description de mon projet" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "requests>=2.32.0", +] + +[dependency-groups] +dev = [ + "pytest>=9.0.0", + "ruff>=0.15.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" +``` + +#### Exécuter un outil ponctuellement (uvx) + +```bash +# Lancer un outil sans l'installer durablement (équivalent de "pipx run") +uvx ruff check . +uvx ruff format . +``` + +> **Poetry ou uv ?** Les deux sont d'excellents choix. Poetry est mature et très répandu ; uv est plus récent, nettement plus rapide, et gère en plus les versions de Python. Tous deux reposent sur `pyproject.toml` : Poetry 2.0+ comme uv lisent le tableau standard `[project]`, ce qui facilite le passage de l'un à l'autre. + +### 3. Black - Formateur de code automatique **Black** formate automatiquement votre code selon les standards Python (PEP 8). Plus besoin de débats sur le style de code ! @@ -332,7 +440,7 @@ black --diff src/ ```toml [tool.black] line-length = 88 -target-version = ['py310'] +target-version = ['py312'] include = '\.pyi?$' extend-exclude = ''' /( @@ -362,9 +470,9 @@ def ma_fonction(x, y, z): return resultat ``` -### 3. Ruff - Linter ultra-rapide +### 4. Ruff - Linter et formateur ultra-rapide -**Ruff** est un linter Python moderne, écrit en Rust, qui remplace plusieurs outils (Flake8, isort, etc.) et est 10-100 fois plus rapide ! +**Ruff** est un linter **et formateur** Python moderne, écrit en Rust, 10 à 100 fois plus rapide que les outils traditionnels. Il remplace à lui seul Flake8, isort, pyupgrade et bien d'autres ; et, avec la commande `ruff format`, il remplace aussi **Black** (formatage compatible à plus de 99 %). #### Installation @@ -377,13 +485,16 @@ poetry add --group dev ruff #### Utilisation ```bash -# Analyser votre code +# Analyser votre code (linting) ruff check . -# Corriger automatiquement les erreurs possibles +# Corriger automatiquement ce qui peut l'être ruff check --fix . -# Formater les imports +# Formater le code (remplace Black, compatible à plus de 99 %) +ruff format . + +# Trier les imports (règle isort intégrée) ruff check --select I --fix . ``` @@ -392,7 +503,7 @@ ruff check --select I --fix . ```toml [tool.ruff] line-length = 88 -target-version = "py310" +target-version = "py312" # Fichiers à exclure exclude = [ @@ -416,11 +527,11 @@ select = [ # Règles à ignorer ignore = [ - "E501", # line too long (géré par Black) + "E501", # longueur de ligne (gérée par le formateur) ] ``` -### 4. mypy - Vérificateur de types +### 5. mypy - Vérificateur de types **mypy** vérifie les annotations de types dans votre code Python pour détecter les erreurs avant l'exécution. @@ -456,13 +567,13 @@ resultat: str = additionner(5, 10) # Erreur : int assigné à str ```toml [tool.mypy] -python_version = "3.10" +python_version = "3.12" warn_return_any = true warn_unused_configs = true disallow_untyped_defs = true ``` -### 5. pre-commit - Hooks Git automatiques +### 6. pre-commit - Hooks Git automatiques **pre-commit** exécute automatiquement des vérifications avant chaque commit Git. @@ -479,26 +590,23 @@ poetry add --group dev pre-commit ```yaml repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v6.0.0 hooks: - id: trailing-whitespace # Supprime espaces en fin de ligne - id: end-of-file-fixer # Ajoute ligne vide en fin de fichier - id: check-yaml # Vérifie syntaxe YAML - id: check-added-large-files # Empêche gros fichiers - - repo: https://github.com/psf/black - rev: 23.11.0 - hooks: - - id: black - + # Ruff : linting + formatage (ruff-format remplace Black) - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.6 + rev: v0.15.11 hooks: - - id: ruff + - id: ruff-check args: [--fix] + - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.7.1 + rev: v2.1.0 hooks: - id: mypy ``` @@ -511,7 +619,9 @@ pre-commit install Maintenant, à chaque `git commit`, les outils s'exécutent automatiquement ! -### 6. pytest - Framework de tests moderne +> **Astuce** : gardez les versions des hooks à jour avec `pre-commit autoupdate`, qui réécrit automatiquement les numéros `rev:` vers les dernières versions disponibles. + +### 7. pytest - Framework de tests moderne **pytest** est l'outil de référence pour écrire et exécuter des tests en Python. @@ -575,17 +685,17 @@ readme = "README.md" packages = [{include = "mon_projet", from = "src"}] [tool.poetry.dependencies] -python = "^3.10" -requests = "^2.31.0" +python = "^3.12" +requests = "^2.32.0" pydantic = "^2.5.0" [tool.poetry.group.dev.dependencies] -pytest = "^7.4.0" -pytest-cov = "^4.1.0" -black = "^23.11.0" -ruff = "^0.1.6" -mypy = "^1.7.0" -pre-commit = "^3.5.0" +pytest = "^9.0.0" +pytest-cov = "^6.0.0" +black = "^26.1.0" +ruff = "^0.15.0" +mypy = "^2.1.0" +pre-commit = "^4.0.0" [build-system] requires = ["poetry-core"] @@ -593,7 +703,7 @@ build-backend = "poetry.core.masonry.api" [tool.black] line-length = 88 -target-version = ['py310'] +target-version = ['py312'] [tool.ruff] line-length = 88 @@ -603,7 +713,7 @@ select = ["E", "F", "I", "B", "C4", "UP"] ignore = ["E501"] [tool.mypy] -python_version = "3.10" +python_version = "3.12" warn_return_any = true warn_unused_configs = true @@ -627,7 +737,7 @@ Description courte et claire de votre projet. ### Prérequis -- Python 3.10 ou supérieur +- Python 3.12 ou supérieur - Poetry (recommandé) ou pip ### Avec Poetry (recommandé) @@ -640,8 +750,8 @@ cd mon-projet # Installer les dépendances poetry install -# Activer l'environnement -poetry shell +# Activer l'environnement (Poetry 2.0+) +poetry env activate ``` ### Avec pip @@ -804,12 +914,13 @@ poetry run mypy src/ | Tâche | Outil traditionnel | Outil moderne | Avantage | |-------|-------------------|---------------|----------| -| Gestion des dépendances | pip + requirements.txt | Poetry | Résolution de dépendances, fichier lock | -| Formatage du code | Manual (PEP 8) | Black | Automatique, pas de débat | -| Linting | Flake8, Pylint | Ruff | 10-100x plus rapide | +| Gestion des dépendances | pip + requirements.txt | Poetry ou uv | Résolution de dépendances, fichier de verrouillage | +| Formatage du code | Manuel (PEP 8) | `ruff format` ou Black | Automatique, pas de débat | +| Linting | Flake8, Pylint, isort | Ruff | 10 à 100x plus rapide, tout-en-un | | Vérification de types | Annotations manuelles | mypy | Détection d'erreurs automatique | | Tests | unittest | pytest | Syntaxe plus simple, plus de fonctionnalités | -| Environnement virtuel | venv + pip | Poetry | Géré automatiquement | +| Environnement virtuel | venv + pip | Poetry ou uv | Géré automatiquement | +| Versions de Python | pyenv (séparé) | uv | Installe et sélectionne Python | --- @@ -825,12 +936,12 @@ Ne vous sentez pas obligé d'utiliser tous ces outils dès le début ! Voici une - .gitignore **Niveau 2 - Intermédiaire :** -- Poetry pour la gestion des dépendances -- Black pour le formatage +- uv ou Poetry pour la gestion des dépendances +- Ruff (`ruff format`) ou Black pour le formatage - pytest pour les tests **Niveau 3 - Avancé :** -- Ruff pour le linting +- Ruff pour le linting (et le formatage) - mypy pour les types - pre-commit pour les hooks - Configuration complète dans pyproject.toml @@ -844,6 +955,7 @@ Ne vous sentez pas obligé d'utiliser tous ces outils dès le début ! Voici une ### Ressources pour aller plus loin +- **Documentation uv** : https://docs.astral.sh/uv/ - **Documentation officielle Poetry** : https://python-poetry.org/docs/ - **Guide Black** : https://black.readthedocs.io/ - **Documentation Ruff** : https://docs.astral.sh/ruff/ @@ -858,9 +970,9 @@ Une bonne architecture de projet Python moderne comprend : ✅ **Structure claire** : séparation du code source, des tests et de la documentation -✅ **Gestion des dépendances** : Poetry ou requirements.txt +✅ **Gestion des dépendances** : uv ou Poetry (ou requirements.txt pour les petits projets) -✅ **Qualité du code** : Black (formatage), Ruff (linting), mypy (types) +✅ **Qualité du code** : Ruff (linting + formatage, ou Black), mypy (types) ✅ **Tests automatisés** : pytest avec couverture de code diff --git a/12-projets-et-bonnes-pratiques/02-gestion-version-git.md b/12-projets-et-bonnes-pratiques/02-gestion-version-git.md index f57ea32..3e99a3d 100644 --- a/12-projets-et-bonnes-pratiques/02-gestion-version-git.md +++ b/12-projets-et-bonnes-pratiques/02-gestion-version-git.md @@ -81,6 +81,12 @@ feature : F --- G --- H La branche principale s'appelle traditionnellement `main` (ou `master` dans les anciens projets). +### HEAD (votre position actuelle) + +**HEAD** est un pointeur qui indique **où vous vous trouvez** dans l'historique. La plupart du temps, `HEAD` désigne le dernier commit de la branche courante — c'est ce que signale la mention `HEAD -> main` dans `git log`, et c'est à partir de ce point que votre prochain commit sera enchaîné. + +Vous rencontrerez aussi une **notation relative** bien pratique : `HEAD~1` désigne le commit juste **avant** HEAD, `HEAD~2` celui d'**encore avant**, et ainsi de suite. C'est elle qui donne tout leur sens à des commandes comme `git reset HEAD~1` (« reviens d'un commit en arrière ») ou `git revert HEAD` (« annule le commit courant »). + --- ## Installation de Git @@ -93,7 +99,7 @@ Ouvrez un terminal et tapez : git --version ``` -Si Git est installé, vous verrez la version (ex : `git version 2.42.0`). +Si Git est installé, vous verrez la version (ex : `git version 2.50.0`). ### Installation @@ -475,6 +481,12 @@ git push -u origin main **Note** : `-u` (ou `--set-upstream`) crée une liaison entre votre branche locale et la branche distante. Vous n'aurez à le faire qu'une seule fois. +> **Authentification GitHub/GitLab** : depuis 2021, le mot de passe du compte n'est plus accepté pour `git push` en HTTPS. Trois options modernes : +> +> - **HTTPS + token** : générez un *Personal Access Token* (GitHub : Settings → Developer settings → Personal access tokens) et utilisez-le comme mot de passe lors du push ; +> - **SSH** : générez une clé avec `ssh-keygen -t ed25519 -C "votre.email@example.com"`, ajoutez la clé publique à votre compte, puis utilisez l'URL SSH (`git@github.com:votre-nom/projet.git`) ; +> - **GitHub CLI** (le plus simple) : `gh auth login` configure l'authentification automatiquement. + ### Commandes pour synchroniser avec le distant ```bash @@ -579,6 +591,10 @@ ENV/ htmlcov/ .tox/ +# Caches des outils (Ruff, mypy) +.ruff_cache/ +.mypy_cache/ + # Variables d'environnement .env .env.local @@ -639,13 +655,18 @@ Un bon message de commit est essentiel pour maintenir un historique clair. ### Types de commits courants +Ces préfixes suivent la convention **[Conventional Commits](https://www.conventionalcommits.org/)**, largement adoptée et exploitée par des outils d'automatisation (génération de changelog, calcul automatique de la version) : + - `feat`: nouvelle fonctionnalité - `fix`: correction de bug - `docs`: modification de documentation - `style`: formatage, point-virgules manquants, etc. (pas de changement de code) - `refactor`: refactorisation du code (ni feat ni fix) - `test`: ajout ou modification de tests -- `chore`: modifications build, dépendances, etc. +- `chore`: modifications diverses (config, outillage, etc.) +- `perf`: amélioration des performances +- `build`: changements du système de build ou des dépendances +- `ci`: changements de la configuration d'intégration continue (CI) ### Exemples de bons messages @@ -685,7 +706,7 @@ git commit -m "fix login, ajout feature export, update readme" 2. **Soyez concis** : 50 caractères maximum pour la première ligne 3. **Expliquez le "pourquoi"**, pas le "quoi" (le code montre le "quoi") 4. **Un commit = une modification logique** : ne mélangez pas plusieurs changements -5. **Écrivez au présent** : "Corrige le bug" plutôt que "Corrigé le bug" +5. **Référencez les tickets** : mentionnez le numéro d'issue concerné, par exemple `fix: corrige le calcul de la TVA (#42)` --- diff --git a/12-projets-et-bonnes-pratiques/03-patterns-de-conception.md b/12-projets-et-bonnes-pratiques/03-patterns-de-conception.md index 3e34508..0539c10 100644 --- a/12-projets-et-bonnes-pratiques/03-patterns-de-conception.md +++ b/12-projets-et-bonnes-pratiques/03-patterns-de-conception.md @@ -60,6 +60,8 @@ print(db1 is db2) # True - c'est la même instance ! print(id(db1) == id(db2)) # True - même adresse mémoire ``` +> **Pourquoi `__new__` et pas `__init__` ?** `__new__` est la méthode qui **crée** réellement l'instance : elle s'exécute *avant* `__init__` et c'est elle qui renvoie l'objet. `__init__`, lui, ne fait qu'**initialiser** un objet déjà créé. Or, pour un Singleton, on veut agir sur la **création** elle-même afin de toujours renvoyer le même objet — c'est donc `__new__` qu'il faut redéfinir. Redéfinir `__init__` ne suffirait pas : l'objet serait déjà construit (une nouvelle instance à chaque appel), trop tard pour l'empêcher. + ### Solution Pythonique (avec décorateur) ```python @@ -722,19 +724,19 @@ class WhippedCreamDecorator(CoffeeDecorator): # Utilisation - on peut empiler les décorateurs ! coffee = SimpleCoffee() -print(f"{coffee.get_description()}: {coffee.get_cost()}€") +print(f"{coffee.get_description()}: {coffee.get_cost():.1f}€") # Café simple: 2.0€ coffee = MilkDecorator(coffee) -print(f"{coffee.get_description()}: {coffee.get_cost()}€") +print(f"{coffee.get_description()}: {coffee.get_cost():.1f}€") # Café simple, lait: 2.5€ coffee = SugarDecorator(coffee) -print(f"{coffee.get_description()}: {coffee.get_cost()}€") +print(f"{coffee.get_description()}: {coffee.get_cost():.1f}€") # Café simple, lait, sucre: 2.7€ coffee = WhippedCreamDecorator(coffee) -print(f"{coffee.get_description()}: {coffee.get_cost()}€") +print(f"{coffee.get_description()}: {coffee.get_cost():.1f}€") # Café simple, lait, sucre, chantilly: 3.4€ # Ou en une seule ligne @@ -1135,8 +1137,8 @@ from abc import ABC, abstractmethod # Modèle de données class User: - def __init__(self, id: int, name: str, email: str): - self.id = id + def __init__(self, id: int | None, name: str, email: str): + self.id = id # None tant que l'utilisateur n'est pas encore enregistré self.name = name self.email = email @@ -1246,6 +1248,8 @@ for user in service.list_users(): print(user) ``` +> ⚠️ **Note de sécurité (SQL).** Le `DatabaseUserRepository` ci-dessus construit ses requêtes par **f-string** (`f"...WHERE id = {user_id}"`, `f"...VALUES ('{user.name}', ...)"`) uniquement pour rester lisible et se concentrer sur le *pattern* — ces requêtes sont d'ailleurs simplement affichées, jamais exécutées. **En conditions réelles, ne faites jamais cela** : insérer une valeur directement dans une chaîne SQL ouvre la porte aux **injections SQL** (voir le chapitre 11). Utilisez systématiquement des **requêtes paramétrées** — `cursor.execute("...WHERE id = ?", (user_id,))` avec `sqlite3`, ou un ORM comme SQLAlchemy qui les génère pour vous. + ### Avantages du pattern Repository ✅ **Séparation des responsabilités** : logique métier ≠ accès aux données @@ -1636,6 +1640,22 @@ Python offre des fonctionnalités natives qui remplacent certains patterns class - **Context managers** : pattern Resource Management - **Générateurs** : pattern Iterator simplifié - **Duck typing** : moins besoin d'interfaces formelles +- **`typing.Protocol`** : interfaces structurelles (duck typing *typé*), alternative légère à `ABC` sans héritage explicite +- **`dataclasses`** : génèrent `__init__`, `__repr__`, `__eq__`... pour les classes de données + +Par exemple, `typing.Protocol` permet de définir une interface sans imposer d'héritage — l'équivalent typé du duck typing : + +```python +from typing import Protocol + +class Payeur(Protocol): + def payer(self, montant: float) -> str: ... + +# Toute classe possédant une méthode payer(montant) -> str est compatible, +# sans hériter de Payeur (compatibilité vérifiée statiquement par mypy). +def encaisser(moyen: Payeur, montant: float) -> str: + return moyen.payer(montant) +``` --- diff --git a/12-projets-et-bonnes-pratiques/04-optimisation-performances.md b/12-projets-et-bonnes-pratiques/04-optimisation-performances.md index 09fba83..89b1618 100644 --- a/12-projets-et-bonnes-pratiques/04-optimisation-performances.md +++ b/12-projets-et-bonnes-pratiques/04-optimisation-performances.md @@ -58,6 +58,8 @@ print(f"Temps d'exécution : {end - start:.4f} secondes") # Temps d'exécution : 1.0001 secondes ``` +> **Bon réflexe** : pour mesurer une *durée*, préférez `time.perf_counter()` à `time.time()` — résolution plus fine et insensible aux changements d'horloge système (`time.time()` sert plutôt à obtenir la date/heure courante). + ### Mesure avec `timeit` (plus précis) ```python @@ -96,9 +98,9 @@ def measure_time(func): """Décorateur qui mesure le temps d'exécution""" @wraps(func) def wrapper(*args, **kwargs): - start = time.time() + start = time.perf_counter() result = func(*args, **kwargs) - end = time.time() + end = time.perf_counter() print(f"{func.__name__} a pris {end - start:.4f} secondes") return result return wrapper @@ -194,6 +196,11 @@ def fonction_gourmande(): # python -m memory_profiler script.py ``` +### Profilers modernes : Scalene et py-spy + +- **[Scalene](https://github.com/plasma-umass/scalene)** : profileur CPU + mémoire + GPU précis et à faible surcoût (`pip install scalene`, puis `scalene script.py`). +- **[py-spy](https://github.com/benfred/py-spy)** : profileur par échantillonnage qui s'attache à un programme *déjà en cours d'exécution*, sans modifier ni redémarrer le code (`py-spy top --pid `). + --- ## Optimisation des structures de données @@ -331,7 +338,7 @@ def avec_boucle(): def avec_comprehension(): return [i * 2 for i in range(1000)] -# ✅ Encore plus rapide : map +# ⚠️ map AVEC une lambda : souvent PLUS LENT ici (voir la note sous le bloc) def avec_map(): return list(map(lambda x: x * 2, range(1000))) @@ -340,6 +347,8 @@ print(f"Comprehension : {timeit.timeit(avec_comprehension, number=10000):.4f}s") print(f"Map : {timeit.timeit(avec_map, number=10000):.4f}s") ``` +> **Piège : `map` n'est pas toujours plus rapide.** Avec une **lambda** (comme ci-dessus), `map` est en réalité souvent **plus lent** que la list comprehension — la lambda ajoute un appel de fonction Python à chaque élément, ce qui annule le gain de `map`. `map` ne devient intéressant qu'avec une **fonction déjà existante** (built-in ou définie), par exemple `map(str, nombres)` ou `map(int, chaines)`. Pour une opération simple comme `x * 2`, **la list comprehension reste la plus rapide**. + ### Éviter les recherches répétées ```python @@ -754,6 +763,12 @@ print(result_np) # [12 14 16 18 20] Python a le **GIL** (Global Interpreter Lock) qui empêche le vrai parallélisme avec des threads pour le code Python pur. Solutions : +> **Évolution (Python 3.13+)** : une version *expérimentale sans GIL* (« free-threading », PEP 703) permet désormais le vrai parallélisme multi-thread, et un compilateur JIT expérimental (PEP 744) a été introduit. Ces fonctionnalités restent optionnelles et expérimentales — le GIL demeure actif par défaut — donc les solutions ci-dessous restent la référence. + +> **Alors pourquoi le `threading` accélère-t-il quand même l'I/O ?** Parce que le GIL est **relâché pendant les opérations d'entrée/sortie bloquantes**. Quand un thread attend une réponse réseau, la lecture d'un fichier ou un `time.sleep()`, il *ne calcule rien* : il lâche le GIL, ce qui laisse un autre thread progresser pendant ce temps d'attente. Comme une tâche I/O passe l'essentiel de son temps à **attendre**, les threads se relaient efficacement et le temps total s'effondre (les 5 téléchargements de l'exemple ci-dessous se chevauchent : ~2 s au lieu de 10 s). +> +> À l'inverse, une tâche **CPU intensif** garde le GIL pour calculer : les threads ne peuvent plus se relayer, et `threading` n'apporte aucun gain (voire un léger surcoût). Il faut alors `multiprocessing`, qui lance de **vrais processus séparés**, chacun avec son propre interpréteur et donc son **propre GIL** — d'où un parallélisme réel sur plusieurs cœurs. + ### Threading pour les opérations I/O Bon pour : requêtes réseau, lecture/écriture fichiers @@ -974,7 +989,7 @@ def calcul_lourd(int n): # Compiler cython calcul.pyx gcc -shared -pthread -fPIC -fwrapv -O2 -Wall -fno-strict-aliasing \ - -I/usr/include/python3.10 -o calcul.so calcul.c + -I/usr/include/python3.12 -o calcul.so calcul.c ``` ### Numba : JIT compilation diff --git a/12-projets-et-bonnes-pratiques/05-deploiement-et-distribution.md b/12-projets-et-bonnes-pratiques/05-deploiement-et-distribution.md index 3348094..7d34566 100644 --- a/12-projets-et-bonnes-pratiques/05-deploiement-et-distribution.md +++ b/12-projets-et-bonnes-pratiques/05-deploiement-et-distribution.md @@ -269,9 +269,8 @@ setup( }, classifiers=[ "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Development Status :: 4 - Beta", @@ -279,13 +278,13 @@ setup( ], package_dir={"": "src"}, packages=find_packages(where="src"), - python_requires=">=3.10", + python_requires=">=3.12", install_requires=requirements, extras_require={ "dev": [ "pytest>=7.0", "black>=23.0", - "flake8>=6.0", + "ruff>=0.15", "mypy>=1.0", ], }, @@ -299,6 +298,8 @@ setup( ### Le fichier `pyproject.toml` (moderne) +> **Note** : `setup.py` est aujourd'hui considéré comme *historique*. Pour un nouveau projet, un simple `pyproject.toml` (ci-dessous) suffit — il remplace à la fois `setup.py` et `setup.cfg`. + Le format moderne et recommandé est `pyproject.toml` (PEP 621) : ```toml @@ -322,7 +323,7 @@ classifiers = [ "Operating System :: OS Independent", ] keywords = ["exemple", "tutorial", "python"] -requires-python = ">=3.10" +requires-python = ">=3.12" dependencies = [ "requests>=2.28.0", "pydantic>=2.0.0", @@ -348,6 +349,8 @@ mon_package = "mon_package.main:main" where = ["src"] ``` +> **À quoi sert `[project.scripts]` (l'équivalent moderne de `console_scripts`) ?** Cette section transforme votre package en **commande en ligne de commande**. La ligne `mon_package = "mon_package.main:main"` se lit ` = ":"` : une fois le package installé (`pip install mon_package`), une commande `mon_package` devient disponible dans le terminal et exécute la fonction `main()` du module `mon_package.main`. C'est exactement ainsi que des outils comme `ruff`, `black` ou `pytest` fournissent leur commande — l'utilisateur tape `pytest` plutôt que `python -m ...`. + ### Le fichier `__init__.py` ```python @@ -391,6 +394,8 @@ python -m build # - dist/mon_package-0.1.0-py3-none-any.whl (wheel) ``` +> **Avec uv** : `uv build` produit les mêmes artefacts (sdist + wheel), sans avoir à installer `build` séparément. + ### Tester localement ```bash @@ -449,6 +454,17 @@ python -m twine upload dist/* pip install mon_package ``` +### Méthode moderne : Trusted Publishing (recommandé en CI) + +Plutôt que de stocker un token, PyPI propose le **Trusted Publishing** (OIDC) : vous déclarez sur PyPI quel dépôt GitHub/GitLab est autorisé à publier, et le pipeline d'intégration continue publie **sans aucun secret à gérer**. C'est la méthode recommandée aujourd'hui pour les pipelines automatisés (action `pypa/gh-action-pypi-publish`). + +Avec **uv**, construction et publication se font sans `build` ni `twine` : + +```bash +uv build # crée dist/*.tar.gz et dist/*.whl +uv publish # publie sur PyPI (token via UV_PUBLISH_TOKEN, ou Trusted Publishing en CI) +``` + ### Automatiser avec `.pypirc` Créez un fichier `~/.pypirc` pour éviter de retaper vos identifiants : @@ -558,7 +574,7 @@ Créez un fichier `runtime.txt` : ```txt # runtime.txt -python-3.11.0 +python-3.12.0 ``` #### Déploiement @@ -677,7 +693,7 @@ vercel ```dockerfile # Dockerfile -FROM python:3.11-slim +FROM python:3.12-slim # Définir le répertoire de travail WORKDIR /app @@ -702,7 +718,7 @@ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] ```dockerfile # Dockerfile -FROM python:3.11-slim +FROM python:3.12-slim # Variables d'environnement ENV PYTHONUNBUFFERED=1 \ @@ -729,6 +745,8 @@ EXPOSE 8000 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] ``` +> **Pourquoi copier `requirements.txt` *avant* le code ?** C'est l'astuce qui rend ce Dockerfile « optimisé ». Docker construit une image en **couches** (*layers*), une par instruction, et **met chaque couche en cache** : tant que ni l'instruction ni les fichiers qu'elle copie n'ont changé, Docker réutilise la couche déjà construite au lieu de la refaire. En copiant d'abord le seul `requirements.txt`, puis en lançant `pip install`, et *seulement ensuite* en copiant le code (`COPY . .`), on obtient ceci : tant que `requirements.txt` ne change pas, l'étape `pip install` (longue) est **reprise du cache**, même si vous avez modifié votre code entre-temps. Si l'on copiait tout d'un bloc *avant* `pip install`, la moindre modification d'une ligne de code invaliderait le cache et **réinstallerait toutes les dépendances** à chaque construction. Ordonner les instructions du « ce qui change le moins » vers le « ce qui change le plus » est donc la clé d'un build Docker rapide. + ### Fichier `.dockerignore` Créez un `.dockerignore` pour exclure les fichiers inutiles : @@ -799,7 +817,7 @@ services: command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload db: - image: postgres:15 + image: postgres:17 environment: - POSTGRES_USER=user - POSTGRES_PASSWORD=pass @@ -862,7 +880,7 @@ docker run -p 8000:8000 votre-nom/mon-app:latest pip install awsebcli # Initialiser -eb init -p python-3.11 mon-app +eb init -p python-3.12 mon-app # Créer un environnement et déployer eb create mon-app-env @@ -935,7 +953,7 @@ gcloud run deploy mon-app \ ```yaml # app.yaml -runtime: python311 +runtime: python312 entrypoint: gunicorn -b :$PORT main:app @@ -966,7 +984,7 @@ az group create --name mon-groupe --location westeurope az appservice plan create --name mon-plan --resource-group mon-groupe --sku B1 --is-linux # Créer l'application web -az webapp create --resource-group mon-groupe --plan mon-plan --name mon-app --runtime "PYTHON|3.11" +az webapp create --resource-group mon-groupe --plan mon-plan --name mon-app --runtime "PYTHON|3.12" # Configurer pour déploiement Git az webapp deployment source config-local-git --name mon-app --resource-group mon-groupe @@ -1006,7 +1024,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: '3.12' - name: Install dependencies run: | @@ -1018,10 +1036,10 @@ jobs: run: | pytest --cov=src tests/ - - name: Lint with flake8 + - name: Lint with Ruff run: | - pip install flake8 - flake8 src/ --max-line-length=88 + pip install ruff + ruff check src/ deploy: needs: test @@ -1059,7 +1077,7 @@ cache: test: stage: test - image: python:3.11 + image: python:3.12 script: - python -m venv venv - source venv/bin/activate @@ -1072,7 +1090,7 @@ test: deploy: stage: deploy - image: python:3.11 + image: python:3.12 script: - pip install build twine - python -m build @@ -1189,7 +1207,7 @@ Un bon README est **essentiel** pour que les autres utilisent votre projet. # Mon Projet Python ![CI/CD](https://github.com/votre-nom/mon-projet/workflows/CI/badge.svg) -![Python Version](https://img.shields.io/badge/python-3.10%2B-blue) +![Python Version](https://img.shields.io/badge/python-3.12%2B-blue) ![License](https://img.shields.io/badge/license-MIT-green) Description courte et accrocheuse de votre projet. @@ -1202,7 +1220,7 @@ Description courte et accrocheuse de votre projet. ## 📋 Prérequis -- Python 3.10 ou supérieur +- Python 3.12 ou supérieur - pip - (Autres prérequis) @@ -1481,9 +1499,9 @@ class User(BaseModel): ### 5. Mise à jour régulière des dépendances ```bash -# Vérifier les vulnérabilités -pip install safety -safety check +# Vérifier les vulnérabilités (pip-audit, outil recommandé par la PyPA) +pip install pip-audit +pip-audit # Mettre à jour les dépendances pip install --upgrade -r requirements.txt diff --git a/12-projets-et-bonnes-pratiques/README.md b/12-projets-et-bonnes-pratiques/README.md index 7c17716..2fdd53c 100644 --- a/12-projets-et-bonnes-pratiques/README.md +++ b/12-projets-et-bonnes-pratiques/README.md @@ -40,7 +40,7 @@ Ce chapitre couvre cinq aspects essentiels du développement professionnel en Py **Ce que vous allez découvrir :** - Comment structurer un projet Python de manière professionnelle -- Les outils modernes qui facilitent le développement (Poetry, Black, Ruff, mypy) +- Les outils modernes qui facilitent le développement (uv, Poetry, Ruff, Black, mypy) - Comment automatiser les tâches répétitives - Les standards et conventions de l'industrie diff --git a/12-projets-et-bonnes-pratiques/exemples/03_01_patterns_creation.py b/12-projets-et-bonnes-pratiques/exemples/03_01_patterns_creation.py index 514a1f1..d2ef9f3 100644 --- a/12-projets-et-bonnes-pratiques/exemples/03_01_patterns_creation.py +++ b/12-projets-et-bonnes-pratiques/exemples/03_01_patterns_creation.py @@ -196,7 +196,7 @@ def get_exporter(format: str) -> DocumentExporter: data = {"nom": "Rapport", "date": "2024-01-15"} -print(f"\n --- Exporteurs ---") +print("\n --- Exporteurs ---") for fmt in ["pdf", "excel", "csv"]: exporter = ExporterFactory.get_exporter(fmt) print(f" {exporter.export(data)}") diff --git a/12-projets-et-bonnes-pratiques/exemples/03_02_patterns_comportementaux.py b/12-projets-et-bonnes-pratiques/exemples/03_02_patterns_comportementaux.py index 246924d..25be576 100644 --- a/12-projets-et-bonnes-pratiques/exemples/03_02_patterns_comportementaux.py +++ b/12-projets-et-bonnes-pratiques/exemples/03_02_patterns_comportementaux.py @@ -218,7 +218,7 @@ def compress_file(self, filename: str, data: str): data = "Beaucoup de donnees a compresser..." * 10 -print(f"\n --- Compression ---") +print("\n --- Compression ---") compressor = FileCompressor(ZipCompression()) compressor.compress_file("document.txt", data) @@ -277,7 +277,7 @@ def __next__(self): return self.current + 1 -print(f"\n --- CountDown ---") +print("\n --- CountDown ---") countdown = CountDown(5) for num in countdown: print(f" {num}") @@ -308,7 +308,7 @@ def __next__(self): data = list(range(1, 26)) paginator = Paginator(data, page_size=10) -print(f"\n --- Pagination ---") +print("\n --- Pagination ---") for page_num, page in enumerate(paginator, 1): print(f" Page {page_num}: {page}") @@ -326,9 +326,9 @@ def paginate(items: list, page_size: int = 10): yield items[i:i + page_size] -print(f"\n --- Generateur countdown ---") +print("\n --- Generateur countdown ---") print(f" {list(countdown_gen(5))}") -print(f"\n --- Generateur pagination ---") +print("\n --- Generateur pagination ---") for page in paginate(list(range(1, 16)), page_size=5): print(f" {page}") diff --git a/12-projets-et-bonnes-pratiques/exemples/03_03_patterns_structurels.py b/12-projets-et-bonnes-pratiques/exemples/03_03_patterns_structurels.py index 496aeac..d14df5a 100644 --- a/12-projets-et-bonnes-pratiques/exemples/03_03_patterns_structurels.py +++ b/12-projets-et-bonnes-pratiques/exemples/03_03_patterns_structurels.py @@ -2,8 +2,8 @@ # Section 12.3 : Patterns de conception courants # Description : Patterns structurels et pythoniques - Decorator (classe cafe, # decorateurs fonctions, permissions), Adapter (media, meteo), -# Repository (InMemory, Database), Context Manager (timer, -# transaction, fichier temporaire) +# Repository (InMemory, Database), Context Manager (classe +# __enter__/__exit__, timer, transaction, fichier temporaire) # Fichier source : 03-patterns-de-conception.md # ============================================================================ @@ -296,8 +296,8 @@ def afficher_meteo(service: WeatherService, ville: str): class User: - def __init__(self, id: int, name: str, email: str): - self.id = id + def __init__(self, id: int | None, name: str, email: str): + self.id = id # None tant que l'utilisateur n'est pas encore enregistre self.name = name self.email = email @@ -348,6 +348,35 @@ def delete(self, user_id: int) -> bool: return False +class DatabaseUserRepository(UserRepository): + """Implementation simulee avec base de donnees (affiche le SQL genere).""" + + def __init__(self, connection): + self.connection = connection + + def find_by_id(self, user_id: int) -> User | None: + print(f" SQL: SELECT * FROM users WHERE id = {user_id}") + return None + + def find_all(self) -> list[User]: + print(" SQL: SELECT * FROM users") + return [] + + def save(self, user: User) -> User: + if user.id: + query = (f"UPDATE users SET name='{user.name}', " + f"email='{user.email}' WHERE id={user.id}") + else: + query = (f"INSERT INTO users (name, email) " + f"VALUES ('{user.name}', '{user.email}')") + print(f" SQL: {query}") + return user + + def delete(self, user_id: int) -> bool: + print(f" SQL: DELETE FROM users WHERE id = {user_id}") + return True + + class UserService: def __init__(self, repository: UserRepository): self.repository = repository @@ -369,7 +398,7 @@ def list_users(self) -> list[User]: user1 = service.register_user("Alice", "alice@example.com") user2 = service.register_user("Bob", "bob@example.com") -print(f"\n Utilisateurs crees :") +print("\n Utilisateurs crees :") for user in service.list_users(): print(f" {user}") @@ -378,6 +407,12 @@ def list_users(self) -> list[User]: repository.delete(2) print(f" Apres suppression id=2 : {len(service.list_users())} utilisateur(s)") +# Meme service, autre implementation (depot SQL simule) : interface identique +print("\n --- Meme UserService avec un depot SQL (simule) ---") +service_sql = UserService(DatabaseUserRepository("postgresql://localhost/db")) +service_sql.register_user("Charlie", "charlie@example.com") +service_sql.get_user(1) + # ============================================================ # PATTERN 10 : CONTEXT MANAGER @@ -387,6 +422,33 @@ def list_users(self) -> list[User]: print("=" * 50) +# --- Context manager a base de classe (__enter__ / __exit__) --- + +class FileHandler: + """Context manager explicite via __enter__ / __exit__.""" + + def __init__(self, filename: str, mode: str = "r"): + self.filename = filename + self.mode = mode + self.file = None + + def __enter__(self): + print(f"\n Ouverture de {self.filename}") + self.file = open(self.filename, self.mode, encoding="utf-8") + return self.file + + def __exit__(self, exc_type, exc_val, exc_tb): + if self.file: + print(f" Fermeture de {self.filename}") + self.file.close() + return False # False = propage les exceptions eventuelles + + +with FileHandler("/tmp/temp_filehandler_test.txt", "w") as fh: + fh.write("Hello World!") +os.remove("/tmp/temp_filehandler_test.txt") + + # --- Timer context manager --- @contextmanager diff --git a/12-projets-et-bonnes-pratiques/exemples/04_01_optimisation_performances.py b/12-projets-et-bonnes-pratiques/exemples/04_01_optimisation_performances.py index 0fc8848..d8bc3f3 100644 --- a/12-projets-et-bonnes-pratiques/exemples/04_01_optimisation_performances.py +++ b/12-projets-et-bonnes-pratiques/exemples/04_01_optimisation_performances.py @@ -64,9 +64,9 @@ def approche2(): def measure_time(func): @wraps(func) def wrapper(*args, **kwargs): - start = time.time() + start = time.perf_counter() result = func(*args, **kwargs) - end = time.time() + end = time.perf_counter() print(f" {func.__name__} a pris {end - start:.4f} secondes") return result return wrapper @@ -115,7 +115,7 @@ def main_profile(): stats = pstats.Stats(profiler, stream=stream) stats.sort_stats('cumulative') stats.print_stats(5) -print(f"\n Top 5 fonctions (profiling) :") +print("\n Top 5 fonctions (profiling) :") for line in stream.getvalue().split('\n')[5:12]: if line.strip(): print(f" {line}") @@ -142,7 +142,7 @@ def main_profile(): _ = 9999 in data_set temps_set = time.time() - start -print(f"\n Recherche 'in' (1000 fois) :") +print("\n Recherche 'in' (1000 fois) :") print(f" Liste : {temps_liste:.4f}s") print(f" Set : {temps_set:.6f}s") print(f" Set est {temps_liste / temps_set:.0f}x plus rapide") @@ -195,6 +195,10 @@ def avec_comprehension(): def avec_map(): + # map AVEC une lambda : souvent PLUS LENT que la comprehension ci-dessus. + # La lambda ajoute un appel de fonction Python par element, ce qui annule + # le gain de map ; map n'est interessant qu'avec une fonction deja existante + # (built-in ou definie), par exemple map(str, nombres). return list(map(lambda x: x * 2, range(1000))) @@ -263,6 +267,30 @@ def find_duplicates_fast(items): print(f" Deduplication O(n) : {t_fast:.6f}s ({len(r2)} doublons)") +# --- Optimisation des boucles : invariant hors boucle + enumerate --- + +import math + + +def boucle_invariant_dans(): + result = [] + for i in range(1000): + result.append(i * math.sqrt(2)) # sqrt(2) recalcule a chaque tour + return result + + +def boucle_invariant_hors(): + sqrt_2 = math.sqrt(2) # calcule une seule fois, hors de la boucle + return [i * sqrt_2 for i in range(1000)] + + +tb_in = timeit.timeit(boucle_invariant_dans, number=5000) +tb_out = timeit.timeit(boucle_invariant_hors, number=5000) +print(f"\n Invariant DANS la boucle : {tb_in:.4f}s") +print(f" Invariant HORS boucle : {tb_out:.4f}s") +print(f" enumerate : {[f'{i}:{v}' for i, v in enumerate(['a', 'b', 'c'])]}") + + # ============================================================ # GENERATEURS vs LISTES # ============================================================ @@ -421,7 +449,7 @@ async def main_async(): print(f" Resultats : {len(results)} reponses") for r in results[:2]: print(f" {r}") -print(f" ...") +print(" ...") # ============================================================ diff --git a/12-projets-et-bonnes-pratiques/exemples/README.md b/12-projets-et-bonnes-pratiques/exemples/README.md index 255d62a..3da28af 100644 --- a/12-projets-et-bonnes-pratiques/exemples/README.md +++ b/12-projets-et-bonnes-pratiques/exemples/README.md @@ -127,7 +127,7 @@ PATTERN 7 : ITERATOR ### 03_03_patterns_structurels.py - **Section** : 12.3 - Patterns de conception courants -- **Description** : Patterns structurels et pythoniques - Decorator (classe cafe avec MilkDecorator/SugarDecorator/WhippedCreamDecorator, decorateurs de fonctions timer/logger/validator, decorateurs de permissions), Adapter (media player et APIs meteo), Repository (InMemoryUserRepository avec UserService), Context Manager (timer, transaction base de donnees, fichier temporaire) +- **Description** : Patterns structurels et pythoniques - Decorator (classe cafe avec MilkDecorator/SugarDecorator/WhippedCreamDecorator, decorateurs de fonctions timer/logger/validator, decorateurs de permissions), Adapter (media player et APIs meteo), Repository (`InMemoryUserRepository` et `DatabaseUserRepository` simule, `UserService` interchangeable), Context Manager (classe `__enter__`/`__exit__` avec `FileHandler`, timer, transaction base de donnees, fichier temporaire) - **Fichier source** : `03-patterns-de-conception.md` - **Sortie attendue** : ``` @@ -175,10 +175,17 @@ PATTERN 9 : REPOSITORY Recherche id=1 : User(id=1, name='Alice', email='alice@example.com') Apres suppression id=2 : 1 utilisateur(s) + --- Meme UserService avec un depot SQL (simule) --- + SQL: INSERT INTO users (name, email) VALUES ('Charlie', 'charlie@example.com') + SQL: SELECT * FROM users WHERE id = 1 + ================================================== PATTERN 10 : CONTEXT MANAGER ================================================== + Ouverture de /tmp/temp_filehandler_test.txt + Fermeture de /tmp/temp_filehandler_test.txt + Debut de operation Traitement en cours... operation termine en X.XXXXs @@ -203,7 +210,7 @@ PATTERN 10 : CONTEXT MANAGER ### 04_01_optimisation_performances.py - **Section** : 12.4 - Optimisation des performances -- **Description** : Mesure du temps (time, timeit, decorateur), profiling (cProfile avec pstats), structures de donnees (list vs set, defaultdict, Counter, deque), optimisations de code (comprehensions vs boucles vs map, concatenation strings join vs +=, deduplication O(n^2) vs O(n)), generateurs vs listes (comparaison memoire), caching (cache dict, lru_cache, @cache pour fibonacci), optimisation memoire avec __slots__, asyncio.gather, resume des optimisations +- **Description** : Mesure du temps (time, timeit, decorateur), profiling (cProfile avec pstats), structures de donnees (list vs set, defaultdict, Counter, deque), optimisations de code (comprehensions vs boucles vs map, concatenation strings join vs +=, deduplication O(n^2) vs O(n), optimisation des boucles avec invariant hors boucle et enumerate), generateurs vs listes (comparaison memoire), caching (cache dict, lru_cache, @cache pour fibonacci), optimisation memoire avec __slots__, asyncio.gather, resume des optimisations - **Fichier source** : `04-optimisation-performances.md` - **Sortie attendue** : ``` @@ -260,6 +267,10 @@ OPTIMISATIONS CODE Deduplication O(n2) : X.XXXXs (250 doublons) Deduplication O(n) : X.XXXXXXs (250 doublons) + Invariant DANS la boucle : X.XXXXs + Invariant HORS boucle : X.XXXXs + enumerate : ['0:a', '1:b', '2:c'] + ================================================== GENERATEURS vs LISTES ================================================== @@ -303,15 +314,15 @@ ASYNCIO RESUME DES OPTIMISATIONS ================================================== - Technique Changement Gain + Technique Changement Gain ----------------------------------------------------------------- - Recherche list -> set O(n) -> O(1) - Comprehension boucle -> [...] ~1.5x plus rapide - Strings += -> join() ~10x plus rapide - Cache recalcul -> lru_cache N fois plus rapide - Memoire list -> generator MB -> bytes - Classes normal -> __slots__ ~40% moins memoire - Parallele sequentiel -> asyncio Nx plus rapide (I/O) + Recherche list -> set O(n) -> O(1) + Comprehension boucle -> [...] ~1.5x plus rapide + Strings += -> join() ~10x plus rapide + Cache recalcul -> lru_cache N fois plus rapide + Memoire list -> generator MB -> bytes + Classes normal -> __slots__ ~40% moins memoire + Parallele sequentiel -> asyncio Nx plus rapide (I/O) ``` --- diff --git a/13-introduction-data-science/01.1-arrays-et-operations-vectorisees.md b/13-introduction-data-science/01.1-arrays-et-operations-vectorisees.md index 8b62173..40cbd9d 100644 --- a/13-introduction-data-science/01.1-arrays-et-operations-vectorisees.md +++ b/13-introduction-data-science/01.1-arrays-et-operations-vectorisees.md @@ -97,6 +97,8 @@ random_arr = np.random.random((2, 3)) # Matrice 2x3 de valeurs aléatoires entr print("Random:\n", random_arr) ``` +> **API aléatoire moderne** : NumPy recommande désormais le générateur `np.random.default_rng()` plutôt que les fonctions historiques `np.random.*`. Par exemple : `rng = np.random.default_rng()` puis `rng.random((2, 3))`, `rng.integers(0, 10, size=5)`, `rng.standard_normal(100)`. L'API historique (`np.random.random`, `np.random.randint`, `np.random.randn`...) reste valide et très répandue ; ce cours l'emploie par simplicité, mais le générateur moderne est préférable (flux indépendants, meilleure reproductibilité). + ## Propriétés des arrays Les arrays NumPy possèdent plusieurs attributs importants : @@ -228,7 +230,7 @@ print("Minimum:", np.min(arr)) # 1 print("Maximum:", np.max(arr)) # 9 # Écart-type -print("Écart-type:", np.std(arr)) # 3.08... +print("Écart-type:", np.std(arr)) # 3.11... # Médiane print("Médiane:", np.median(arr)) # 5.0 @@ -277,6 +279,8 @@ print("Résultat du broadcasting:\n", resultat) # [17 28 39]] ``` +> **Comment NumPy décide-t-il ?** Le broadcasting suit deux règles simples : NumPy **aligne les formes par la droite**, puis, dimension par dimension, deux tailles sont compatibles si elles sont **égales** ou si **l'une vaut 1** (la dimension de taille 1 est alors « étirée » pour correspondre à l'autre). Ici, la matrice a la forme `(3, 3)` et le vecteur `(3,)` ; ce dernier est traité comme `(1, 3)`, donc sa ligne unique est répétée sur les 3 lignes de la matrice — d'où l'addition ligne par ligne. Pour ajouter au contraire un vecteur **colonne par colonne**, il faudrait lui donner la forme `(3, 1)` (par exemple avec `vecteur.reshape(3, 1)`). + ### Exemple pratique : normalisation de données Le broadcasting est très utile pour normaliser des données : @@ -403,6 +407,13 @@ print(f"Temps avec NumPy: {temps_numpy:.4f} secondes") print(f"NumPy est {temps_liste/temps_numpy:.1f}x plus rapide!") ``` +> **Le secret de NumPy : la mémoire contiguë.** Pourquoi le « code C » est-il possible ici, et pas pour une liste ? Parce que les deux structures sont rangées très différemment en mémoire : +> +> - une **liste Python** est un tableau de **pointeurs** vers des objets dispersés : chaque élément est un objet Python complet (avec son type, son compteur de références, sa valeur). Multiplier la liste oblige l'interpréteur, *pour chaque élément*, à suivre un pointeur, vérifier le type, extraire la valeur, calculer, puis recréer un objet ; +> - un **array NumPy** est un **unique bloc de mémoire contiguë** contenant les valeurs brutes, toutes du **même type** (`dtype`). NumPy peut alors parcourir ce bloc d'un seul élan en C, sans indirection ni vérification de type, en profitant de la mémoire cache du processeur (et souvent d'instructions vectorielles SIMD). +> +> C'est cette homogénéité de type et cette contiguïté qui rendent la vectorisation 10 à 100 fois plus rapide — et c'est aussi la raison pour laquelle un array impose un type unique, là où une liste accepte des types mélangés. + ## Bonnes pratiques 1. **Éviter les boucles** : Privilégiez toujours les opérations vectorisées diff --git a/13-introduction-data-science/01.2-indexation-slicing-avances.md b/13-introduction-data-science/01.2-indexation-slicing-avances.md index d8977ae..3003424 100644 --- a/13-introduction-data-science/01.2-indexation-slicing-avances.md +++ b/13-introduction-data-science/01.2-indexation-slicing-avances.md @@ -239,7 +239,7 @@ print("Valeurs entre 15 et 35:", arr[(arr >= 15) & (arr <= 35)]) # Opérateur OU : | print("Valeurs < 15 ou > 35:", arr[(arr < 15) | (arr > 35)]) -# [10 5 40] +# [10 40 5] # Opérateur NON : ~ print("Valeurs PAS égales à 25:", arr[arr != 25]) @@ -355,7 +355,7 @@ resultat2 = matrice[lignes][:, colonnes] print("Même résultat:\n", resultat2) ``` -### Indexation avec broadcasting +> **`np.ix_` — pourquoi est-il nécessaire ?** Si l'on écrivait directement `matrice[lignes, colonnes]` avec ces deux listes, NumPy les **apparierait** élément par élément (il faudrait qu'elles aient la même longueur, et il renverrait les éléments `(0,1)`, `(2,3)`… — des points isolés, pas une sous-matrice). `np.ix_(lignes, colonnes)` construit au contraire une **grille** qui croise *toutes* les lignes avec *toutes* les colonnes : on obtient bien la sous-matrice rectangulaire (ici 3 lignes × 2 colonnes). La « Méthode 2 » (`matrice[lignes][:, colonnes]`) aboutit au même résultat en deux étapes successives. ```python matrice = np.array([[1, 2, 3, 4], @@ -514,7 +514,7 @@ copie = arr[1:4].copy() print("Copie partage la base:", copie.base is arr) # False # L'array original n'a pas de base -print("Original a une base:", arr.base is None) # True +print("L'original n'a pas de base:", arr.base is None) # True ``` ## Exemples pratiques @@ -642,6 +642,8 @@ image[sombre] = image[sombre] + 50 print("\nImage après éclaircissement des zones sombres:\n", image) ``` +> **`np.clip(valeurs, min, max)`** « rabote » les valeurs pour les maintenir dans un intervalle : tout ce qui est inférieur à `min` devient `min`, tout ce qui dépasse `max` devient `max`. Ici, après avoir ajouté 50 à des pixels pouvant déjà valoir jusqu'à 255, `np.clip(..., 0, 255)` empêche de **dépasser** la valeur maximale d'un pixel sur 8 bits (255) — un réflexe indispensable en traitement d'image. + ## Astuces et pièges à éviter ### ⚠️ Piège 1 : Modifier une vue modifie l'original diff --git a/13-introduction-data-science/02.1-dataframes-et-series.md b/13-introduction-data-science/02.1-dataframes-et-series.md index 8fcb66f..106a7dc 100644 --- a/13-introduction-data-science/02.1-dataframes-et-series.md +++ b/13-introduction-data-science/02.1-dataframes-et-series.md @@ -157,7 +157,7 @@ temperatures = pd.Series([15, 18, 22, 20, 17], index=['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi']) # Accès par index de position -print("Premier élément:", temperatures[0]) # 15 +print("Premier élément:", temperatures.iloc[0]) # 15 # Accès par étiquette print("Température mardi:", temperatures['Mardi']) # 18 @@ -236,6 +236,8 @@ print("Évolution:") print(evolution) ``` +> **Pandas aligne par étiquette, pas par position.** C'est une différence majeure avec NumPy. Quand on additionne deux Series, Pandas ne combine pas les éléments « 1er avec 1er, 2e avec 2e » : il **fait correspondre les index** (`'Produit A'` avec `'Produit A'`…), quel que soit leur ordre. Conséquence : si une étiquette n'existe que d'un seul côté, le résultat vaut `NaN` pour cette ligne. Par exemple, une Series indexée `['A', 'B']` additionnée à une Series indexée `['B', 'C']` donne `NaN` en A, la somme en B, et `NaN` en C. Cet **alignement automatique** évite d'avoir à trier les données au préalable, mais c'est aussi une cause classique de `NaN` inattendus lorsque les index ne coïncident pas. + #### Statistiques descriptives ```python @@ -244,7 +246,7 @@ temperatures = pd.Series([15, 18, 22, 20, 17, 19, 21]) # Statistiques de base print("Moyenne:", temperatures.mean()) # 18.857... print("Médiane:", temperatures.median()) # 19.0 -print("Écart-type:", temperatures.std()) # 2.478... +print("Écart-type:", temperatures.std()) # 2.410... print("Minimum:", temperatures.min()) # 15 print("Maximum:", temperatures.max()) # 22 print("Somme:", temperatures.sum()) # 132 @@ -257,7 +259,7 @@ print(temperatures.describe()) ``` count 7.000000 mean 18.857143 -std 2.478232 +std 2.410295 min 15.000000 25% 17.500000 50% 19.000000 @@ -473,13 +475,15 @@ print("Taille:", df.size) # 16 (4 lignes × 4 colonnes) **Sortie de df.dtypes :** ``` -Nom object -Âge int64 -Ville object -Salaire int64 -dtype: object +Nom str +Âge int64 +Ville str +Salaire int64 +dtype: object ``` +> **Pandas 3.0 — le type `str`.** Depuis pandas 3.0 (janvier 2026), les colonnes de texte ont par défaut le type **`str`** (et non plus `object` comme dans les versions ≤ 2.x) : un type dédié aux chaînes, plus rapide et plus économe en mémoire. La dernière ligne `dtype: object` ne décrit pas vos données mais la **Series renvoyée par `df.dtypes`** elle-même (qui contient des objets décrivant les types) ; elle reste donc `object`. Avec pandas 2.x, vous verriez encore `object` pour `Nom` et `Ville`. + ### Visualisation des données #### head() et tail() @@ -903,6 +907,8 @@ print("\nVentes par produit:") print(ventes_par_produit) ``` +> **`idxmax()` (et `idxmin()`)** renvoie l'**étiquette** (l'index) de la ligne où se trouve la valeur maximale — et non la valeur elle-même, qu'on obtiendrait avec `max()`. C'est ce qui rend possible l'expression `ventes.loc[ventes['Quantite'].idxmax(), 'Produit']` : on récupère d'abord l'index de la vente dont la quantité est la plus élevée, puis on lit le `Produit` situé sur cette ligne. Idéal pour répondre à « quelle ligne contient le maximum ? ». + ### Exemple 2 : Carnet d'adresses ```python @@ -1041,13 +1047,15 @@ resultat = (df[df['Âge'] > 25] [['Nom', 'Salaire']]) ``` -### 5. Attention aux SettingWithCopyWarning +### 5. Attention à l'assignation chaînée (chained assignment) ```python -# ❌ Peut causer des problèmes -# df[df['A'] > 0]['B'] = 5 # Warning ! +# ❌ Assignation chaînée : à éviter +# df[df['A'] > 0]['B'] = 5 +# Avant pandas 3.0 : déclenchait un SettingWithCopyWarning (modification peu fiable). +# Depuis pandas 3.0 (Copy-on-Write activé par défaut) : ne modifie PAS df ! -# ✅ Utiliser .loc[] +# ✅ Utiliser .loc[] en une seule indexation # df.loc[df['A'] > 0, 'B'] = 5 ``` diff --git a/13-introduction-data-science/02.2-nettoyage-transformation.md b/13-introduction-data-science/02.2-nettoyage-transformation.md index 98b1f77..1ce2d2e 100644 --- a/13-introduction-data-science/02.2-nettoyage-transformation.md +++ b/13-introduction-data-science/02.2-nettoyage-transformation.md @@ -39,12 +39,12 @@ print(df) **Sortie :** ``` - Nom Âge Ville Salaire Email -0 Alice 25.0 Paris 35000.0 alice@mail.com -1 Bob NaN Lyon 42000.0 None -2 Charlie 35.0 None NaN charlie@mail.com -3 David 28.0 Toulouse 38000.0 david@mail.com -4 Eve NaN Paris 32000.0 None + Nom Âge Ville Salaire Email +0 Alice 25.0 Paris 35000.0 alice@mail.com +1 Bob NaN Lyon 42000.0 NaN +2 Charlie 35.0 NaN NaN charlie@mail.com +3 David 28.0 Toulouse 38000.0 david@mail.com +4 Eve NaN Paris 32000.0 NaN ``` ### Détecter les valeurs manquantes @@ -171,7 +171,7 @@ print(df_rempli_dict) # Remplir seulement certaines colonnes df_copie = df.copy() -df_copie['Âge'].fillna(0, inplace=True) +df_copie['Âge'] = df_copie['Âge'].fillna(0) print("\nSeulement colonne Âge remplie:") print(df_copie) ``` @@ -453,6 +453,8 @@ print("\nCatégories uniques:") print(df['Ville'].cat.categories) ``` +> **Pourquoi `category` économise-t-il la mémoire ?** Une colonne `object`/`str` stocke chaque chaîne, même répétée des milliers de fois. Le type `category`, lui, range les données en deux parties : la liste des **valeurs uniques** (les *catégories* — ici `Lyon`, `Marseille`, `Paris`, stockées **une seule fois**) et un tableau de petits **entiers** (les *codes*) qui, pour chaque ligne, désigne la catégorie correspondante. Stocker 6000 entiers d'un octet plus 3 chaînes coûte bien moins que 6000 chaînes complètes — d'où le gain. Le type `category` n'est donc avantageux que lorsqu'une colonne contient **peu de valeurs distinctes répétées souvent** (villes, pays, statuts…). + ## Manipulation de chaînes de caractères ### Méthodes de base @@ -707,6 +709,8 @@ print("\nDate + 1 mois:") print(df[['Date_debut', 'Dans_1_mois']]) ``` +> **`pd.Timedelta` ou `pd.DateOffset` ?** `Timedelta` représente une durée **fixe** (jours, heures, minutes, secondes) — idéale pour « + 10 jours ». Mais un mois n'a pas de durée fixe (28 à 31 jours) : on utilise alors `pd.DateOffset(months=1)`, qui effectue un décalage **calendaire** (le 15 janvier devient le 15 février, etc.). En résumé : `Timedelta` pour des durées exactes, `DateOffset` pour des décalages en mois ou en années. + ## Renommage et réorganisation ### Renommer les colonnes @@ -1125,7 +1129,7 @@ df['Client'] = df['Client'].str.strip().str.title() df['Date'] = pd.to_datetime(df['Date']) # 3. Gérer les valeurs manquantes -df['Quantité'].fillna(df['Quantité'].median(), inplace=True) +df['Quantité'] = df['Quantité'].fillna(df['Quantité'].median()) df['Prix'] = df['Prix'].ffill() df.dropna(subset=['Date'], inplace=True) @@ -1270,7 +1274,7 @@ print("\nAprès nettoyage des valeurs impossibles:") print(df) # Remplir avec interpolation -df['Température'].interpolate(inplace=True) +df['Température'] = df['Température'].interpolate() print("\nAprès interpolation:") print(df[['DateTime', 'Température']].head(10)) diff --git a/13-introduction-data-science/02.3-groupby-et-agregations.md b/13-introduction-data-science/02.3-groupby-et-agregations.md index b397a70..43f8615 100644 --- a/13-introduction-data-science/02.3-groupby-et-agregations.md +++ b/13-introduction-data-science/02.3-groupby-et-agregations.md @@ -329,6 +329,8 @@ print("\nAvec rang dans chaque ville:") print(df) ``` +> **`rank(method='dense')`** attribue un **rang** à chaque valeur (1 pour la plus petite, 2 pour la suivante, etc.). L'option `method='dense'` garantit des rangs **consécutifs, sans trou** : en cas d'égalité, les valeurs identiques partagent le même rang et le suivant ne saute pas de numéro (1, 2, 2, 3… plutôt que 1, 2, 2, 4). Combiné à `transform`, ce rang est calculé **indépendamment dans chaque ville**. + ## Filter : Filtrer des groupes entiers La méthode `filter()` permet de conserver ou supprimer des groupes entiers selon une condition. @@ -396,6 +398,8 @@ print("\nAvec valeurs normalisées par groupe:") print(df_norm) ``` +> **Pandas 3.0 — `apply` et colonnes de groupement.** Depuis pandas 3.0, la colonne servant au regroupement (ici `Catégorie`) n'est plus transmise à la fonction appliquée : elle est automatiquement **exclue** du sous-DataFrame reçu et figure dans l'index du résultat. Les trois exemples ci-dessus fonctionnent donc tels quels, car leurs fonctions n'utilisent que la colonne `Valeur`. En pandas 2.2.x, ce changement était annoncé par un *DeprecationWarning* invitant à passer `include_groups=False`. + ## Opérations sur les index ### Grouper par niveau d'index @@ -827,6 +831,8 @@ print("\nAvec moyenne mobile:") print(df) ``` +> **`rolling(3, min_periods=1)`** définit une **fenêtre glissante** : pour chaque ligne, la fonction (`.mean()` ici) est calculée sur les 3 dernières valeurs (la ligne courante et les deux précédentes). `min_periods=1` autorise le calcul même quand la fenêtre n'est pas encore pleine (sur les deux premières lignes, la moyenne porte sur 1 puis 2 valeurs) au lieu de renvoyer `NaN`. Une moyenne mobile « lisse » une série pour en dégager la tendance. + ## Bonnes pratiques ### 1. Choisir la bonne méthode d'agrégation diff --git a/13-introduction-data-science/03.1-graphiques-base-matplotlib.md b/13-introduction-data-science/03.1-graphiques-base-matplotlib.md index e34189b..2d1baca 100644 --- a/13-introduction-data-science/03.1-graphiques-base-matplotlib.md +++ b/13-introduction-data-science/03.1-graphiques-base-matplotlib.md @@ -486,7 +486,7 @@ Dans le prochain chapitre, nous découvrirons **Plotly**, qui permet de créer d ## Ressources supplémentaires -- [Documentation officielle de Matplotlib](https://matplotlib.org/stable/contents.html) +- [Documentation officielle de Matplotlib](https://matplotlib.org/stable/index.html) - [Galerie d'exemples Matplotlib](https://matplotlib.org/stable/gallery/index.html) - [Cheat Sheet Matplotlib](https://github.com/matplotlib/cheatsheets) diff --git a/13-introduction-data-science/04-analyse-exploratoire.md b/13-introduction-data-science/04-analyse-exploratoire.md index b34a506..12f8d40 100644 --- a/13-introduction-data-science/04-analyse-exploratoire.md +++ b/13-introduction-data-science/04-analyse-exploratoire.md @@ -617,6 +617,7 @@ titanic['survived'].value_counts().plot(kind='bar', ax=axes[0]) axes[0].set_title('Distribution de la survie') axes[0].set_xlabel('Survie (0 = Non, 1 = Oui)') axes[0].set_ylabel('Nombre de passagers') +axes[0].set_xticks([0, 1]) axes[0].set_xticklabels(['Décédé', 'Survécu'], rotation=0) # Diagramme circulaire @@ -672,6 +673,7 @@ axes[0].set_ylabel('Fréquence') # Âge par survie sns.boxplot(x='survived', y='age', data=titanic, ax=axes[1]) axes[1].set_title('Distribution de l\'âge par survie') +axes[1].set_xticks([0, 1]) axes[1].set_xticklabels(['Décédé', 'Survécu']) plt.tight_layout() @@ -681,7 +683,9 @@ plt.show() print("\n8. ANALYSE CROISÉE") print("-" * 80) -# Tableau croisé +# Tableau croisé (crosstab) : compte les passagers pour chaque combinaison +# classe x sexe (en lignes) et survie (en colonnes). Contrairement à pivot_table +# qui agrege une valeur, crosstab compte des effectifs ; margins=True ajoute les totaux. cross_tab = pd.crosstab([titanic['pclass'], titanic['sex']], titanic['survived'], margins=True) diff --git a/13-introduction-data-science/README.md b/13-introduction-data-science/README.md index fc278af..d320ba4 100644 --- a/13-introduction-data-science/README.md +++ b/13-introduction-data-science/README.md @@ -55,7 +55,7 @@ Nous vivons dans une époque où les données sont partout : - **90% des données mondiales** ont été créées ces deux dernières années - Chaque jour, nous générons **2.5 quintillions d'octets** de données -- D'ici 2025, on estime que **463 exaoctets** de données seront créés chaque jour +- On estime à environ **463 exaoctets** le volume de données créé chaque jour ### Impact dans le monde réel @@ -929,7 +929,7 @@ Avant de vous lancer dans le Deep Learning : ## Structure de ce chapitre -Ce chapitre est organisé en trois sections principales qui couvrent les fondations de la Data Science en Python : +Ce chapitre est organisé en quatre sections principales qui couvrent les fondations de la Data Science en Python : ### 13.1 Calcul numérique avec NumPy Vous apprendrez : @@ -957,6 +957,15 @@ Vous maîtriserez : **Pourquoi la visualisation ?** "Un graphique vaut mille mots". C'est essentiel pour explorer les données et communiquer les résultats. +### 13.4 Analyse exploratoire des données (EDA) +Vous saurez : +- Explorer et résumer un jeu de données inconnu +- Détecter les valeurs manquantes, les doublons et les valeurs aberrantes +- Visualiser distributions, relations et corrélations +- Synthétiser vos découvertes en insights exploitables + +**Pourquoi l'EDA ?** Avant de modéliser, il faut comprendre ses données. L'analyse exploratoire est l'étape qui transforme un fichier brut en connaissances utiles, et conditionne la réussite de tout ce qui suit. + ## Conclusion de l'introduction La Data Science est un domaine passionnant qui combine créativité, logique et impact réel. Python et son écosystème riche font de ce voyage d'apprentissage à la fois accessible et puissant. diff --git a/13-introduction-data-science/exemples/01_01_numpy_arrays_operations.py b/13-introduction-data-science/exemples/01_01_numpy_arrays_operations.py index d303ca1..c9bbd92 100644 --- a/13-introduction-data-science/exemples/01_01_numpy_arrays_operations.py +++ b/13-introduction-data-science/exemples/01_01_numpy_arrays_operations.py @@ -35,7 +35,7 @@ print(f" NumPy est {temps_liste/temps_numpy:.1f}x plus rapide!") # --- Simplicite du code --- -print(f"\n --- Simplicite ---") +print("\n --- Simplicite ---") liste1 = [1, 2, 3, 4, 5] liste2 = [10, 20, 30, 40, 50] resultat_liste = [] @@ -83,7 +83,7 @@ print(f" Array 3D:\n{arr_3d}") # --- Fonctions de creation --- -print(f"\n --- Fonctions de creation ---") +print("\n --- Fonctions de creation ---") zeros = np.zeros(5) print(f" Zeros: {zeros}") @@ -104,7 +104,7 @@ print(f" Random:\n{random_arr}") # --- Types de donnees --- -print(f"\n --- Types de donnees ---") +print("\n --- Types de donnees ---") arr_int = np.array([1, 2, 3], dtype=np.int32) print(f" Type int32: {arr_int.dtype}") @@ -162,7 +162,7 @@ print(f" Puissance (^2): {arr ** 2}") # --- Operations entre arrays --- -print(f"\n --- Operations entre arrays ---") +print("\n --- Operations entre arrays ---") arr1 = np.array([1, 2, 3, 4]) arr2 = np.array([10, 20, 30, 40]) @@ -207,7 +207,7 @@ print(f" Mediane: {np.median(arr)}") # --- Agregations 2D --- -print(f"\n --- Agregations 2D ---") +print("\n --- Agregations 2D ---") arr_2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) @@ -235,7 +235,7 @@ print(f"\n Resultat du broadcasting:\n{resultat}") # --- Normalisation --- -print(f"\n --- Normalisation ---") +print("\n --- Normalisation ---") notes = np.array([[85, 90, 78], [92, 88, 95], [78, 85, 88]]) @@ -269,7 +269,7 @@ print(f" Array modifie (< 10 -> 0): {arr_copie}") # --- Conditions multiples --- -print(f"\n --- Conditions multiples ---") +print("\n --- Conditions multiples ---") arr = np.array([1, 5, 10, 15, 20, 25, 30]) masque = (arr >= 10) & (arr <= 20) @@ -287,14 +287,14 @@ print("=" * 50) # --- Temperatures --- -print(f"\n --- Conversion temperatures ---") +print("\n --- Conversion temperatures ---") temperatures_celsius = np.array([0, 10, 20, 25, 30, 35, 40]) print(f" Temperatures en Celsius: {temperatures_celsius}") temperatures_fahrenheit = temperatures_celsius * 9/5 + 32 print(f" Temperatures en Fahrenheit: {temperatures_fahrenheit}") -print(f"\n --- Statistiques ---") +print("\n --- Statistiques ---") print(f" Temperature moyenne: {np.mean(temperatures_celsius):.1f} C") print(f" Temperature minimale: {np.min(temperatures_celsius)} C") print(f" Temperature maximale: {np.max(temperatures_celsius)} C") @@ -305,19 +305,19 @@ print(f" Nombre de jours chauds: {len(jours_chauds)}") # --- Normalisation --- -print(f"\n --- Normalisation de donnees ---") +print("\n --- Normalisation de donnees ---") donnees = np.array([10, 20, 30, 40, 50]) donnees_normalisees = (donnees - np.mean(donnees)) / np.std(donnees) print(f" Donnees normalisees: {donnees_normalisees}") # --- Analyse financiere --- -print(f"\n --- Analyse financiere ---") +print("\n --- Analyse financiere ---") prix = np.array([100, 102, 98, 105, 107]) rendements = (prix[1:] - prix[:-1]) / prix[:-1] * 100 print(f" Rendements quotidiens (%): {rendements}") # --- Application remise --- -print(f"\n --- Application remise ---") +print("\n --- Application remise ---") prix = np.array([19.99, 49.99, 99.99, 149.99]) prix_reduits = prix * 0.8 print(f" Prix originaux: {prix}") @@ -326,7 +326,7 @@ print(f" Prix arrondis: {prix_reduits_arrondis}") # --- Signal sinusoidal --- -print(f"\n --- Signal sinusoidal ---") +print("\n --- Signal sinusoidal ---") t = np.linspace(0, 1, 100) frequence = 5 signal = np.sin(2 * np.pi * frequence * t) diff --git a/13-introduction-data-science/exemples/01_02_numpy_indexation_slicing.py b/13-introduction-data-science/exemples/01_02_numpy_indexation_slicing.py index dde88b8..dc47c8c 100644 --- a/13-introduction-data-science/exemples/01_02_numpy_indexation_slicing.py +++ b/13-introduction-data-science/exemples/01_02_numpy_indexation_slicing.py @@ -271,7 +271,7 @@ [7, 8, 9]]) lignes, colonnes = np.where(matrice > 5) -print(f"\n Positions ou valeur > 5:") +print("\n Positions ou valeur > 5:") print(f" Lignes: {lignes}") print(f" Colonnes: {colonnes}") print(f" Valeurs: {matrice[lignes, colonnes]}") @@ -351,7 +351,7 @@ copie = arr[1:4].copy() print(f" Copie partage la base: {copie.base is arr}") -print(f" Original a une base: {arr.base is None}") +print(f" L'original n'a pas de base: {arr.base is None}") # ============================================================ @@ -362,7 +362,7 @@ print("=" * 50) # --- Normalisation min-max --- -print(f"\n --- Normalisation min-max ---") +print("\n --- Normalisation min-max ---") donnees = np.array([10, 25, 15, 30, 20, 35]) min_val = np.min(donnees) max_val = np.max(donnees) @@ -370,7 +370,7 @@ print(f" Donnees normalisees: {donnees_normalisees}") # --- Remplacement conditionnel --- -print(f"\n --- Remplacement conditionnel ---") +print("\n --- Remplacement conditionnel ---") donnees = np.array([15, 18, 200, 19, -50, 17, 20, 16], dtype=float) seuil_bas = 10 seuil_haut = 100 @@ -381,7 +381,7 @@ print(f" Donnees nettoyees: {donnees_clean}") # --- Extraction de sous-matrices --- -print(f"\n --- Scores etudiants ---") +print("\n --- Scores etudiants ---") scores = np.array([[85, 90, 78, 92], [88, 75, 95, 87], [70, 85, 80, 88], @@ -401,7 +401,7 @@ print(f"\n Etudiants d'elite (moyenne > 85):\n{elite}") # --- Grille de donnees --- -print(f"\n --- Grille de donnees ---") +print("\n --- Grille de donnees ---") x = np.arange(0, 5) y = np.arange(0, 3) X, Y = np.meshgrid(x, y) @@ -412,6 +412,6 @@ print(f" Distances:\n{np.round(distances, 2)}") masque = distances < 2.5 -print(f"\n Points proches (distance < 2.5):") +print("\n Points proches (distance < 2.5):") print(f" X: {X[masque]}") print(f" Y: {Y[masque]}") diff --git a/13-introduction-data-science/exemples/02_01_pandas_dataframes_series.py b/13-introduction-data-science/exemples/02_01_pandas_dataframes_series.py index 0de8f32..7cb51e4 100644 --- a/13-introduction-data-science/exemples/02_01_pandas_dataframes_series.py +++ b/13-introduction-data-science/exemples/02_01_pandas_dataframes_series.py @@ -44,7 +44,7 @@ print(f"\n Series constante:\n{serie_constante}") # --- Acces --- -print(f"\n --- Acces ---") +print("\n --- Acces ---") temperatures = pd.Series([15, 18, 22, 20, 17], index=['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi']) @@ -54,7 +54,7 @@ print(f" Lundi a Mercredi:\n{temperatures['Lundi':'Mercredi']}") # --- Proprietes --- -print(f"\n --- Proprietes ---") +print("\n --- Proprietes ---") print(f" Valeurs: {temperatures.values}") print(f" Index: {temperatures.index.tolist()}") print(f" Type: {temperatures.dtype}") @@ -62,7 +62,7 @@ print(f" Forme: {temperatures.shape}") # --- Operations --- -print(f"\n --- Operations arithmetiques ---") +print("\n --- Operations arithmetiques ---") print(f" Temperatures + 5:\n{temperatures + 5}") print(f" En Fahrenheit:\n{temperatures * 9/5 + 32}") @@ -76,7 +76,7 @@ print(f" Evolution:\n{evolution}") # --- Statistiques --- -print(f"\n --- Statistiques ---") +print("\n --- Statistiques ---") temperatures = pd.Series([15, 18, 22, 20, 17, 19, 21]) print(f" Moyenne: {temperatures.mean():.6f}") print(f" Mediane: {temperatures.median()}") @@ -87,7 +87,7 @@ print(f"\n describe():\n{temperatures.describe()}") # --- Filtrage --- -print(f"\n --- Filtrage ---") +print("\n --- Filtrage ---") temperatures = pd.Series([15, 18, 22, 20, 17], index=['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi']) @@ -139,7 +139,7 @@ print(f"\n Avec index personnalise:\n{df}") # --- Proprietes --- -print(f"\n --- Proprietes ---") +print("\n --- Proprietes ---") df = pd.DataFrame({ 'Nom': ['Alice', 'Bob', 'Charlie', 'David'], 'Age': [25, 30, 35, 28], @@ -357,7 +357,7 @@ print("=" * 50) # --- Analyse de ventes --- -print(f"\n --- Analyse de ventes ---") +print("\n --- Analyse de ventes ---") ventes = pd.DataFrame({ 'Date': ['2024-01-01', '2024-01-02', '2024-01-03', '2024-01-04', '2024-01-05'], 'Produit': ['Laptop', 'Souris', 'Clavier', 'Laptop', 'Ecran'], @@ -375,7 +375,7 @@ print(f"\n Ventes par produit:\n{ventes_par_produit}") # --- Notes d'etudiants --- -print(f"\n --- Notes d'etudiants ---") +print("\n --- Notes d'etudiants ---") notes = pd.DataFrame({ 'Etudiant': ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve'], 'Math': [15, 12, 18, 14, 16], @@ -396,7 +396,7 @@ print(f"\n Etudiants avec mention (>=15):\n{mentions[['Etudiant', 'Moyenne']]}") # --- Suivi d'activite --- -print(f"\n --- Suivi d'activite physique ---") +print("\n --- Suivi d'activite physique ---") activite = pd.DataFrame({ 'Jour': ['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche'], 'Sport': ['Course', 'Repos', 'Velo', 'Course', 'Repos', 'Natation', 'Randonnee'], diff --git a/13-introduction-data-science/exemples/02_02_pandas_nettoyage_transformation.py b/13-introduction-data-science/exemples/02_02_pandas_nettoyage_transformation.py index bdfeaf5..7e546ca 100644 --- a/13-introduction-data-science/exemples/02_02_pandas_nettoyage_transformation.py +++ b/13-introduction-data-science/exemples/02_02_pandas_nettoyage_transformation.py @@ -35,7 +35,7 @@ print(f" Lignes avec NaN: {len(df[df.isnull().any(axis=1)])}") # --- Suppression --- -print(f"\n --- Suppression ---") +print("\n --- Suppression ---") df = pd.DataFrame({ 'A': [1, 2, np.nan, 4], 'B': [5, np.nan, np.nan, 8], @@ -55,7 +55,7 @@ print(f"\n dropna(axis=1, how='all'):\n{df.dropna(axis=1, how='all')}") # --- Remplissage --- -print(f"\n --- Remplissage ---") +print("\n --- Remplissage ---") df = pd.DataFrame({ 'Nom': ['Alice', 'Bob', 'Charlie'], 'Age': [25, np.nan, 35], @@ -75,7 +75,7 @@ print(f"\n fillna(median):\n{df.fillna(df.median())}") # --- Propagation --- -print(f"\n --- Propagation ---") +print("\n --- Propagation ---") df = pd.DataFrame({'Valeur': [1, np.nan, np.nan, 4, np.nan, 6]}) print(f" Original:\n{df}") print(f"\n ffill:\n{df.ffill()}") @@ -83,11 +83,11 @@ print(f"\n ffill(limit=1):\n{df.ffill(limit=1)}") # --- Interpolation --- -print(f"\n --- Interpolation ---") +print("\n --- Interpolation ---") print(f" Interpolation lineaire:\n{df.interpolate()}") # --- Remplacement valeurs invalides --- -print(f"\n --- Remplacement ---") +print("\n --- Remplacement ---") df = pd.DataFrame({ 'Age': [25, -1, 35, 999, 28], 'Ville': ['Paris', 'N/A', 'Lyon', 'Inconnu', 'Marseille'], @@ -409,7 +409,7 @@ def categoriser_age(age): print("=" * 50) # --- Nettoyage ventes --- -print(f"\n --- Nettoyage dataset ventes ---") +print("\n --- Nettoyage dataset ventes ---") df = pd.DataFrame({ 'Date': ['2024-01-01', '2024-01-02', None, '2024-01-04', '2024-01-05'], 'Produit': ['Laptop', 'Souris', 'Clavier', 'Laptop', 'Souris'], @@ -432,7 +432,7 @@ def categoriser_age(age): print(f"\n Ventes par client:\n{df.groupby('Client')['Montant'].sum()}") # --- Combinaison sources --- -print(f"\n --- Combinaison de sources ---") +print("\n --- Combinaison de sources ---") ventes_jan = pd.DataFrame({ 'Produit': ['A', 'B', 'C'], 'Ventes': [100, 150, 200], diff --git a/13-introduction-data-science/exemples/02_03_pandas_groupby_agregations.py b/13-introduction-data-science/exemples/02_03_pandas_groupby_agregations.py index ce01490..132cf86 100644 --- a/13-introduction-data-science/exemples/02_03_pandas_groupby_agregations.py +++ b/13-introduction-data-science/exemples/02_03_pandas_groupby_agregations.py @@ -36,7 +36,7 @@ print(f"\n Nombre de groupes: {groupe.ngroups}") print(f" Noms des groupes: {list(groupe.groups.keys())}") -print(f"\n Contenu des groupes:") +print("\n Contenu des groupes:") for nom, groupe_df in groupe: print(f" --- {nom} ---") print(f"{groupe_df}") @@ -289,7 +289,7 @@ def stats_groupe(groupe): print("=" * 50) # --- Analyse ventes par region --- -print(f"\n --- Analyse ventes par region ---") +print("\n --- Analyse ventes par region ---") ventes = pd.DataFrame({ 'Date': pd.date_range('2024-01-01', periods=12, freq='MS'), 'Region': ['Nord', 'Sud', 'Est', 'Ouest'] * 3, @@ -312,7 +312,7 @@ def stats_groupe(groupe): print(f"\n Pourcentage par region:\n{pct}") # --- Notes etudiants --- -print(f"\n --- Notes etudiants ---") +print("\n --- Notes etudiants ---") notes = pd.DataFrame({ 'Etudiant': ['Alice', 'Bob', 'Charlie', 'Alice', 'Bob', 'Charlie'] * 2, 'Matiere': ['Math', 'Math', 'Math', 'Physique', 'Physique', 'Physique'] * 2, @@ -337,7 +337,7 @@ def stats_groupe(groupe): print(f"\n Progression T1 -> T2:\n{notes_pivot}") # --- Analyse RH --- -print(f"\n --- Analyse RH ---") +print("\n --- Analyse RH ---") employes = pd.DataFrame({ 'Nom': ['Alice', 'Bob', 'Charlie', 'David', 'Eve', 'Frank', 'Grace', 'Henry'], 'Departement': ['IT', 'Ventes', 'IT', 'RH', 'Ventes', 'IT', 'RH', 'Ventes'], diff --git a/13-introduction-data-science/exemples/03_01_matplotlib_graphiques_base.py b/13-introduction-data-science/exemples/03_01_matplotlib_graphiques_base.py index 916c5c5..c370de4 100644 --- a/13-introduction-data-science/exemples/03_01_matplotlib_graphiques_base.py +++ b/13-introduction-data-science/exemples/03_01_matplotlib_graphiques_base.py @@ -47,7 +47,7 @@ plt.title('Mon premier graphique') plt.savefig(os.path.join(OUTPUT_DIR, '01_line_simple.png'), dpi=100, bbox_inches='tight') plt.close() -print(f"\n 01_line_simple.png sauvegarde") +print("\n 01_line_simple.png sauvegarde") # --- Personnalisation --- x = np.linspace(0, 10, 100) @@ -63,7 +63,7 @@ plt.grid(True) plt.savefig(os.path.join(OUTPUT_DIR, '02_line_trigo.png'), dpi=100, bbox_inches='tight') plt.close() -print(f" 02_line_trigo.png sauvegarde") +print(" 02_line_trigo.png sauvegarde") # ============================================================ @@ -84,7 +84,7 @@ plt.title('Ventes par produit') plt.savefig(os.path.join(OUTPUT_DIR, '03_bar_vertical.png'), dpi=100, bbox_inches='tight') plt.close() -print(f"\n 03_bar_vertical.png sauvegarde") +print("\n 03_bar_vertical.png sauvegarde") # --- Barres horizontales --- plt.figure(figsize=(10, 6)) @@ -94,7 +94,7 @@ plt.title('Ventes par produit (horizontal)') plt.savefig(os.path.join(OUTPUT_DIR, '04_bar_horizontal.png'), dpi=100, bbox_inches='tight') plt.close() -print(f" 04_bar_horizontal.png sauvegarde") +print(" 04_bar_horizontal.png sauvegarde") # --- Barres groupees --- categories = ['Q1', 'Q2', 'Q3', 'Q4'] @@ -117,7 +117,7 @@ plt.savefig(os.path.join(OUTPUT_DIR, '05_bar_groupees.png'), dpi=100, bbox_inches='tight') plt.close() -print(f" 05_bar_groupees.png sauvegarde") +print(" 05_bar_groupees.png sauvegarde") # ============================================================ @@ -140,7 +140,7 @@ plt.grid(True, alpha=0.3) plt.savefig(os.path.join(OUTPUT_DIR, '06_scatter_simple.png'), dpi=100, bbox_inches='tight') plt.close() -print(f"\n 06_scatter_simple.png sauvegarde") +print("\n 06_scatter_simple.png sauvegarde") # --- Couleurs et tailles variables --- np.random.seed(42) @@ -158,7 +158,7 @@ plt.title('Nuage de points avec couleurs et tailles variables') plt.savefig(os.path.join(OUTPUT_DIR, '07_scatter_avance.png'), dpi=100, bbox_inches='tight') plt.close() -print(f" 07_scatter_avance.png sauvegarde") +print(" 07_scatter_avance.png sauvegarde") # ============================================================ @@ -180,7 +180,7 @@ plt.grid(True, alpha=0.3) plt.savefig(os.path.join(OUTPUT_DIR, '08_hist_simple.png'), dpi=100, bbox_inches='tight') plt.close() -print(f"\n 08_hist_simple.png sauvegarde") +print("\n 08_hist_simple.png sauvegarde") # --- Histogrammes multiples --- np.random.seed(42) @@ -196,7 +196,7 @@ plt.legend() plt.savefig(os.path.join(OUTPUT_DIR, '09_hist_multiples.png'), dpi=100, bbox_inches='tight') plt.close() -print(f" 09_hist_multiples.png sauvegarde") +print(" 09_hist_multiples.png sauvegarde") # ============================================================ @@ -215,7 +215,7 @@ plt.title('Langages de programmation les plus utilises') plt.savefig(os.path.join(OUTPUT_DIR, '10_pie_simple.png'), dpi=100, bbox_inches='tight') plt.close() -print(f"\n 10_pie_simple.png sauvegarde") +print("\n 10_pie_simple.png sauvegarde") # --- Avec personnalisation --- couleurs = ['#ff9999', '#66b3ff', '#99ff99', '#ffcc99', '#ff99cc'] @@ -228,7 +228,7 @@ plt.axis('equal') plt.savefig(os.path.join(OUTPUT_DIR, '11_pie_personnalise.png'), dpi=100, bbox_inches='tight') plt.close() -print(f" 11_pie_personnalise.png sauvegarde") +print(" 11_pie_personnalise.png sauvegarde") # ============================================================ @@ -262,7 +262,7 @@ plt.tight_layout() plt.savefig(os.path.join(OUTPUT_DIR, '12_subplots.png'), dpi=100, bbox_inches='tight') plt.close() -print(f"\n 12_subplots.png sauvegarde") +print("\n 12_subplots.png sauvegarde") # ============================================================ @@ -286,7 +286,7 @@ plt.title('Graphique avec style ggplot') plt.savefig(os.path.join(OUTPUT_DIR, '13_style_ggplot.png'), dpi=100, bbox_inches='tight') plt.close() -print(f" 13_style_ggplot.png sauvegarde") +print(" 13_style_ggplot.png sauvegarde") # ============================================================ @@ -303,7 +303,7 @@ plt.savefig(os.path.join(OUTPUT_DIR, '14_sauvegarde.png'), dpi=300, bbox_inches='tight') plt.close() -print(f"\n 14_sauvegarde.png sauvegarde (dpi=300)") +print("\n 14_sauvegarde.png sauvegarde (dpi=300)") # ============================================================ @@ -371,7 +371,7 @@ print(f"\n Ventes par trimestre: Q1={q1}, Q2={q2}, Q3={q3}, Q4={q4}") print(f" Total annuel: {sum(ventes)}") print(f" Mois au-dessus de l'objectif: {sum(1 for v in ventes if v >= 100)}/{len(ventes)}") -print(f"\n 15_dashboard_complet.png sauvegarde") +print("\n 15_dashboard_complet.png sauvegarde") # --- Resume des fichiers generes --- print(f"\n{'=' * 50}") diff --git a/13-introduction-data-science/exemples/03_02_plotly_visualisations_interactives.py b/13-introduction-data-science/exemples/03_02_plotly_visualisations_interactives.py index 1e240f3..5366c2b 100644 --- a/13-introduction-data-science/exemples/03_02_plotly_visualisations_interactives.py +++ b/13-introduction-data-science/exemples/03_02_plotly_visualisations_interactives.py @@ -47,7 +47,7 @@ title='Fonction sinus', labels={'x': 'Angle (radians)', 'y': 'sin(x)'}) fig.write_html(os.path.join(OUTPUT_DIR, '01_line_simple.html')) -print(f"\n 01_line_simple.html sauvegarde") +print("\n 01_line_simple.html sauvegarde") # --- Multi-lignes --- x = np.linspace(0, 10, 100) @@ -64,7 +64,7 @@ title='Fonctions trigonometriques', labels={'valeur': 'f(x)', 'x': 'Angle (radians)'}) fig.write_html(os.path.join(OUTPUT_DIR, '02_line_multi.html')) -print(f" 02_line_multi.html sauvegarde") +print(" 02_line_multi.html sauvegarde") # --- Avec markers --- x = np.arange(0, 10) @@ -75,7 +75,7 @@ title='Fonction quadratique') fig.update_traces(marker=dict(size=10)) fig.write_html(os.path.join(OUTPUT_DIR, '03_line_markers.html')) -print(f" 03_line_markers.html sauvegarde") +print(" 03_line_markers.html sauvegarde") # ============================================================ @@ -95,7 +95,7 @@ title='Nuage de points interactif', labels={'x': 'Variable X', 'y': 'Variable Y'}) fig.write_html(os.path.join(OUTPUT_DIR, '04_scatter_simple.html')) -print(f"\n 04_scatter_simple.html sauvegarde") +print("\n 04_scatter_simple.html sauvegarde") # --- Avec couleurs et tailles variables --- np.random.seed(42) @@ -113,7 +113,7 @@ hover_data=['taille'], title='Nuage de points avec couleurs et tailles') fig.write_html(os.path.join(OUTPUT_DIR, '05_scatter_avance.html')) -print(f" 05_scatter_avance.html sauvegarde") +print(" 05_scatter_avance.html sauvegarde") # --- Dataset Iris --- df_iris = px.data.iris() @@ -124,7 +124,7 @@ hover_data=['petal_width'], title='Dataset Iris - Analyse multidimensionnelle') fig.write_html(os.path.join(OUTPUT_DIR, '06_scatter_iris.html')) -print(f" 06_scatter_iris.html sauvegarde") +print(" 06_scatter_iris.html sauvegarde") print(f" Dataset Iris: {len(df_iris)} lignes, {len(df_iris.columns)} colonnes") print(f" Especes: {list(df_iris['species'].unique())}") @@ -146,7 +146,7 @@ color=ventes, color_continuous_scale='blues') fig.write_html(os.path.join(OUTPUT_DIR, '07_bar_vertical.html')) -print(f"\n 07_bar_vertical.html sauvegarde") +print("\n 07_bar_vertical.html sauvegarde") # --- Barres groupees --- df = pd.DataFrame({ @@ -159,7 +159,7 @@ barmode='group', title='Comparaison des ventes 2023 vs 2024') fig.write_html(os.path.join(OUTPUT_DIR, '08_bar_groupees.html')) -print(f" 08_bar_groupees.html sauvegarde") +print(" 08_bar_groupees.html sauvegarde") # --- Barres empilees --- df = pd.DataFrame({ @@ -174,7 +174,7 @@ barmode='stack', title='Ventes par region et par mois') fig.write_html(os.path.join(OUTPUT_DIR, '09_bar_empilees.html')) -print(f" 09_bar_empilees.html sauvegarde") +print(" 09_bar_empilees.html sauvegarde") # --- Barres horizontales --- langages = ['Python', 'JavaScript', 'Java', 'C#', 'C++', 'PHP', 'TypeScript'] @@ -187,7 +187,7 @@ color=popularite, color_continuous_scale='viridis') fig.write_html(os.path.join(OUTPUT_DIR, '10_bar_horizontal.html')) -print(f" 10_bar_horizontal.html sauvegarde") +print(" 10_bar_horizontal.html sauvegarde") # ============================================================ @@ -206,7 +206,7 @@ title='Distribution normale', labels={'x': 'Valeur', 'y': 'Frequence'}) fig.write_html(os.path.join(OUTPUT_DIR, '11_hist_simple.html')) -print(f"\n 11_hist_simple.html sauvegarde") +print("\n 11_hist_simple.html sauvegarde") # --- Superposes --- np.random.seed(42) @@ -224,7 +224,7 @@ barmode='overlay', opacity=0.6) fig.write_html(os.path.join(OUTPUT_DIR, '12_hist_superposes.html')) -print(f" 12_hist_superposes.html sauvegarde") +print(" 12_hist_superposes.html sauvegarde") # ============================================================ @@ -248,7 +248,7 @@ title='Distribution des valeurs par categorie', color='Categorie') fig.write_html(os.path.join(OUTPUT_DIR, '13_boxplot.html')) -print(f"\n 13_boxplot.html sauvegarde") +print("\n 13_boxplot.html sauvegarde") print(f" Medianes: A={df[df['Categorie']=='A']['Valeurs'].median():.1f}, " f"B={df[df['Categorie']=='B']['Valeurs'].median():.1f}, " f"C={df[df['Categorie']=='C']['Valeurs'].median():.1f}") @@ -269,7 +269,7 @@ title='Langages de programmation les plus utilises', hole=0.3) fig.write_html(os.path.join(OUTPUT_DIR, '14_pie_donut.html')) -print(f"\n 14_pie_donut.html sauvegarde") +print("\n 14_pie_donut.html sauvegarde") # --- Pie avec Graph Objects --- labels = ['Python', 'JavaScript', 'Java', 'C++', 'Autres'] @@ -287,7 +287,7 @@ fig.update_layout(title='Langages de programmation') fig.write_html(os.path.join(OUTPUT_DIR, '15_pie_go.html')) -print(f" 15_pie_go.html sauvegarde") +print(" 15_pie_go.html sauvegarde") # ============================================================ @@ -311,7 +311,7 @@ color='couleur', title='Nuage de points 3D interactif') fig.write_html(os.path.join(OUTPUT_DIR, '16_scatter_3d.html')) -print(f"\n 16_scatter_3d.html sauvegarde") +print("\n 16_scatter_3d.html sauvegarde") # --- Surface 3D --- x = np.linspace(-5, 5, 50) @@ -329,7 +329,7 @@ ) ) fig.write_html(os.path.join(OUTPUT_DIR, '17_surface_3d.html')) -print(f" 17_surface_3d.html sauvegarde") +print(" 17_surface_3d.html sauvegarde") # ============================================================ @@ -348,7 +348,7 @@ title='Carte de chaleur', color_continuous_scale='RdBu') fig.write_html(os.path.join(OUTPUT_DIR, '18_heatmap_simple.html')) -print(f"\n 18_heatmap_simple.html sauvegarde") +print("\n 18_heatmap_simple.html sauvegarde") # --- Matrice de correlation --- df_iris = px.data.iris() @@ -364,7 +364,7 @@ zmin=-1, zmax=1, text_auto=True) fig.write_html(os.path.join(OUTPUT_DIR, '19_heatmap_correlation.html')) -print(f" 19_heatmap_correlation.html sauvegarde") +print(" 19_heatmap_correlation.html sauvegarde") print(f"\n Matrice de correlation Iris:\n{corr_matrix.round(3)}") @@ -392,7 +392,7 @@ range_y=[25, 90], title='Evolution mondiale : Esperance de vie vs PIB (1952-2007)') fig.write_html(os.path.join(OUTPUT_DIR, '20_scatter_anime.html')) -print(f"\n 20_scatter_anime.html sauvegarde") +print("\n 20_scatter_anime.html sauvegarde") print(f" Dataset Gapminder: {len(df_gap)} lignes, {len(df_gap.columns)} colonnes") print(f" Annees: {sorted(df_gap['year'].unique())}") print(f" Continents: {list(df_gap['continent'].unique())}") @@ -410,7 +410,7 @@ range_x=[0, 1.5e9], title='Evolution de la population des 10 pays les plus peuples') fig.write_html(os.path.join(OUTPUT_DIR, '21_bar_anime.html')) -print(f" 21_bar_anime.html sauvegarde") +print(" 21_bar_anime.html sauvegarde") # ============================================================ @@ -428,7 +428,7 @@ facet_col='species', title='Iris - Analyse par espece') fig.write_html(os.path.join(OUTPUT_DIR, '22_facets.html')) -print(f"\n 22_facets.html sauvegarde") +print("\n 22_facets.html sauvegarde") # --- Subplots avec Graph Objects --- np.random.seed(42) @@ -457,7 +457,7 @@ fig.update_layout(height=600, title_text='Tableau de bord multi-graphiques') fig.write_html(os.path.join(OUTPUT_DIR, '23_subplots_go.html')) -print(f" 23_subplots_go.html sauvegarde") +print(" 23_subplots_go.html sauvegarde") # ============================================================ @@ -498,7 +498,7 @@ fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='lightgray') fig.update_yaxes(showgrid=True, gridwidth=1, gridcolor='lightgray') fig.write_html(os.path.join(OUTPUT_DIR, '24_personnalise.html')) -print(f" 24_personnalise.html sauvegarde") +print(" 24_personnalise.html sauvegarde") # --- Annotations et formes --- x = np.linspace(0, 10, 100) @@ -526,7 +526,7 @@ fig.update_layout(title='Graphique avec annotations et formes') fig.write_html(os.path.join(OUTPUT_DIR, '25_annotations.html')) -print(f" 25_annotations.html sauvegarde") +print(" 25_annotations.html sauvegarde") # ============================================================ @@ -547,7 +547,7 @@ fig = px.line(df, x='date', y=['ventes', 'visites'], title='Evolution des ventes et visites en 2024') fig.write_html(os.path.join(OUTPUT_DIR, '26_pandas_integration.html')) -print(f"\n 26_pandas_integration.html sauvegarde") +print("\n 26_pandas_integration.html sauvegarde") print(f" Ventes finales: {df['ventes'].iloc[-1]:.1f}") print(f" Visites finales: {df['visites'].iloc[-1]:.1f}") @@ -644,7 +644,7 @@ fig.write_html(os.path.join(OUTPUT_DIR, '27_dashboard_complet.html')) print(f"\n Ventes par region: Nord={ventes_regions[0]}, Sud={ventes_regions[1]}, Est={ventes_regions[2]}") print(f" Total: {sum(ventes_regions)}") -print(f" 27_dashboard_complet.html sauvegarde") +print(" 27_dashboard_complet.html sauvegarde") # --- Resume des fichiers generes --- diff --git a/13-introduction-data-science/exemples/04_analyse_exploratoire.py b/13-introduction-data-science/exemples/04_analyse_exploratoire.py index 36e476c..dfd714d 100644 --- a/13-introduction-data-science/exemples/04_analyse_exploratoire.py +++ b/13-introduction-data-science/exemples/04_analyse_exploratoire.py @@ -41,13 +41,13 @@ print("PREMIERES OBSERVATIONS") print("=" * 50) -print(f"\n Premieres lignes:") +print("\n Premieres lignes:") print(df.head()) -print(f"\n Dernieres lignes:") +print("\n Dernieres lignes:") print(df.tail()) -print(f"\n Echantillon aleatoire:") +print("\n Echantillon aleatoire:") print(df.sample(5, random_state=42)) @@ -62,13 +62,13 @@ print(f" Nombre de colonnes: {df.shape[1]}") print(f" Dimensions: {df.shape}") -print(f"\n Types de donnees:") +print("\n Types de donnees:") print(df.dtypes) -print(f"\n Noms des colonnes:") +print("\n Noms des colonnes:") print(df.columns.tolist()) -print(f"\n Informations:") +print("\n Informations:") df.info() @@ -79,10 +79,10 @@ print("VALEURS MANQUANTES") print("=" * 50) -print(f"\n Valeurs manquantes par colonne:") +print("\n Valeurs manquantes par colonne:") print(df.isnull().sum()) -print(f"\n Pourcentage de valeurs manquantes:") +print("\n Pourcentage de valeurs manquantes:") print((df.isnull().sum() / len(df)) * 100) # Visualisation des valeurs manquantes @@ -91,7 +91,7 @@ plt.title('Carte des valeurs manquantes') plt.savefig(os.path.join(OUTPUT_DIR, '01_valeurs_manquantes.png'), dpi=100, bbox_inches='tight') plt.close() -print(f"\n 01_valeurs_manquantes.png sauvegarde") +print("\n 01_valeurs_manquantes.png sauvegarde") # ============================================================ @@ -104,7 +104,7 @@ print(f"\n Nombre de lignes dupliquees: {df.duplicated().sum()}") doublons = df[df.duplicated()] -print(f"\n Lignes dupliquees:") +print("\n Lignes dupliquees:") print(doublons) df_sans_doublons = df.drop_duplicates() @@ -118,10 +118,10 @@ print("STATISTIQUES DESCRIPTIVES") print("=" * 50) -print(f"\n Statistiques descriptives:") +print("\n Statistiques descriptives:") print(df.describe()) -print(f"\n Statistiques pour sepal_length:") +print("\n Statistiques pour sepal_length:") print(df['sepal_length'].describe()) moyenne = df['sepal_length'].mean() @@ -157,10 +157,10 @@ print("VARIABLES CATEGORIELLES") print("=" * 50) -print(f"\n Valeurs uniques:") +print("\n Valeurs uniques:") print(df['species'].value_counts()) -print(f"\n Proportions:") +print("\n Proportions:") print(df['species'].value_counts(normalize=True)) print(f"\n Nombre de categories: {df['species'].nunique()}") @@ -180,13 +180,13 @@ aplatissement = df['sepal_length'].kurtosis() print(f" Aplatissement (kurtosis): {aplatissement:.2f}") -print(f"\n Interpretation:") +print("\n Interpretation:") if abs(asymetrie) < 0.5: - print(f" - Distribution approximativement symetrique") + print(" - Distribution approximativement symetrique") elif asymetrie > 0: - print(f" - Distribution asymetrique a droite (queue a droite)") + print(" - Distribution asymetrique a droite (queue a droite)") else: - print(f" - Distribution asymetrique a gauche (queue a gauche)") + print(" - Distribution asymetrique a gauche (queue a gauche)") # ============================================================ @@ -209,7 +209,7 @@ plt.legend() plt.savefig(os.path.join(OUTPUT_DIR, '02_histogramme.png'), dpi=100, bbox_inches='tight') plt.close() -print(f"\n 02_histogramme.png sauvegarde") +print("\n 02_histogramme.png sauvegarde") # Histogramme + KDE plt.figure(figsize=(10, 6)) @@ -217,7 +217,7 @@ plt.title('Distribution avec courbe de densite') plt.savefig(os.path.join(OUTPUT_DIR, '03_hist_kde.png'), dpi=100, bbox_inches='tight') plt.close() -print(f" 03_hist_kde.png sauvegarde") +print(" 03_hist_kde.png sauvegarde") # Histogrammes multiples fig = df.hist(figsize=(15, 12), bins=30, edgecolor='black') @@ -225,7 +225,7 @@ plt.tight_layout() plt.savefig(os.path.join(OUTPUT_DIR, '04_hist_multiples.png'), dpi=100, bbox_inches='tight') plt.close() -print(f" 04_hist_multiples.png sauvegarde") +print(" 04_hist_multiples.png sauvegarde") # ============================================================ @@ -241,7 +241,7 @@ plt.title('Box plot - Longueur du sepale') plt.savefig(os.path.join(OUTPUT_DIR, '05_boxplot_simple.png'), dpi=100, bbox_inches='tight') plt.close() -print(f"\n 05_boxplot_simple.png sauvegarde") +print("\n 05_boxplot_simple.png sauvegarde") # Box plots par categorie plt.figure(figsize=(12, 6)) @@ -249,7 +249,7 @@ plt.title('Distribution de la longueur du sepale par espece') plt.savefig(os.path.join(OUTPUT_DIR, '06_boxplot_espece.png'), dpi=100, bbox_inches='tight') plt.close() -print(f" 06_boxplot_espece.png sauvegarde") +print(" 06_boxplot_espece.png sauvegarde") # Violin plot plt.figure(figsize=(12, 6)) @@ -257,7 +257,7 @@ plt.title('Violin plot - Longueur du sepale par espece') plt.savefig(os.path.join(OUTPUT_DIR, '07_violin.png'), dpi=100, bbox_inches='tight') plt.close() -print(f" 07_violin.png sauvegarde") +print(" 07_violin.png sauvegarde") # ============================================================ @@ -276,7 +276,7 @@ plt.xticks(rotation=45) plt.savefig(os.path.join(OUTPUT_DIR, '08_bar_especes.png'), dpi=100, bbox_inches='tight') plt.close() -print(f"\n 08_bar_especes.png sauvegarde") +print("\n 08_bar_especes.png sauvegarde") # Diagramme circulaire plt.figure(figsize=(8, 8)) @@ -285,7 +285,7 @@ plt.ylabel('') plt.savefig(os.path.join(OUTPUT_DIR, '09_pie_especes.png'), dpi=100, bbox_inches='tight') plt.close() -print(f" 09_pie_especes.png sauvegarde") +print(" 09_pie_especes.png sauvegarde") # ============================================================ @@ -299,7 +299,7 @@ plt.suptitle('Matrice de relations entre variables', y=1.02) plt.savefig(os.path.join(OUTPUT_DIR, '10_pairplot.png'), dpi=100, bbox_inches='tight') plt.close() -print(f"\n 10_pairplot.png sauvegarde") +print("\n 10_pairplot.png sauvegarde") # ============================================================ @@ -311,7 +311,7 @@ correlation_matrix = df.select_dtypes(include=[np.number]).corr() -print(f"\n Matrice de correlation:") +print("\n Matrice de correlation:") print(correlation_matrix.round(3)) plt.figure(figsize=(10, 8)) @@ -320,7 +320,7 @@ plt.title('Matrice de correlation') plt.savefig(os.path.join(OUTPUT_DIR, '11_correlation.png'), dpi=100, bbox_inches='tight') plt.close() -print(f"\n 11_correlation.png sauvegarde") +print("\n 11_correlation.png sauvegarde") # ============================================================ @@ -338,7 +338,7 @@ plt.title('Relation entre longueur et largeur du sepale') plt.savefig(os.path.join(OUTPUT_DIR, '12_scatter_simple.png'), dpi=100, bbox_inches='tight') plt.close() -print(f"\n 12_scatter_simple.png sauvegarde") +print("\n 12_scatter_simple.png sauvegarde") # Scatter par categorie plt.figure(figsize=(10, 6)) @@ -352,7 +352,7 @@ plt.legend() plt.savefig(os.path.join(OUTPUT_DIR, '13_scatter_especes.png'), dpi=100, bbox_inches='tight') plt.close() -print(f" 13_scatter_especes.png sauvegarde") +print(" 13_scatter_especes.png sauvegarde") # Scatter avec regression sns.lmplot(x='sepal_length', y='sepal_width', data=df, @@ -360,7 +360,7 @@ plt.title('Relation avec ligne de regression') plt.savefig(os.path.join(OUTPUT_DIR, '14_scatter_regression.png'), dpi=100, bbox_inches='tight') plt.close() -print(f" 14_scatter_regression.png sauvegarde") +print(" 14_scatter_regression.png sauvegarde") # ============================================================ @@ -375,10 +375,10 @@ 'sepal_width': ['mean', 'median', 'std', 'min', 'max'] }) -print(f"\n Statistiques par espece:") +print("\n Statistiques par espece:") print(stats_par_espece) -print(f"\n Moyennes par espece:") +print("\n Moyennes par espece:") print(df.groupby('species').mean(numeric_only=True)) # Bar plot des moyennes @@ -389,7 +389,7 @@ plt.xticks(rotation=45) plt.savefig(os.path.join(OUTPUT_DIR, '15_bar_moyennes.png'), dpi=100, bbox_inches='tight') plt.close() -print(f"\n 15_bar_moyennes.png sauvegarde") +print("\n 15_bar_moyennes.png sauvegarde") # Box plots cote a cote fig, axes = plt.subplots(2, 2, figsize=(15, 12)) @@ -409,7 +409,7 @@ plt.tight_layout() plt.savefig(os.path.join(OUTPUT_DIR, '16_boxplots_complets.png'), dpi=100, bbox_inches='tight') plt.close() -print(f" 16_boxplots_complets.png sauvegarde") +print(" 16_boxplots_complets.png sauvegarde") # ============================================================ @@ -451,17 +451,17 @@ def detecter_outliers_zscore(df, colonne, seuil=3): titanic = sns.load_dataset('titanic') # 1. PREMIERES OBSERVATIONS -print(f"\n1. PREMIERES OBSERVATIONS") +print("\n1. PREMIERES OBSERVATIONS") print("-" * 80) print(f" Dimensions: {titanic.shape[0]} lignes, {titanic.shape[1]} colonnes") -print(f"\n Premieres lignes:") +print("\n Premieres lignes:") print(titanic.head()) -print(f"\n Types de donnees:") +print("\n Types de donnees:") print(titanic.dtypes) # 2. VALEURS MANQUANTES -print(f"\n2. VALEURS MANQUANTES") +print("\n2. VALEURS MANQUANTES") print("-" * 80) missing = titanic.isnull().sum() missing_percent = (missing / len(titanic)) * 100 @@ -477,15 +477,15 @@ def detecter_outliers_zscore(df, colonne, seuil=3): plt.title('Carte des valeurs manquantes - Titanic') plt.savefig(os.path.join(OUTPUT_DIR, '17_titanic_missing.png'), dpi=100, bbox_inches='tight') plt.close() -print(f"\n 17_titanic_missing.png sauvegarde") +print("\n 17_titanic_missing.png sauvegarde") # 3. STATISTIQUES DESCRIPTIVES -print(f"\n3. STATISTIQUES DESCRIPTIVES") +print("\n3. STATISTIQUES DESCRIPTIVES") print("-" * 80) print(titanic.describe()) # 4. ANALYSE DE LA SURVIE -print(f"\n4. ANALYSE DE LA SURVIE") +print("\n4. ANALYSE DE LA SURVIE") print("-" * 80) print(f" Taux de survie global: {titanic['survived'].mean():.2%}") print(f" Nombre de survivants: {titanic['survived'].sum()}") @@ -497,6 +497,7 @@ def detecter_outliers_zscore(df, colonne, seuil=3): axes[0].set_title('Distribution de la survie') axes[0].set_xlabel('Survie (0 = Non, 1 = Oui)') axes[0].set_ylabel('Nombre de passagers') +axes[0].set_xticks([0, 1]) axes[0].set_xticklabels(['Decede', 'Survecu'], rotation=0) titanic['survived'].value_counts().plot(kind='pie', ax=axes[1], @@ -508,10 +509,10 @@ def detecter_outliers_zscore(df, colonne, seuil=3): plt.tight_layout() plt.savefig(os.path.join(OUTPUT_DIR, '18_titanic_survie.png'), dpi=100, bbox_inches='tight') plt.close() -print(f" 18_titanic_survie.png sauvegarde") +print(" 18_titanic_survie.png sauvegarde") # 5. ANALYSE PAR SEXE -print(f"\n5. ANALYSE PAR SEXE") +print("\n5. ANALYSE PAR SEXE") print("-" * 80) print(titanic.groupby('sex')['survived'].agg(['count', 'sum', 'mean'])) @@ -522,10 +523,10 @@ def detecter_outliers_zscore(df, colonne, seuil=3): plt.xlabel('Sexe') plt.savefig(os.path.join(OUTPUT_DIR, '19_titanic_sexe.png'), dpi=100, bbox_inches='tight') plt.close() -print(f"\n 19_titanic_sexe.png sauvegarde") +print("\n 19_titanic_sexe.png sauvegarde") # 6. ANALYSE PAR CLASSE -print(f"\n6. ANALYSE PAR CLASSE") +print("\n6. ANALYSE PAR CLASSE") print("-" * 80) print(titanic.groupby('pclass')['survived'].agg(['count', 'sum', 'mean'])) @@ -536,10 +537,10 @@ def detecter_outliers_zscore(df, colonne, seuil=3): plt.xlabel('Classe') plt.savefig(os.path.join(OUTPUT_DIR, '20_titanic_classe.png'), dpi=100, bbox_inches='tight') plt.close() -print(f"\n 20_titanic_classe.png sauvegarde") +print("\n 20_titanic_classe.png sauvegarde") # 7. ANALYSE PAR AGE -print(f"\n7. ANALYSE PAR AGE") +print("\n7. ANALYSE PAR AGE") print("-" * 80) print(f" Age moyen: {titanic['age'].mean():.2f} ans") print(f" Age median: {titanic['age'].median():.2f} ans") @@ -555,21 +556,22 @@ def detecter_outliers_zscore(df, colonne, seuil=3): sns.boxplot(x='survived', y='age', data=titanic, ax=axes[1]) axes[1].set_title("Distribution de l'age par survie") +axes[1].set_xticks([0, 1]) axes[1].set_xticklabels(['Decede', 'Survecu']) plt.tight_layout() plt.savefig(os.path.join(OUTPUT_DIR, '21_titanic_age.png'), dpi=100, bbox_inches='tight') plt.close() -print(f" 21_titanic_age.png sauvegarde") +print(" 21_titanic_age.png sauvegarde") # 8. ANALYSE CROISEE -print(f"\n8. ANALYSE CROISEE") +print("\n8. ANALYSE CROISEE") print("-" * 80) cross_tab = pd.crosstab([titanic['pclass'], titanic['sex']], titanic['survived'], margins=True) -print(f" Tableau croise Classe x Sexe x Survie:") +print(" Tableau croise Classe x Sexe x Survie:") print(cross_tab) g = sns.catplot(x='pclass', y='survived', hue='sex', data=titanic, @@ -577,10 +579,10 @@ def detecter_outliers_zscore(df, colonne, seuil=3): g.fig.suptitle('Taux de survie par classe et par sexe', y=1.02) plt.savefig(os.path.join(OUTPUT_DIR, '22_titanic_croise.png'), dpi=100, bbox_inches='tight') plt.close() -print(f"\n 22_titanic_croise.png sauvegarde") +print("\n 22_titanic_croise.png sauvegarde") # 9. CORRELATIONS -print(f"\n9. MATRICE DE CORRELATION") +print("\n9. MATRICE DE CORRELATION") print("-" * 80) numeric_cols = titanic.select_dtypes(include=[np.number]).columns @@ -592,13 +594,13 @@ def detecter_outliers_zscore(df, colonne, seuil=3): plt.title('Matrice de correlation - Titanic') plt.savefig(os.path.join(OUTPUT_DIR, '23_titanic_correlation.png'), dpi=100, bbox_inches='tight') plt.close() -print(f" 23_titanic_correlation.png sauvegarde") +print(" 23_titanic_correlation.png sauvegarde") -print(f"\n Correlations avec la survie:") +print("\n Correlations avec la survie:") print(correlation_matrix['survived'].sort_values(ascending=False).round(3)) # 10. INSIGHTS PRINCIPAUX -print(f"\n10. INSIGHTS PRINCIPAUX") +print("\n10. INSIGHTS PRINCIPAUX") print("=" * 80) print(" 1. Taux de survie global: ~38%") taux_femmes = titanic[titanic['sex'] == 'female']['survived'].mean() diff --git a/13-introduction-data-science/exemples/README.md b/13-introduction-data-science/exemples/README.md index a8ce9c0..3cf9717 100644 --- a/13-introduction-data-science/exemples/README.md +++ b/13-introduction-data-science/exemples/README.md @@ -21,7 +21,7 @@ Ce dossier contient les fichiers d'exemples executables pour le chapitre 13. - **Fichier source** : `01.2-indexation-slicing-avances.md` - **Description** : Indexation 1D/2D, slicing, indexation par liste d'indices, indexation booleenne (conditions simples et multiples), modification avec indexation, indexation fancy (np.ix_, diagonale), fonctions utiles (where, argmax, argmin, nonzero), vues vs copies, exemples pratiques (normalisation min-max, remplacement outliers, scores etudiants, grille meshgrid) - **Sortie attendue** : - - Indexation: premier element=10, dernier=-1, matrice[0,2]=3 + - Indexation: premier element=10, dernier element=50, matrice[0,2]=3 - Slicing: arr[2:5]=[20 30 40], arr inverse=[90 80 ... 0] - Indexation booleenne: valeurs > 20 correctement filtrees - Vues: modification de la vue modifie l'original @@ -34,8 +34,8 @@ Ce dossier contient les fichiers d'exemples executables pour le chapitre 13. - **Fichier source** : `02-manipulation-donnees-pandas.md`, `02.1-dataframes-et-series.md` - **Description** : Series (creation depuis liste/dict/NumPy, acces iloc/label/slice, proprietes, operations arithmetiques, statistiques describe(), filtrage), DataFrames (creation depuis dict/liste/NumPy/Series, proprietes, head/tail, acces colonnes/lignes, loc/iloc, modification, filtrage avec conditions/isin/str.contains, tri), valeurs manquantes (isnull, dropna, fillna), groupby basique, exemples pratiques (analyse ventes, notes etudiants, tracker activite) - **Sortie attendue** : - - Series: 5 elements, dtype float64 - - DataFrame: shape (5,3), colonnes [Nom, Age, Ville] + - Series: 5 elements, dtype int64 + - DataFrame: shape (4,4), colonnes [Nom, Age, Ville, Salaire] - Filtrage: ages > 30 correctement selectionnes - Valeurs manquantes: NaN detectes et remplaces par moyenne - GroupBy ventes: somme par produit calculee @@ -45,7 +45,7 @@ Ce dossier contient les fichiers d'exemples executables pour le chapitre 13. - **Fichier source** : `02.2-nettoyage-transformation.md` - **Description** : Valeurs manquantes (detection, dropna, fillna avec constantes/stats/ffill/bfill/interpolation), doublons (duplicated, drop_duplicates), conversion de types (astype, to_numeric avec coerce, categories), methodes string (upper/lower/strip/contains/replace/split), dates (to_datetime, extraction composants, timedelta), renommage/reorganisation, apply/map/replace, pivot/melt/stack, concat/merge (inner/left/outer), exemples pratiques (nettoyage ventes, combinaison multi-sources) - **Sortie attendue** : - - Valeurs manquantes: 2 NaN detectes, remplaces par moyenne ~25 + - Valeurs manquantes: 2 NaN detectes dans 'Age', remplissage par constante/moyenne/mediane, ffill/bfill, interpolation - Doublons: 2 doublons supprimes - Conversion categories: reduction memoire significative - Methodes string: upper/lower/strip fonctionnent correctement diff --git a/LICENSE b/LICENSE index 2d71ad2..f7dc295 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 Nicolas DEOUX +Copyright (c) 2025-2026 Nicolas DEOUX Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index b49852b..7a63b6a 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # 🐍 Formation Python - Du Débutant à l'Avancé -![Python Version](https://img.shields.io/badge/Python-3.10%2B-blue?logo=python&logoColor=white) -![License](https://img.shields.io/badge/License-MIT-green.svg) -![Modules](https://img.shields.io/badge/Modules-13-brightgreen.svg) -![Language](https://img.shields.io/badge/Langue-Français-blue.svg) +![Python Version](https://img.shields.io/badge/Python-3.12%2B-blue?logo=python&logoColor=white) +![License](https://img.shields.io/badge/License-MIT-green.svg) +![Modules](https://img.shields.io/badge/Modules-13-brightgreen.svg) +![Language](https://img.shields.io/badge/Langue-Français-blue.svg) ![Status](https://img.shields.io/badge/Status-Actif-success.svg) **Formation complète Python 3, des fondamentaux jusqu'aux concepts avancés (FastAPI, Data Science, Type Hints).** @@ -32,12 +32,12 @@ Formation progressive et complète sur **Python 3** couvrant l'intégralité du **✨ Points clés :** - 📚 **13 modules progressifs** du niveau débutant à expert - 🎯 **75+ sujets** couverts avec exemples concrets -- 🆕 **Technologies modernes** (FastAPI, Type Hints, Poetry, SQLAlchemy) -- 📊 **Module Data Science** complet (NumPy, Pandas, Matplotlib) +- 🆕 **Technologies modernes** (FastAPI, Type Hints, uv, Poetry, SQLAlchemy) +- 📊 **Module Data Science** complet (NumPy, Pandas, Matplotlib, Plotly) - 🔥 **Édition 2025-2026** avec les dernières pratiques Python - 🇫🇷 **100% en français** et gratuit (MIT License) -**Durée estimée :** 40-60 heures • **Niveau :** Tous niveaux • **Prérequis :** Aucun +**Durée estimée :** 50-70 heures • **Niveau :** Tous niveaux • **Prérequis :** Aucun --- @@ -57,14 +57,21 @@ Formation progressive et complète sur **Python 3** couvrant l'intégralité du **Module 7 : Bibliothèques standard** - os/sys, datetime, math/random, itertools, logging, **typing avancé** 🆕 **Module 8 : Concurrence** - Threading, multiprocessing, asyncio, patterns -### 🎯 Modules Avancés (9-13) +### 🎯 Modules Avancés (9-12) **Module 9 : Débogage** - Exceptions, debugging, profiling, optimisation **Module 10 : Tests** - unittest, pytest, mocking, couverture de code, documentation, PEP 8, **mypy** 🆕 **Module 11 : Web/APIs** - **FastAPI** 🆕, Flask, REST, **SQLAlchemy** 🆕 **Module 12 : Bonnes pratiques** - Architecture, Git, design patterns, optimisation, déploiement + +### 📊 Module de Spécialisation (13) + **Module 13 : Data Science** 📊 - **NumPy, Pandas, Matplotlib/Plotly** 🆕 *(optionnel)* +### 📎 Annexes (référence) + +**Annexes** 📎 - [Glossaire](annexes/01-glossaire.md), [récapitulatif des PEP](annexes/02-pep-et-standards.md), [aide-mémoire](annexes/03-aide-memoire.md), [pour aller plus loin](annexes/04-pour-aller-plus-loin.md) + > 📋 Consultez [SOMMAIRE.md](SOMMAIRE.md) pour la table des matières complète --- @@ -77,7 +84,7 @@ Formation progressive et complète sur **Python 3** couvrant l'intégralité du # Vérifier la version de Python python --version # ou python3 --version -# Télécharger Python 3.10+ (recommandé : 3.13+) +# Télécharger Python 3.12+ (recommandé : 3.13 ou 3.14, la dernière version) # 🌐 https://www.python.org/downloads/ ``` @@ -116,6 +123,17 @@ poetry install -E web poetry install -E data ``` +### Alternative avec uv (ultra-rapide) 🆕 + +```bash +# Installer uv : https://docs.astral.sh/uv/ (gestionnaire signé par l'éditeur de Ruff) +# Créer l'environnement et installer les dépendances (bien plus rapide que pip) +uv venv +source .venv/bin/activate # 🐧 Linux/Mac · .venv\Scripts\activate sous Windows +uv pip install fastapi uvicorn flask requests sqlalchemy pydantic # Modules web +uv pip install numpy pandas matplotlib plotly # Modules data science +``` + ### Votre premier programme ```python @@ -138,7 +156,7 @@ python hello.py ## 📁 Structure du projet ``` -formation-python-complete/ +formation-python/ ├── 📄 README.md ├── 📋 SOMMAIRE.md (table des matières détaillée) ├── 🛠️ VSCODE-SETUP.md (configuration VS Code) @@ -160,7 +178,8 @@ formation-python-complete/ ├── 📂 10-tests-et-qualite/ ├── 📂 11-developpement-web-et-apis/ ├── 📂 12-projets-et-bonnes-pratiques/ -└── 📂 13-introduction-data-science/ (optionnel) +├── 📂 13-introduction-data-science/ (optionnel) +└── 📎 annexes/ (glossaire, PEP, aide-mémoire, pour aller plus loin) ``` --- @@ -204,25 +223,26 @@ source venv/bin/activate ## 🛠️ Technologies couvertes ### Langage & Outils -![Python](https://img.shields.io/badge/Python-3.10+-3776AB?logo=python&logoColor=white) -![Type Hints](https://img.shields.io/badge/Type_Hints-mypy-blue) +![Python](https://img.shields.io/badge/Python-3.12+-3776AB?logo=python&logoColor=white) +![Type Hints](https://img.shields.io/badge/Type_Hints-mypy-blue) +![uv](https://img.shields.io/badge/uv-Package_Manager-261230) ![Poetry](https://img.shields.io/badge/Poetry-Package_Manager-60A5FA) ### Frameworks Web -![FastAPI](https://img.shields.io/badge/FastAPI-0.100+-009688?logo=fastapi&logoColor=white) -![Flask](https://img.shields.io/badge/Flask-3.0+-000000?logo=flask&logoColor=white) +![FastAPI](https://img.shields.io/badge/FastAPI-0.100+-009688?logo=fastapi&logoColor=white) +![Flask](https://img.shields.io/badge/Flask-3.0+-000000?logo=flask&logoColor=white) ### Données & ORM -![SQLAlchemy](https://img.shields.io/badge/SQLAlchemy-2.0-red) -![Pandas](https://img.shields.io/badge/Pandas-2.0+-150458?logo=pandas&logoColor=white) -![NumPy](https://img.shields.io/badge/NumPy-1.24+-013243?logo=numpy&logoColor=white) +![SQLAlchemy](https://img.shields.io/badge/SQLAlchemy-2.0-red) +![Pandas](https://img.shields.io/badge/Pandas-3.0+-150458?logo=pandas&logoColor=white) +![NumPy](https://img.shields.io/badge/NumPy-2.0+-013243?logo=numpy&logoColor=white) ### Tests & Qualité -![pytest](https://img.shields.io/badge/pytest-7.0+-0A9EDC?logo=pytest&logoColor=white) +![pytest](https://img.shields.io/badge/pytest-8.0+-0A9EDC?logo=pytest&logoColor=white) ![mypy](https://img.shields.io/badge/mypy-Type_Checker-blue) ### Visualisation -![Matplotlib](https://img.shields.io/badge/Matplotlib-3.7+-11557c) +![Matplotlib](https://img.shields.io/badge/Matplotlib-3.7+-11557c) ![Plotly](https://img.shields.io/badge/Plotly-5.0+-3F4F75?logo=plotly&logoColor=white) --- @@ -276,20 +296,20 @@ async def creer_utilisateur(user: Utilisateur, db: Session = Depends(get_db)): ## ❓ FAQ -**Q : Dois-je avoir des connaissances préalables en programmation ?** +**Q : Dois-je avoir des connaissances préalables en programmation ?** R : Non ! Le Module 1 commence vraiment à zéro. Si vous avez déjà programmé dans un autre langage, vous progresserez plus vite. -**Q : Combien de temps faut-il pour terminer la formation ?** -R : Entre 40 et 60 heures selon votre rythme. Comptez 4-8 semaines à raison de 1-2h par jour. +**Q : Combien de temps faut-il pour terminer la formation ?** +R : Entre 50 et 70 heures selon votre rythme. Comptez 5-9 semaines à raison de 1-2h par jour. -**Q : Le Module 13 (Data Science) est-il obligatoire ?** +**Q : Le Module 13 (Data Science) est-il obligatoire ?** R : Non, c'est un module optionnel pour ceux qui s'intéressent à l'analyse de données. -**Q : Puis-je utiliser cette formation pour enseigner ?** +**Q : Puis-je utiliser cette formation pour enseigner ?** R : Oui ! La licence MIT vous permet d'utiliser, modifier et partager ce contenu librement. -**Q : Quelle version de Python dois-je utiliser ?** -R : Python 3.10 ou supérieur est recommandé. Les exemples utilisent la syntaxe moderne (type hints natifs comme `list[str]`, `dict[str, int]`, match/case, etc.). Python 3.13+ est idéal pour bénéficier des dernières améliorations. +**Q : Quelle version de Python dois-je utiliser ?** +R : Python 3.12 ou supérieur est requis (le projet cible `python = "^3.12"`). Les exemples utilisent la syntaxe moderne (type hints natifs comme `list[str]`, `dict[str, int]`, match/case, ainsi que l'alias `type` de la PEP 695 introduit en 3.12). Python 3.13 ou 3.14 (la dernière version) est idéal pour bénéficier des dernières améliorations. --- @@ -345,11 +365,11 @@ Merci à la communauté Python, aux créateurs de frameworks open source (FastAP **🐍 Bon apprentissage avec Python ! 🐍** -[![Python](https://img.shields.io/badge/Made_with-Python-3776AB?logo=python&logoColor=white)](https://www.python.org/) +[![Python](https://img.shields.io/badge/Made_with-Python-3776AB?logo=python&logoColor=white)](https://www.python.org/) [![Love](https://img.shields.io/badge/Made_with-❤️-red)](https://github.com/NDXDeveloper) **[⬆ Retour en haut](#-formation-python---du-débutant-à-lavancé)** -*Dernière mise à jour : Mars 2026* +*Dernière mise à jour : Juin 2026* diff --git a/SOMMAIRE.md b/SOMMAIRE.md index 29f230b..edbc882 100644 --- a/SOMMAIRE.md +++ b/SOMMAIRE.md @@ -109,6 +109,14 @@ --- +### 📎 [Annexes](annexes/README.md) +- A. [Glossaire](annexes/01-glossaire.md) +- B. [Récapitulatif des PEP et standards](annexes/02-pep-et-standards.md) +- C. [Aide-mémoire (cheat-sheets)](annexes/03-aide-memoire.md) +- D. [Pour aller plus loin](annexes/04-pour-aller-plus-loin.md) + +--- + ## 📋 Légende | Symbole | Signification | @@ -116,5 +124,6 @@ | 🆕 | Contenu nouveau / modernisé pour 2026 | | ⚠️ | Niveau expert / optionnel | | 📊 | Module spécialisé Data Science | +| 📎 | Annexe / référence transversale | --- diff --git a/VSCODE-SETUP.md b/VSCODE-SETUP.md index 3adf305..b80e96c 100644 --- a/VSCODE-SETUP.md +++ b/VSCODE-SETUP.md @@ -42,11 +42,9 @@ Créer ce fichier à la racine du projet pour une configuration optimale : "**/*.pyc": true, "**/.pytest_cache": true, "**/.mypy_cache": true, - "**/venv": true - }, - - "files.associations": { - "*.md": "markdown" + "**/.ruff_cache": true, + "**/venv": true, + "**/.venv": true }, "editor.minimap.enabled": true, @@ -57,6 +55,10 @@ Créer ce fichier à la racine du projet pour une configuration optimale : > **Note :** Depuis 2023, les paramètres `python.formatting.provider` et `python.linting.*` sont dépréciés. Le formatage et le linting utilisent désormais des extensions dédiées (Ruff, Black, mypy) avec leurs propres paramètres. +> **Interpréteur Python — adaptez le chemin à votre environnement.** Avec **uv** (recommandé aux chapitres 6 et 12), l'environnement s'appelle `.venv` : utilisez alors `"${workspaceFolder}/.venv/bin/python"` (l'exclusion de `.venv` est déjà prévue dans `files.exclude`). Sous **Windows**, le chemin devient `"${workspaceFolder}\\venv\\Scripts\\python.exe"`. + +> **Une seule source de vérité pour les outils.** Définissez de préférence la longueur de ligne, les règles **Ruff** et les options **mypy** dans `pyproject.toml` (voir chapitre 12.1) : la ligne de commande (`ruff`, `mypy`) **et** l'éditeur appliquent ainsi exactement les mêmes règles, et le `ruff.lineLength` ci-dessus devient redondant. + --- ## Extensions VS Code recommandées @@ -68,6 +70,7 @@ Créer le fichier `.vscode/extensions.json` : "recommendations": [ "ms-python.python", "ms-python.vscode-pylance", + "ms-python.debugpy", "ms-python.mypy-type-checker", "charliermarsh.ruff", "kevinrose.vsc-python-indent", @@ -84,6 +87,7 @@ Créer le fichier `.vscode/extensions.json` : |-----------|------| | **Python** (`ms-python.python`) | Support Python de base (IntelliSense, débogage, notebooks) | | **Pylance** (`ms-python.vscode-pylance`) | Serveur de langage rapide (auto-complétion, navigation, types) | +| **Python Debugger** (`ms-python.debugpy`) | Débogueur Python utilisé par `launch.json` (installé automatiquement avec l'extension Python) | | **Mypy Type Checker** (`ms-python.mypy-type-checker`) | Vérification statique des types en temps réel | | **Ruff** (`charliermarsh.ruff`) | Linter et formateur ultra-rapide (remplace Black, Flake8, isort) | | **Python Indent** (`kevinrose.vsc-python-indent`) | Indentation intelligente après `def`, `if`, `for`, etc. | @@ -91,10 +95,14 @@ Créer le fichier `.vscode/extensions.json` : | **Markdown All in One** (`yzhang.markdown-all-in-one`) | Édition Markdown avancée (aperçu, raccourcis, table des matières) | | **Even Better TOML** (`tamasfe.even-better-toml`) | Support des fichiers `pyproject.toml` | +> **Pylance et mypy.** Pylance fournit aussi une vérification de types (moteur Pyright), **désactivée par défaut** (`python.analysis.typeCheckingMode` vaut `"off"`) : aucun doublon avec mypy tant qu'on ne l'active pas. Si vous passez à `"basic"` ou `"standard"`, attendez-vous à des diagnostics en partie redondants avec ceux de mypy. + --- ## Raccourcis utiles VS Code +> **macOS :** remplacez `Ctrl` par `Cmd` (⌘) dans la majorité des raccourcis ci-dessous. + ### Raccourcis généraux | Raccourci | Action | @@ -271,6 +279,7 @@ Créer le fichier `.vscode/python.code-snippets` : # Extensions essentielles code --install-extension ms-python.python code --install-extension ms-python.vscode-pylance +code --install-extension ms-python.debugpy code --install-extension ms-python.mypy-type-checker code --install-extension charliermarsh.ruff @@ -327,11 +336,11 @@ Si vous utilisez PyCharm, voici les configurations recommandées : ## 🎯 Résumé Ces configurations vous permettent de : -- ✅ Formater et corriger automatiquement avec **Ruff** à la sauvegarde -- ✅ Vérifier les types en temps réel avec **mypy** -- ✅ Exécuter les tests avec **pytest** depuis VS Code -- ✅ Bénéficier d'une auto-complétion intelligente avec **Pylance** -- ✅ Naviguer facilement dans le code (définitions, références) +- ✅ Formater et corriger automatiquement avec **Ruff** à la sauvegarde +- ✅ Vérifier les types en temps réel avec **mypy** +- ✅ Exécuter les tests avec **pytest** depuis VS Code +- ✅ Bénéficier d'une auto-complétion intelligente avec **Pylance** +- ✅ Naviguer facilement dans le code (définitions, références) - ✅ Gagner du temps avec les **snippets** personnalisés **Conseil :** Copiez ces fichiers dans votre projet pour une expérience de développement optimale ! diff --git a/annexes/01-glossaire.md b/annexes/01-glossaire.md new file mode 100644 index 0000000..e1bce8b --- /dev/null +++ b/annexes/01-glossaire.md @@ -0,0 +1,213 @@ +🔝 Retour au [Sommaire](/SOMMAIRE.md) + +# Annexe A — Glossaire + +Ce glossaire rassemble les **termes essentiels** rencontrés tout au long de la formation. Chaque définition renvoie au(x) chapitre(s) où le concept est traité en détail. + +> 💡 Les termes sont classés par ordre alphabétique. Les renvois `(ch. X)` pointent vers le chapitre correspondant du [sommaire](/SOMMAIRE.md). + +--- + +## A + +**Annotation de type (*type hint*)** — Indication facultative du type attendu d'une variable, d'un paramètre ou d'une valeur de retour (`def f(x: int) -> str`). Elle n'est pas vérifiée à l'exécution, mais exploitée par les outils (mypy, IDE). *(ch. 1.6, 7.6, 10.6)* + +**ABC (*Abstract Base Class*)** — Classe de base abstraite (module `abc`) qui définit une interface via des méthodes `@abstractmethod` que les sous-classes doivent implémenter. *(ch. 3, 12.3)* + +**API / Endpoint** — Une *API* expose des fonctionnalités via des points d'accès (*endpoints*), chacun étant une URL associée à un verbe HTTP (GET, POST…). *(ch. 11)* + +**Argument / Paramètre** — Le *paramètre* est le nom déclaré dans la signature d'une fonction ; l'*argument* est la valeur réellement passée à l'appel. *(ch. 1.4)* + +**`async` / `await`** — Mots-clés définissant des coroutines et suspendant leur exécution en attendant une opération d'E/S non bloquante. *(ch. 8.2)* + +--- + +## B + +**Boucle d'événements (*event loop*)** — Cœur d'`asyncio` : elle ordonnance les coroutines et reprend chacune lorsque son `await` est prêt. *(ch. 8.2)* + +**Broadcasting (*diffusion*)** — Mécanisme NumPy permettant d'appliquer une opération entre des tableaux de formes différentes (ex. matrice + vecteur) sans boucle explicite. *(ch. 13.1.1)* + +--- + +## C + +**CI/CD (*intégration et déploiement continus*)** — Automatisation des tests à chaque modification (CI) et de la mise en production (CD). *(ch. 12.5)* + +**Closure (*fermeture*)** — Fonction interne qui « capture » et mémorise les variables de sa portée englobante, même après la fin de la fonction externe. *(ch. 5.5)* + +**Compréhension (*comprehension*)** — Syntaxe concise pour construire une liste, un dictionnaire ou un set à partir d'un itérable : `[x*2 for x in xs]`. *(ch. 2.2)* + +**Context manager (*gestionnaire de contexte*)** — Objet utilisable avec `with`, qui garantit l'acquisition et la libération propre d'une ressource via `__enter__`/`__exit__` (ou `@contextmanager`). *(ch. 4.1, 12.3)* + +**Coroutine** — Fonction définie avec `async def`, dont l'exécution peut être suspendue et reprise ; brique de base d'`asyncio`. *(ch. 8.2)* + +**Conteneur (Docker)** — Unité logicielle isolée empaquetant une application et ses dépendances, exécutable à l'identique partout. *(ch. 12.5)* + +**Copy-on-Write (CoW)** — Stratégie (activée par défaut depuis pandas 3.0) où une copie n'est réellement effectuée qu'au moment d'une modification, ce qui rend l'assignation chaînée inopérante (pandas émet alors un avertissement `ChainedAssignmentError`). *(ch. 13.2)* + +**Couverture de code (*coverage*)** — Pourcentage du code réellement exécuté par les tests, mesuré avec `pytest-cov`/`coverage`. *(ch. 10.3)* + +--- + +## D + +**Dataclass** — Classe générée par le décorateur `@dataclass` qui crée automatiquement `__init__`, `__repr__`, `__eq__`… pour les classes de données. *(ch. 3, 12.3)* + +**DataFrame** — Structure tabulaire 2D de pandas (lignes × colonnes étiquetées), comparable à une feuille de calcul. *(ch. 13.2.1)* + +**Décorateur** — Fonction (ou classe) qui en enveloppe une autre pour en modifier le comportement, appliquée avec la syntaxe `@`. *(ch. 3.4, 5.3, 12.3)* + +**Docstring** — Chaîne de documentation placée en première instruction d'un module, d'une fonction ou d'une classe, accessible via `help()` ou `__doc__`. *(ch. 10.4)* + +**Duck typing** — Style Python où la compatibilité d'un objet dépend de ses méthodes/attributs (« si ça fait coin-coin, c'est un canard ») plutôt que de son type déclaré. *(ch. 3, 12.3)* + +--- + +## E + +**EDA (*Exploratory Data Analysis*)** — Analyse exploratoire : première phase d'investigation d'un jeu de données (structure, valeurs manquantes, distributions, corrélations). *(ch. 13.4)* + +**Encapsulation** — Principe de la POO : regrouper données et méthodes dans une classe et restreindre l'accès direct à l'état interne. *(ch. 3)* + +**Exception** — Objet signalant une erreur ou un événement particulier, propagé jusqu'à un bloc `try/except` qui le gère. *(ch. 1.5, 9)* + +**Expression régulière (*regex*)** — Motif décrivant un ensemble de chaînes, utilisé pour rechercher, valider ou remplacer du texte via le module `re` (ex. `\d+` = une suite de chiffres). *(ch. 2.4)* + +--- + +## F + +**f-string** — Littéral de chaîne préfixé par `f` permettant l'interpolation d'expressions : `f"{nom} a {age} ans"`. *(ch. 1.2)* + +**Fixture** — En pytest, fonction qui prépare un contexte de test (données, ressources) réutilisable et injecté par dépendance. *(ch. 10.2)* + +**Fonction d'ordre supérieur** — Fonction qui prend une fonction en argument et/ou en retourne une (`map`, `filter`, décorateurs). *(ch. 5.1)* + +--- + +## G + +**Générateur** — Fonction utilisant `yield` pour produire des valeurs à la demande (*lazy*), économisant la mémoire. *(ch. 5.4, 12.4)* + +**GIL (*Global Interpreter Lock*)** — Verrou de CPython n'autorisant qu'un seul thread à exécuter du bytecode Python à la fois. Il **simplifie la gestion mémoire interne** (le comptage de références, non *thread-safe*) mais limite en contrepartie le parallélisme CPU des threads (contourné par `multiprocessing`, et par le mode *free-threading* expérimental depuis Python 3.13). *(ch. 8.1, 12.4)* + +--- + +## H + +**Héritage** — Mécanisme par lequel une classe (fille) réutilise et spécialise les attributs et méthodes d'une autre (parente). *(ch. 3.2)* + +--- + +## I + +**Immuable / Mutable** — Un objet *immuable* (int, str, tuple) ne peut être modifié après création ; un objet *mutable* (list, dict, set) le peut. *(ch. 2.1)* + +**Itérateur / Itérable** — Un *itérable* peut être parcouru (`for`) ; un *itérateur* produit les éléments un à un via `__next__`. *(ch. 5.4, 12.3)* + +--- + +## L + +**Lambda** — Petite fonction anonyme d'une seule expression : `lambda x: x * 2`. *(ch. 5.1)* + +**Linter** — Outil d'analyse statique signalant erreurs et écarts de style (Ruff, Flake8, Pylint). *(ch. 10.5, 12.1)* + +--- + +## M + +**Métaclasse** — « Classe d'une classe » : contrôle la création des classes elles-mêmes (`type` par défaut). *(ch. 3.5)* + +**Méthode de classe / statique** — `@classmethod` reçoit la classe (`cls`) ; `@staticmethod` ne reçoit ni `self` ni `cls` (fonction rangée dans la classe). *(ch. 3.4)* + +**Méthode spéciale (*dunder*)** — Méthode au nom encadré de doubles tirets bas (`__init__`, `__str__`, `__eq__`, `__enter__`, `__iter__`…) appelée *automatiquement* par Python pour une opération donnée (création, affichage via `str()`, comparaison `==`, bloc `with`, boucle `for`…). Elles permettent à vos objets de se comporter comme des types natifs. *(ch. 3.3)* + +**Mocking** — Remplacement d'une dépendance par un objet simulé (`unittest.mock`) pour isoler le code testé. *(ch. 10.2)* + +**Module / Package** — Un *module* est un fichier `.py` ; un *package* est un dossier de modules (avec `__init__.py`). *(ch. 6)* + +--- + +## N + +**Namespace (*espace de noms*)** — Table associant des noms à des objets (local, englobant, global, *built-in* : règle **LEGB**). *(ch. 1.4)* + +**ndarray** — Tableau N-dimensionnel de NumPy, homogène et contigu en mémoire, base des calculs vectorisés. *(ch. 13.1)* + +--- + +## O + +**ORM (*Object-Relational Mapping*)** — Couche (ex. SQLAlchemy) qui fait correspondre des classes/objets Python à des tables/lignes de base de données. *(ch. 11.6)* + +--- + +## P + +**Paramètres variadiques (`*args` / `**kwargs`)** — `*args` collecte les arguments positionnels supplémentaires (en tuple), `**kwargs` les arguments nommés (en dict). *(ch. 1.4)* + +**Patron de conception (*design pattern*)** — Solution réutilisable et éprouvée à un problème de conception récurrent (Singleton, Factory, Observer, Strategy…). *(ch. 12.3)* + +**PEP (*Python Enhancement Proposal*)** — Document de proposition d'évolution de Python (voir [Annexe B](02-pep-et-standards.md)). *(transversal)* + +**Polymorphisme** — Capacité d'objets de types différents à répondre à la même interface/méthode. *(ch. 3.2)* + +**Profilage (*profiling*)** — Mesure des temps d'exécution et des ressources par fonction (`cProfile`, `timeit`) pour cibler les optimisations. *(ch. 9.4, 12.4)* + +**Propriété (*property*)** — Attribut « calculé » exposant un *getter*/*setter* via `@property`, sans changer l'interface. *(ch. 3.4)* + +**Pydantic** — Bibliothèque de validation de données par les annotations de types, au cœur de FastAPI. *(ch. 11.2)* + +--- + +## R + +**Récursion** — Technique où une fonction s'appelle elle-même, avec un *cas de base* qui arrête la descente. *(ch. 1.4)* + +**REST** — Style d'architecture d'API web fondé sur les ressources et les verbes HTTP (GET/POST/PUT/DELETE). *(ch. 11.5)* + +--- + +## S + +**Sérialisation** — Conversion d'un objet en un format stockable/transmissible (JSON, pickle) et inversement (*désérialisation*). *(ch. 4.2, 4.3)* + +**Series** — Tableau 1D étiqueté de pandas (l'équivalent d'une colonne de DataFrame). *(ch. 13.2.1)* + +**Slicing (*découpage*)** — Extraction d'une sous-partie d'une séquence/tableau via `[début:fin:pas]`. *(ch. 2.1, 13.1.2)* + +--- + +## T + +**Thread / Processus** — Un *thread* partage la mémoire de son processus (idéal pour l'E/S) ; un *processus* a sa propre mémoire (idéal pour le CPU). *(ch. 8.1)* + +**Traceback (*trace d'appels*)** — Rapport affiché lorsqu'une exception n'est pas interceptée : il liste la suite des appels de fonctions ayant mené à l'erreur (à lire de bas en haut, l'erreur réelle se trouvant en dernier). *(ch. 9.1, 9.3)* + +**Tuple nommé (*namedtuple*)** — Tuple dont les champs sont accessibles par nom. *(ch. 2.3)* + +--- + +## V + +**Vectorisation** — Application d'une opération à un tableau entier en une instruction (code C optimisé) au lieu d'une boucle Python. *(ch. 13.1.1)* + +**venv (*environnement virtuel*)** — Environnement Python isolé propre à un projet, avec ses propres dépendances. *(ch. 6.4)* + +**Verrou (*lock*) / Condition de course (*race condition*)** — Un *verrou* protège une ressource partagée entre threads ; une *condition de course* est un bug dû à un accès concurrent non synchronisé. *(ch. 8.3)* + +**Vue vs Copie** — En NumPy/pandas, une *vue* partage les données de l'original (la modifier le modifie) ; une *copie* est indépendante. *(ch. 13.1.2)* + +--- + +## W + +**Walrus (`:=`)** — Opérateur d'affectation dans une expression (`if (n := len(xs)) > 5:`) : il affecte *et* renvoie une valeur. Introduit par la PEP 572. *(ch. 1.3)* + +**Wheel (`.whl`) / PyPI** — Le *wheel* est le format de paquet installable ; *PyPI* est le dépôt public où l'on publie (`uv publish`/`twine`) et installe (`pip`) les paquets. *(ch. 6.3, 12.5)* + +--- + +🔝 Retour au [Sommaire](/SOMMAIRE.md) · Annexe suivante : [Récapitulatif des PEP et standards](02-pep-et-standards.md) ⏭️ diff --git a/annexes/02-pep-et-standards.md b/annexes/02-pep-et-standards.md new file mode 100644 index 0000000..ea137ce --- /dev/null +++ b/annexes/02-pep-et-standards.md @@ -0,0 +1,96 @@ +🔝 Retour au [Sommaire](/SOMMAIRE.md) + +# Annexe B — Récapitulatif des PEP et standards + +Une **PEP** (*Python Enhancement Proposal*) est un document décrivant une évolution proposée du langage ou de son écosystème. Cette annexe synthétise les PEP **réellement utiles** pour comprendre les choix de cette formation, regroupées par thème. + +> 💡 Référence officielle : [peps.python.org](https://peps.python.org/). La colonne « Version » indique la version de Python qui a introduit la fonctionnalité (— pour les PEP de processus/conventions). + +--- + +## 🐍 Le langage : syntaxe et fonctionnalités + +| PEP | Apport | Version | Chapitre | +|-----|--------|---------|----------| +| **PEP 20** | *The Zen of Python* (`import this`) — la philosophie du langage | — | transversal | +| **PEP 498** | f-strings : `f"{nom}"` | 3.6 | 1.2 | +| **PEP 572** | Opérateur *walrus* `:=` (affectation dans une expression) | 3.8 | 1.3 | +| **PEP 343** | Instruction `with` et gestionnaires de contexte | 2.5 | 4.1, 12.3 | +| **PEP 255 / 342 / 380** | Générateurs, `yield`, puis `yield from` | 2.2 → 3.3 | 5.4 | +| **PEP 492 / 525 / 530** | `async`/`await`, générateurs et compréhensions asynchrones | 3.5-3.6 | 8.2 | +| **PEP 318** | Décorateurs de fonctions et de méthodes (`@`) | 2.4 | 3.4, 5.3 | +| **PEP 3119** | Classes de base abstraites (`abc`, `@abstractmethod`) | 3.0 | 3, 12.3 | +| **PEP 557** | `dataclasses` (`@dataclass`) | 3.7 | 3, 12.3 | +| **PEP 634-636** | *Structural pattern matching* (`match`/`case`) | 3.10 | 1.3 | +| **PEP 703** | *Free-threading* (interpréteur sans GIL), **expérimental** | 3.13 | 8.1, 12.4 | +| **PEP 744** | Compilateur JIT, **expérimental** | 3.13 | 12.4 | + +--- + +## 🏷️ Le typage (annotations de types) + +| PEP | Apport | Version | Chapitre | +|-----|--------|---------|----------| +| **PEP 484** | Annotations de types (`def f(x: int) -> str`) | 3.5 | 1.6, 10.6 | +| **PEP 526** | Annotations de variables (`x: int = 0`) | 3.6 | 1.6 | +| **PEP 585** | Génériques natifs : `list[int]` au lieu de `List[int]` | 3.9 | 7.6 | +| **PEP 604** | Unions avec `\|` : `int \| None` au lieu de `Optional[int]` | 3.10 | 7.6 | +| **PEP 612 / 646 / 655** | `ParamSpec`, génériques variadiques, `TypedDict` requis/optionnel | 3.10-3.11 | 7.6 | +| **PEP 695** | Nouvelle syntaxe des génériques et alias `type` | 3.12 | 7.6 | +| **PEP 563 / 649** | Évaluation différée des annotations | 3.7 / 3.14 | 7.6 | + +> ⚠️ **Socle de la formation** : Python **3.12+**. On privilégie donc `list[str]`, `dict[str, int]`, `X | None` (PEP 585/604) plutôt que `typing.List`, `typing.Optional`. + +--- + +## 📝 Style et documentation + +| PEP | Apport | Chapitre | +|-----|--------|----------| +| **PEP 8** | Guide de style officiel (indentation, nommage, espaces) | 10.5 | +| **PEP 257** | Conventions de *docstrings* | 10.4 | +| **PEP 287** | Docstrings reStructuredText (optionnel) | 10.4 | + +Outils qui appliquent ces standards : **Ruff** (lint + format), **Black**, **mypy** *(ch. 10, 12.1)*. + +--- + +## 📦 Packaging et projets + +| PEP | Apport | Chapitre | +|-----|--------|----------| +| **PEP 517 / 518** | Système de *build* standardisé via `pyproject.toml` | 12.1, 12.5 | +| **PEP 621** | Métadonnées de projet dans le tableau `[project]` | 12.1, 12.5 | +| **PEP 440** | Schéma de versions ; **PEP 508** : spécification des dépendances | 6.3 | +| **PEP 427 / 660** | Format *wheel* (`.whl`) et installations éditables (`pip install -e`) | 12.5 | +| **PEP 735** | Groupes de dépendances (`[dependency-groups]`) | 12.1 | +| **PEP 405** | Environnements virtuels (`venv`) | 6.4 | + +> 💡 Outillage moderne (2026) reposant sur ces PEP : **uv**, **Poetry 2.x**, **hatchling**, **build**/**twine** *(ch. 12.1, 12.5)*. + +--- + +## ⚙️ Le processus Python + +| PEP | Apport | +|-----|--------| +| **PEP 1** | Définit ce qu'est une PEP et son cycle de vie | +| **PEP 602** | Cadence de publication **annuelle** (une version mineure par an) | +| **PEP 387** | Politique de rétrocompatibilité et de dépréciation | + +**Cycle de versions** : une version de Python est publiée chaque **octobre**, avec ~5 ans de support (2 ans de corrections de bugs, puis sécurité uniquement). + +--- + +## Comment lire une PEP ? + +1. **Abstract** : le résumé (lisez-le toujours en premier). +2. **Motivation** : le problème résolu. +3. **Specification** : les détails techniques. +4. **Rationale / Rejected Ideas** : les choix de conception et alternatives écartées. + +Les PEP les plus utiles à connaître par cœur pour le quotidien : **PEP 8** (style), **PEP 20** (philosophie), **PEP 484** (typage), **PEP 621** (packaging). + +--- + +🔝 Retour au [Sommaire](/SOMMAIRE.md) · Annexe précédente : [Glossaire](01-glossaire.md) · Annexe suivante : [Aide-mémoire](03-aide-memoire.md) ⏭️ diff --git a/annexes/03-aide-memoire.md b/annexes/03-aide-memoire.md new file mode 100644 index 0000000..784d73b --- /dev/null +++ b/annexes/03-aide-memoire.md @@ -0,0 +1,262 @@ +🔝 Retour au [Sommaire](/SOMMAIRE.md) + +# Annexe C — Aide-mémoire (*cheat-sheets*) + +Rappels condensés des syntaxes et commandes les plus utilisées. Pour les détails, reportez-vous au chapitre indiqué. + +> ⚠️ **Note de maintenance** : les commandes d'outils (versions, options) évoluent vite — en cas de doute, la documentation officielle fait foi. Socle de la formation : **Python 3.12+**. +> +> 💡 Chaque ligne de code est **autonome et copiable** (`...` représente du code à compléter). + +--- + +## Structures de données *(ch. 2)* + +```python +liste = [1, 2, 3] # mutable, ordonnée +tuple_ = (1, 2, 3) # immuable +ensemble = {1, 2, 3} # valeurs uniques, non ordonnées +dico = {"a": 1, "b": 2} # paires clé-valeur + +liste[0] # premier élément +liste[-1] # dernier élément +liste[1:3] # slice [début:fin] +liste.append(4) # ajoute à la fin +liste.pop() # retire et renvoie le dernier +dico.get("c", 0) # valeur par défaut si clé absente +"a" in dico # test d'appartenance (O(1) pour dict/set) +``` + +## Compréhensions *(ch. 2.2)* + +```python +[x * 2 for x in xs if x > 0] # liste +{k: v for k, v in items} # dictionnaire +{x for x in xs} # set +(x * 2 for x in xs) # générateur (paresseux) +``` + +## Chaînes et f-strings *(ch. 1.2, 2.4)* + +```python +f"{nom} a {age} ans" # interpolation +f"{prix:.2f} €" # 2 décimales +f"{n:>10}" # alignement à droite sur 10 +f"{valeur=}" # debug : affiche "valeur=..." +texte.strip().lower() # nettoyage + minuscules +texte.split(",") # découpe en liste +"-".join(liste) # assemble avec un séparateur +texte.replace("a", "b") # remplacement +texte.startswith("http") # test de préfixe + +import re +re.findall(r"\d+", texte) # toutes les suites de chiffres +``` + +## Fonctions *(ch. 1.4, 5)* + +```python +def f(a, b=0, *args, **kwargs): # positionnels, défaut, variadiques + ... + +carre = lambda x: x * 2 # fonction anonyme + +list(map(str, xs)) # applique une fonction +list(filter(None, xs)) # garde les valeurs « vraies » + +from functools import reduce, lru_cache, cache +``` + +## Exceptions *(ch. 1.5, 9)* + +```python +try: + ... +except (ValueError, KeyError) as e: + ... +else: # si aucune exception + ... +finally: # toujours exécuté + ... + +raise ValueError("message") from cause # chaînage d'exceptions + +# Groupes d'exceptions (3.11+) : except* (ne se mélange pas avec except) +try: + ... +except* TypeError: + ... +``` + +## Fichiers et chemins *(ch. 4)* + +```python +from pathlib import Path + +chemin = Path("data") / "fichier.txt" # construction de chemin +chemin.exists() # le fichier existe-t-il ? +contenu = chemin.read_text(encoding="utf-8") + +with open(chemin, "w", encoding="utf-8") as f: + f.write("contenu") + +import json +obj = json.load(f) # lecture JSON +json.dump(obj, f) # écriture JSON +``` + +## Programmation orientée objet *(ch. 3)* + +```python +from dataclasses import dataclass + +@dataclass +class Point: + x: int + y: int + + def norme(self) -> float: + return (self.x**2 + self.y**2) ** 0.5 + +class Enfant(Parent): + def __init__(self, *args): + super().__init__(*args) # appel au parent +``` + +## Annotations de types *(ch. 1.6, 7.6)* + +```python +def f(noms: list[str], age: int | None = None) -> dict[str, int]: + ... + +type Vecteur = list[float] # alias de type (PEP 695, 3.12+) + +from typing import Protocol, TypeVar, Self +``` + +## Concurrence *(ch. 8)* + +```python +import asyncio + +async def fetch(u): + await asyncio.sleep(1) + return u + +async def main(): + return await asyncio.gather(*(fetch(u) for u in urls)) + +asyncio.run(main()) + +from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor + +with ThreadPoolExecutor() as ex: # idéal pour l'E/S + resultats = list(ex.map(telecharger, urls)) +``` + +--- + +## 🛠️ Outils en ligne de commande *(ch. 6, 10, 12)* + +### uv — gestion de projet (le plus rapide) +```bash +uv init mon_projet # nouveau projet +uv add requests # ajoute une dépendance +uv add --dev pytest ruff # dépendances de développement +uv run python app.py # exécute dans l'environnement +uv run pytest # lance les tests +uv sync # synchronise depuis le fichier de verrouillage +uv lock # met à jour uv.lock +uv python install 3.12 # installe une version de Python +uv build # construit sdist + wheel +uv publish # publie sur PyPI +uvx ruff check . # outil ponctuel (sans l'installer) +``` + +### venv + pip (classique, sans uv) *(ch. 6.4)* +```bash +python -m venv .venv # crée un environnement virtuel +source .venv/bin/activate # active (Linux/macOS) +.venv\Scripts\activate # active (Windows) +pip install requests # installe un paquet +pip install -r requirements.txt # installe depuis un fichier +pip freeze > requirements.txt # fige les versions installées +``` + +### Ruff — lint + format +```bash +ruff check . # analyse (linting) +ruff check --fix . # corrige ce qui peut l'être +ruff format . # formate (remplace Black) +``` + +### pytest / mypy +```bash +pytest # tous les tests +pytest -v --cov=src # verbeux + couverture +pytest tests/test_x.py # un fichier précis +mypy src/ # vérification des types +``` + +### Git (essentiel) *(ch. 12.2)* +```bash +git status # état du dépôt +git add . # prépare toutes les modifications +git commit -m "feat: ..." # enregistre un commit +git push # envoie vers le distant +git switch -c feature/x # crée et bascule sur une branche +git switch main # bascule sur main +git merge feature/x # fusionne une branche +git log --oneline --graph # historique condensé +git restore # annule les modifications non indexées +git revert # annule un commit (par un nouveau commit) +``` + +--- + +## 📊 NumPy *(ch. 13.1)* + +```python +import numpy as np + +a = np.array([[1, 2], [3, 4]]) +np.zeros((2, 3)) # tableau de zéros +np.arange(0, 10, 2) # 0,2,4,6,8 +a.shape # dimensions (lignes, colonnes) +a.dtype # type des éléments +a * 2 # opération vectorisée (élément par élément) +a @ b # produit matriciel +a.sum(axis=0) # somme par colonne +a[a > 2] # masque booléen + +rng = np.random.default_rng() # générateur moderne (recommandé) +rng.integers(0, 10, 5) # 5 entiers dans [0, 10[ +``` + +## 🐼 pandas *(ch. 13.2)* + +```python +import pandas as pd + +df = pd.read_csv("data.csv") +df.head() # premières lignes +df.info() # types et valeurs manquantes +df.describe() # statistiques descriptives +df["col"] # une colonne (Series) +df[["a", "b"]] # plusieurs colonnes +df[df["age"] > 18] # filtrage par condition +df.loc[i, "col"] # accès par étiquette +df.iloc[0] # accès par position +df["x"] = df["a"] * df["b"] # nouvelle colonne +df.groupby("ville")["ventes"].sum() # agrégation par groupe +df.isnull().sum() # NaN par colonne +df["c"] = df["c"].fillna(df["c"].mean()) # remplissage par la moyenne +df.merge(autre, on="id") # jointure type SQL +df.sort_values("col", ascending=False) # tri +``` + +> ⚠️ **pandas 3.0+ (Copy-on-Write)** : l'**assignation chaînée** (`df[cond]["c"] = ...` ou `df["c"].fillna(0, inplace=True)`) est désormais **sans effet** et déclenche un avertissement `ChainedAssignmentError`. Préférez la **réassignation** (`df["c"] = df["c"].fillna(0)`) ou `.loc` en une seule indexation (`df.loc[cond, "c"] = ...`). À noter : `inplace=True` reste valide sur un DataFrame entier (`df.fillna(0, inplace=True)`). + +--- + +🔝 Retour au [Sommaire](/SOMMAIRE.md) · Annexe précédente : [PEP et standards](02-pep-et-standards.md) · Annexe suivante : [Pour aller plus loin](04-pour-aller-plus-loin.md) ⏭️ diff --git a/annexes/04-pour-aller-plus-loin.md b/annexes/04-pour-aller-plus-loin.md new file mode 100644 index 0000000..21945b8 --- /dev/null +++ b/annexes/04-pour-aller-plus-loin.md @@ -0,0 +1,81 @@ +🔝 Retour au [Sommaire](/SOMMAIRE.md) + +# Annexe D — Pour aller plus loin + +Vous avez terminé les 13 chapitres : vous maîtrisez Python du débutant à l'avancé, plus une introduction à la Data Science. Et maintenant ? Cette annexe propose des **directions d'approfondissement** selon vos objectifs, ainsi que des **ressources sélectionnées**. + +> 💡 Un conseil avant tout : **consolidez par la pratique**. Choisissez **un** projet réel qui vous motive et appliquez ce que vous avez appris (structure, tests, Git, déploiement) avant d'empiler de nouvelles technologies. + +--- + +## 🧭 Feuille de route par objectif + +### Vous visez la Data Science / le Machine Learning +Suite naturelle du chapitre 13. +1. **Statistiques & probabilités** : distributions, tests d'hypothèses, inférence. +2. **scikit-learn** : régression, classification, clustering, validation croisée, *pipelines*. +3. **Deep Learning** : **PyTorch** (recherche, très populaire) ou **Keras/TensorFlow**. +4. **Données spécialisées** : NLP (spaCy, *transformers*), vision (OpenCV), séries temporelles. +5. **Passage à l'échelle** : **Polars** (alternative pandas ultra-rapide), DuckDB, Spark. + +### Vous visez le développement web / backend +Suite du chapitre 11. +1. **FastAPI avancé** : authentification (OAuth2/JWT), dépendances, tâches de fond, WebSockets. +2. **Django** : framework « batteries incluses » (ORM, admin, auth) pour les grosses applications. +3. **Bases de données** : SQL avancé, migrations (Alembic), Redis (cache), bases NoSQL. +4. **Asynchrone en production** : `asyncio` avancé, files de tâches (Celery, ARQ). +5. **Front** : un peu de HTML/CSS/JS, ou HTMX pour rester côté Python. + +### Vous visez le Data Engineering / la production (MLOps/DevOps) +Suite du chapitre 12. +1. **Orchestration** : Airflow, Prefect, dbt pour les *pipelines* de données. +2. **Conteneurs & cloud** : Docker avancé, Kubernetes, services managés (AWS/GCP/Azure). +3. **CI/CD avancé** : tests d'intégration, déploiement continu, *infrastructure as code* (Terraform). +4. **MLOps** : suivi d'expériences (MLflow), versionnement de données (DVC), *serving* de modèles. +5. **Observabilité** : logs structurés, métriques (Prometheus), traçage, alerting. + +### Vous voulez devenir un·e meilleur·e développeur·se Python +Transversal, quel que soit le domaine. +1. **Algorithmique & structures de données** : complexité, arbres, graphes (utile en entretien). +2. **Conception logicielle** : SOLID approfondi, architecture hexagonale, DDD. +3. **Performance** : profilage fin, **Cython**/**Numba**, extensions en **Rust** (PyO3 / maturin). +4. **Concurrence avancée** : `asyncio` en profondeur, *free-threading* (PEP 703). +5. **Contribuer à l'open source** : lire le code de projets matures (Requests, FastAPI, pandas). + +--- + +## 📚 Ressources sélectionnées + +### Livres de référence +- **« Fluent Python »** (Luciano Ramalho) — *le* livre pour passer Python au niveau supérieur. +- **« Python for Data Analysis »** (Wes McKinney, créateur de pandas) — pour la Data Science. +- **« Hands-On Machine Learning »** (Aurélien Géron) — ML/DL très pratique. +- **« Architecture Patterns with Python »** (Percival & Gregory) — conception backend. + +### Sites & documentation +- **Documentation officielle Python** : [docs.python.org/fr](https://docs.python.org/fr/) (en français). +- **Real Python** : [realpython.com](https://realpython.com/) — tutoriels approfondis. +- **PyData / talks YouTube** — conférences de qualité (PyCon, EuroPython, PyData). +- **peps.python.org** — pour suivre l'évolution du langage (voir [Annexe B](02-pep-et-standards.md)). + +### Pratiquer +- **Kaggle** — jeux de données et compétitions (Data Science). +- **Exercism / Advent of Code** — exercices d'algorithmique et de logique. +- **GitHub** — créez un portfolio de projets ; lisez du code open source. + +### Communautés +- **Stack Overflow**, **r/Python** et **r/learnpython** (Reddit), **Discord Python francophone**. +- Meetups locaux et associations (AFPy pour la communauté Python francophone). + +--- + +## ✅ La bonne mentalité + +- **La technologie change, les fondamentaux restent.** Ce que vous avez appris ici (structure, tests, lisibilité, Git, types) reste valable quel que soit le *framework* à la mode. +- **Apprenez juste-à-temps.** N'apprenez pas une techno « au cas où » : apprenez-la quand un projet en a besoin. +- **Lisez du code.** On progresse autant en lisant du bon code qu'en en écrivant. +- **Restez curieux et patient.** Devenir un·e bon·ne développeur·se est un parcours continu — vous avez déjà posé des fondations solides. 🚀 + +--- + +🔝 Retour au [Sommaire](/SOMMAIRE.md) · Annexe précédente : [Aide-mémoire](03-aide-memoire.md) diff --git a/annexes/README.md b/annexes/README.md new file mode 100644 index 0000000..254be65 --- /dev/null +++ b/annexes/README.md @@ -0,0 +1,27 @@ +🔝 Retour au [Sommaire](/SOMMAIRE.md) + +# 📎 Annexes + +Ces annexes regroupent des **références transversales** pour accompagner les 13 chapitres de la formation. Contrairement aux chapitres (qui *enseignent*), les annexes servent à *consulter rapidement* : une définition, une PEP, une syntaxe, une piste pour la suite. + +## Contenu + +| Annexe | Description | +|--------|-------------| +| **A.** [Glossaire](01-glossaire.md) | Définitions des termes essentiels, avec renvois aux chapitres | +| **B.** [Récapitulatif des PEP et standards](02-pep-et-standards.md) | Les PEP clés (langage, typage, style, packaging) qui structurent le cours | +| **C.** [Aide-mémoire (*cheat-sheets*)](03-aide-memoire.md) | Rappels condensés de syntaxe et de commandes d'outils | +| **D.** [Pour aller plus loin](04-pour-aller-plus-loin.md) | Feuille de route post-formation et ressources sélectionnées | + +## Comment les utiliser ? + +- Consultez le **glossaire** dès qu'un terme vous échappe. +- Gardez l'**aide-mémoire** ouvert pendant que vous codez. +- Parcourez le **récap des PEP** pour comprendre *pourquoi* le langage est ainsi. +- Lisez **« Pour aller plus loin »** quand vous aurez terminé les 13 chapitres. + +> 💡 Ces annexes sont volontairement **concises** et **durables** : elles renvoient aux chapitres pour les explications détaillées plutôt que de les dupliquer. + +--- + +🔝 Retour au [Sommaire](/SOMMAIRE.md) diff --git a/pyproject.toml b/pyproject.toml index 645ecff..5a2b8aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,20 +1,20 @@ [tool.poetry] -name = "formation-python-complete" +name = "formation-python" version = "1.0.0" description = "Formation Python complète et moderne - Du débutant à l'avancé" authors = ["Nicolas DEOUX "] license = "MIT" readme = "README.md" -homepage = "https://github.com/NDXDeveloper/formation-python-complete" -repository = "https://github.com/NDXDeveloper/formation-python-complete" +homepage = "https://github.com/NDXDeveloper/formation-python" +repository = "https://github.com/NDXDeveloper/formation-python" keywords = ["python", "formation", "tutorial", "fastapi", "data-science"] [tool.poetry.dependencies] -python = "^3.10" +python = "^3.12" # Web Frameworks -fastapi = {version = "^0.109.0", optional = true} -uvicorn = {extras = ["standard"], version = "^0.27.0", optional = true} +fastapi = {version = ">=0.109.0", optional = true} +uvicorn = {extras = ["standard"], version = ">=0.27.0", optional = true} flask = {version = "^3.0.0", optional = true} requests = {version = "^2.31.0", optional = true} @@ -23,24 +23,24 @@ sqlalchemy = {version = "^2.0.25", optional = true} pydantic = {version = "^2.5.0", optional = true} # HTTP & Auth -httpx = {version = "^0.27.0", optional = true} +httpx = {version = ">=0.27.0", optional = true} email-validator = {version = "^2.1.0", optional = true} pyjwt = {version = "^2.8.0", optional = true} # Data Science -numpy = {version = "^1.24.0", optional = true} -pandas = {version = "^2.1.0", optional = true} +numpy = {version = ">=2.0.0", optional = true} +pandas = {version = ">=3.0.0", optional = true} matplotlib = {version = "^3.8.0", optional = true} -plotly = {version = "^5.18.0", optional = true} +plotly = {version = ">=5.18.0", optional = true} [tool.poetry.group.dev.dependencies] # Type Checking -mypy = "^1.8.0" +mypy = ">=1.8.0" # Testing -pytest = "^7.4.0" -pytest-cov = "^4.1.0" -pytest-asyncio = "^0.23.0" +pytest = ">=8.0.0" +pytest-cov = ">=5.0.0" +pytest-asyncio = ">=0.23.0" # Code Quality ruff = ">=0.4.0" @@ -52,13 +52,13 @@ all = ["fastapi", "uvicorn", "flask", "requests", "sqlalchemy", "pydantic", "htt [tool.ruff] line-length = 88 -target-version = "py310" +target-version = "py312" [tool.ruff.format] quote-style = "double" [tool.mypy] -python_version = "3.10" +python_version = "3.12" warn_return_any = true warn_unused_configs = true disallow_untyped_defs = true