---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.0
kernelspec:
  name: python3
  display_name: Python 3
---

# Structures de contrôle

```{code-cell} python
:tags: [hide-input]

import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
```

## `if` / `elif` / `else`

La structure conditionnelle `if` est le mécanisme fondamental qui permet à un programme de prendre des décisions. En Python, cette structure repose sur l'**indentation** pour délimiter les blocs de code — là où d'autres langages utilisent des accolades `{}`. Cette règle n'est pas un choix arbitraire : elle garantit que la structure visuelle du code reflète toujours sa structure logique, rendant la lecture intuitive.

```{code-cell} python
temperature = 18

if temperature < 0:
    print("Il gèle : mettez un manteau chaud.")
elif temperature < 10:
    print("Il fait froid : prenez une veste.")
elif temperature < 20:
    print("Temps frais : un pull suffira.")
elif temperature < 30:
    print("Agréable : habits légers.")
else:
    print("Canicule : restez à l'ombre et hydratez-vous.")
```

```{prf:remark}
:label: remark-03-01
L'indentation en Python est **syntaxiquement obligatoire** : c'est ce qui définit les blocs. La convention PEP 8 prescrit **4 espaces** par niveau d'indentation (et non des tabulations). Mélanger espaces et tabulations dans un même fichier est une erreur de syntaxe en Python 3. La plupart des éditeurs modernes configurés pour Python convertissent automatiquement les tabulations en espaces.
```

### Expressions conditionnelles ternaires

Python propose une forme condensée pour les conditions simples, l'**expression conditionnelle** (souvent appelée opérateur ternaire), qui permet d'écrire une condition sur une seule ligne :

```{code-cell} python
# Forme longue
age = 20
if age >= 18:
    statut = "majeur"
else:
    statut = "mineur"

# Forme ternaire équivalente
statut = "majeur" if age >= 18 else "mineur"
print(statut)

# Usages courants
maximum = a if (a := 15) > (b := 10) else b
print(f"Maximum de {a} et {b} : {maximum}")

# Calcul conditionnel
prix_ttc = lambda prix_ht: prix_ht * 1.20 if prix_ht > 0 else 0
print(prix_ttc(100))
```

```{prf:definition} Expression vs instruction
:label: definition-03-01
En Python, une **instruction** (*statement*) est une unité d'exécution qui ne produit pas de valeur (comme `if`, `for`, `while`, `def`). Une **expression** est une unité qui s'évalue en une valeur (comme `x + 1`, `"bonjour".upper()`, `True if x > 0 else False`). L'expression conditionnelle ternaire (`valeur_si_vrai if condition else valeur_si_faux`) est une expression : elle peut apparaître partout où une valeur est attendue — dans une assignation, un appel de fonction, une compréhension.
```

## La boucle `while`

La boucle `while` répète un bloc de code **tant qu'une condition est vraie**. Elle est adaptée aux situations où le nombre d'itérations n'est pas connu à l'avance.

```{code-cell} python
# Exemple : algorithme d'Euclide pour le PGCD
def pgcd(a, b):
    while b != 0:
        a, b = b, a % b
    return a

print(pgcd(48, 18))   # 6
print(pgcd(100, 75))  # 25
```

### `break` et `continue`

```{code-cell} python
# break : sortir immédiatement de la boucle
n = 0
while True:  # boucle infinie contrôlée par break
    n += 1
    if n > 5:
        break
print(f"Sorti de la boucle à n = {n}")

# continue : passer à l'itération suivante
i = 0
résultats = []
while i < 10:
    i += 1
    if i % 2 == 0:
        continue  # Ignorer les pairs
    résultats.append(i)
print(f"Nombres impairs : {résultats}")
```

### La clause `else` sur `while`

Python offre une fonctionnalité originale : une clause `else` sur les boucles. Le bloc `else` est exécuté **uniquement si la boucle s'est terminée normalement**, c'est-à-dire sans être interrompue par un `break`.

```{code-cell} python
# Recherche avec while ... else
def chercher_diviseur(n, limite):
    d = 2
    while d <= limite:
        if n % d == 0:
            print(f"{n} est divisible par {d}")
            break
        d += 1
    else:
        print(f"Aucun diviseur trouvé jusqu'à {limite}")

chercher_diviseur(15, 10)
chercher_diviseur(17, 10)
```

```{prf:remark}
:label: remark-03-02
La clause `else` sur les boucles est l'une des fonctionnalités les plus déroutantes pour les nouveaux venus en Python, mais elle s'avère très utile dans des algorithmes de recherche. Une façon de la lire : "la boucle s'est terminée sans trouver ce qu'elle cherchait". Elle évite d'avoir à utiliser un drapeau booléen (`trouvé = False`) pour détecter si un `break` a eu lieu.
```

## La boucle `for`

La boucle `for` en Python n'est pas une boucle à compteur comme en C ou Java : c'est une boucle d'**itération sur une séquence** (ou tout objet itérable). Cette distinction est fondamentale et explique la puissance et l'élégance du `for` Python.

```{code-cell} python
# Itération sur une liste
fruits = ["pomme", "banane", "cerise", "datte"]
for fruit in fruits:
    print(f"- {fruit}")
```

### `range()`

`range()` génère une séquence d'entiers à la demande, sans créer de liste en mémoire :

```{code-cell} python
# range(stop) : de 0 à stop-1
for i in range(5):
    print(i, end=" ")
print()

# range(start, stop) : de start à stop-1
for i in range(2, 8):
    print(i, end=" ")
print()

# range(start, stop, step) : avec un pas
for i in range(0, 20, 3):
    print(i, end=" ")
print()

# Compter à rebours
for i in range(10, 0, -2):
    print(i, end=" ")
print()
```

### `enumerate()` et `zip()`

```{code-cell} python
# enumerate() : indice + valeur simultanément
langages = ["Python", "Rust", "Go", "TypeScript"]
for i, langage in enumerate(langages, start=1):
    print(f"{i}. {langage}")
```

```{code-cell} python
# zip() : itérer sur plusieurs séquences en parallèle
noms = ["Alice", "Bob", "Charlie"]
notes = [18, 14, 16]
matieres = ["Maths", "Physique", "Informatique"]

for nom, note, matiere in zip(noms, notes, matieres):
    print(f"{nom} a eu {note}/20 en {matiere}")

# zip s'arrête à la plus courte séquence
# Pour toutes les longueurs : itertools.zip_longest
```

### La clause `else` sur `for`

```{code-cell} python
def est_premier(n):
    if n < 2:
        return False
    for d in range(2, int(n**0.5) + 1):
        if n % d == 0:
            break
    else:
        return True
    return False

print([n for n in range(2, 30) if est_premier(n)])
```

## `match` / `case` (Python 3.10+)

Introduit en Python 3.10 par la **PEP 634**, le `match`/`case` apporte le **filtrage structurel** (*structural pattern matching*). Il va bien au-delà d'un simple `switch`/`case` : il peut décomposer des structures de données complexes, capturer des variables, tester des types et des gardes conditionnelles, le tout de façon déclarative et lisible.

```{prf:definition} Filtrage structurel
:label: definition-03-02
Le **filtrage structurel** est une technique qui permet de tester simultanément la forme et le contenu d'une valeur, en capturant des parties de cette valeur dans des variables. Contrairement à une succession de `if`/`elif`, le `match`/`case` Python ne se contente pas de comparer des valeurs scalaires : il peut décomposer des listes, des tuples, des objets et des dictionnaires en une seule expression.
```

### Patterns de valeur et de capture

```{code-cell} python
def analyser_commande(commande):
    match commande:
        case "quitter":
            return "Au revoir !"
        case "aide":
            return "Commandes disponibles : quitter, aide, version"
        case "version":
            return "Python 3.12"
        case _:              # Wildcard : correspond à tout
            return f"Commande inconnue : {commande!r}"

print(analyser_commande("quitter"))
print(analyser_commande("aide"))
print(analyser_commande("inconnu"))
```

### Patterns de séquence

```{code-cell} python
def traiter_point(point):
    match point:
        case (0, 0):
            return "Origine"
        case (x, 0):
            return f"Sur l'axe X, x = {x}"
        case (0, y):
            return f"Sur l'axe Y, y = {y}"
        case (x, y):
            return f"Point en ({x}, {y})"
        case _:
            return "Pas un point valide"

print(traiter_point((0, 0)))
print(traiter_point((3, 0)))
print(traiter_point((2, 5)))
```

```{code-cell} python
# Patterns de séquence avec longueur variable
def analyser_liste(lst):
    match lst:
        case []:
            return "Liste vide"
        case [seul]:
            return f"Un seul élément : {seul}"
        case [premier, *reste]:
            return f"Premier : {premier}, reste : {reste}"

print(analyser_liste([]))
print(analyser_liste([42]))
print(analyser_liste([1, 2, 3, 4, 5]))
```

### Patterns de classe et gardes `if`

```{code-cell} python
from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

@dataclass
class Cercle:
    centre: Point
    rayon: float

def décrire_forme(forme):
    match forme:
        case Point(x=0, y=0):
            return "Point à l'origine"
        case Point(x=x, y=y) if x == y:
            return f"Point sur la diagonale : ({x}, {y})"
        case Point(x=x, y=y):
            return f"Point quelconque : ({x}, {y})"
        case Cercle(rayon=r) if r <= 0:
            return "Rayon invalide"
        case Cercle(centre=c, rayon=r):
            return f"Cercle de rayon {r} centré en ({c.x}, {c.y})"
        case _:
            return "Forme inconnue"

print(décrire_forme(Point(0, 0)))
print(décrire_forme(Point(3, 3)))
print(décrire_forme(Point(1, 5)))
print(décrire_forme(Cercle(Point(0, 0), 5)))
```

## Compréhensions

Les **compréhensions** sont une syntaxe concise et très pythonique pour construire des collections à partir d'itérables existants, avec un filtrage optionnel. Elles sont souvent plus lisibles et plus rapides que les boucles `for` équivalentes.

### Compréhensions de liste

```{code-cell} python
# Forme générale : [expression for variable in itérable if condition]

# Sans filtre
carrés = [x**2 for x in range(10)]
print(carrés)

# Avec filtre
carrés_pairs = [x**2 for x in range(10) if x % 2 == 0]
print(carrés_pairs)

# Imbriquée : produit cartésien
paires = [(x, y) for x in range(3) for y in range(3) if x != y]
print(paires)
```

### Compréhensions de dictionnaire et d'ensemble

```{code-cell} python
# Compréhension de dictionnaire
mots = ["bonjour", "monde", "python", "génial"]
longueurs = {mot: len(mot) for mot in mots}
print(longueurs)

# Inverser un dictionnaire
original = {"a": 1, "b": 2, "c": 3}
inverse = {v: k for k, v in original.items()}
print(inverse)

# Compréhension d'ensemble (élimine automatiquement les doublons)
nombres = [1, 2, 2, 3, 3, 3, 4]
ensemble = {x**2 for x in nombres}
print(ensemble)
```

### Expressions génératrices

```{code-cell} python
# Une expression génératrice est comme une compréhension de liste
# mais ne crée pas toute la liste en mémoire : elle génère les éléments
# à la demande (évaluation paresseuse)

# Somme sans créer de liste intermédiaire
total = sum(x**2 for x in range(1_000_000))
print(total)

# La parenthèse extérieure suffit quand c'est l'unique argument
premier_pair = next(x for x in range(100) if x % 2 == 0 and x > 10)
print(premier_pair)
```

```{prf:remark}
:label: remark-03-03
Les expressions génératrices sont préférables aux compréhensions de liste lorsque la collection n'est parcourue qu'une seule fois et qu'on n'a pas besoin de la stocker en entier. Elles consomment une mémoire constante, indépendamment de la taille de l'itérable source — un avantage décisif pour les grands jeux de données. En revanche, elles ne peuvent être parcourues qu'une seule fois : après épuisement, l'itérateur est vide.
```

## Visualisation des patterns `match`/`case`

```{code-cell} python
:tags: [hide-input]

fig, ax = plt.subplots(figsize=(13, 9))
ax.set_xlim(0, 13)
ax.set_ylim(0, 10)
ax.axis('off')
ax.set_title("Types de patterns dans le match / case Python 3.10+",
             fontsize=14, fontweight='bold', pad=20)

palette = sns.color_palette("muted", 6)

# Nœud racine
cx, cy = 6.5, 9.0
root_box = patches.FancyBboxPatch((cx - 1.2, cy - 0.4), 2.4, 0.8,
    boxstyle="round,pad=0.1", linewidth=2,
    edgecolor='#333333', facecolor='#333333', alpha=0.15)
ax.add_patch(root_box)
root_brd = patches.FancyBboxPatch((cx - 1.2, cy - 0.4), 2.4, 0.8,
    boxstyle="round,pad=0.1", linewidth=2,
    edgecolor='#333333', facecolor='none')
ax.add_patch(root_brd)
ax.text(cx, cy, "match valeur:", ha='center', va='center',
        fontsize=11, fontweight='bold', color='#333333',
        fontfamily='monospace')

patterns = [
    (1.1, 6.8, palette[0], "Valeur littérale",
     'case 42:\ncase "texte":\ncase True:',
     "Compare à une\nvaleur exacte"),
    (3.8, 6.8, palette[1], "Capture",
     'case x:\ncase nom:',
     "Lie la valeur\nà une variable"),
    (6.5, 6.8, palette[2], "Séquence",
     'case [a, b]:\ncase [h, *t]:',
     "Décompose\nlistes/tuples"),
    (9.2, 6.8, palette[3], "Classe",
     'case Point(x=x):\ncase Cercle(r=r):',
     "Décompose\ndes objets"),
    (11.9, 6.8, palette[4], "Wildcard",
     'case _:',
     "Correspond\nà tout"),
]

for (x, y, color, titre, code, descr) in patterns:
    # Boîte principale
    w, h = 2.2, 2.8
    box = patches.FancyBboxPatch((x - w/2, y - h/2), w, h,
        boxstyle="round,pad=0.12", linewidth=1.8,
        edgecolor=color, facecolor=color, alpha=0.18)
    ax.add_patch(box)
    brd = patches.FancyBboxPatch((x - w/2, y - h/2), w, h,
        boxstyle="round,pad=0.12", linewidth=1.8,
        edgecolor=color, facecolor='none')
    ax.add_patch(brd)
    ax.text(x, y + h/2 - 0.28, titre, ha='center', va='center',
            fontsize=8.5, fontweight='bold', color=color)
    # Code
    code_box = patches.FancyBboxPatch((x - w/2 + 0.1, y - 0.55),
        w - 0.2, 0.9,
        boxstyle="round,pad=0.05", linewidth=1,
        edgecolor=color, facecolor='white', alpha=0.9)
    ax.add_patch(code_box)
    ax.text(x, y - 0.1, code, ha='center', va='center',
            fontsize=7, color='#333333', fontfamily='monospace')
    # Description
    ax.text(x, y - h/2 + 0.35, descr, ha='center', va='center',
            fontsize=7.5, color='#555555', style='italic')
    # Flèche depuis la racine
    ax.annotate('', xy=(x, y + h/2 + 0.05),
                xytext=(cx, cy - 0.42),
                arrowprops=dict(arrowstyle='->', color='#aaaaaa', lw=1.2,
                                connectionstyle='arc3,rad=0'))

# Garde conditionnelle
gx, gy = 6.5, 3.5
g_box = patches.FancyBboxPatch((gx - 2.5, gy - 0.8), 5.0, 1.6,
    boxstyle="round,pad=0.15", linewidth=2,
    edgecolor=palette[5], facecolor=palette[5], alpha=0.18)
ax.add_patch(g_box)
g_brd = patches.FancyBboxPatch((gx - 2.5, gy - 0.8), 5.0, 1.6,
    boxstyle="round,pad=0.15", linewidth=2,
    edgecolor=palette[5], facecolor='none')
ax.add_patch(g_brd)
ax.text(gx, gy + 0.4, "Garde conditionnelle (if)", ha='center', va='center',
        fontsize=10, fontweight='bold', color=palette[5])
ax.text(gx, gy - 0.2, "case Point(x=x, y=y) if x > 0 and y > 0:",
        ha='center', va='center', fontsize=8.5, color='#333333',
        fontfamily='monospace')

ax.text(gx, 1.8,
        "Les gardes s'appliquent à tout type de pattern — elles raffinent\n"
        "la correspondance avec une condition arbitraire.",
        ha='center', va='center', fontsize=8.5, color='#666666',
        style='italic')

plt.tight_layout()
plt.show()
```

## Résumé

Dans ce chapitre, nous avons parcouru l'ensemble des structures de contrôle de Python :

- Le `if`/`elif`/`else` utilise l'**indentation obligatoire** pour délimiter les blocs. L'**expression ternaire** (`valeur_a if condition else valeur_b`) permet d'écrire des conditions simples en une ligne.
- La boucle `while` répète tant qu'une condition est vraie. `break` l'interrompt immédiatement, `continue` passe à l'itération suivante. La clause `else` sur `while` est exécutée uniquement si la boucle s'est terminée sans `break`.
- La boucle `for` itère sur n'importe quel **itérable**. `range()` génère des séquences d'entiers à la demande. `enumerate()` fournit l'indice et la valeur simultanément ; `zip()` permet d'itérer sur plusieurs séquences en parallèle. La clause `else` sur `for` fonctionne de même que sur `while`.
- Le `match`/`case` (Python 3.10+) apporte le **filtrage structurel** : il peut décomposer des valeurs littérales, des séquences, des objets (classes), capturer des variables, et se raffiner avec des gardes `if`. C'est un outil puissant pour gérer des structures de données complexes de façon déclarative.
- Les **compréhensions** (de liste, de dictionnaire, d'ensemble) sont une syntaxe concise pour construire des collections avec filtrage optionnel. Les **expressions génératrices** sont leur équivalent paresseux, qui consomment une mémoire constante.

Dans le chapitre suivant, nous aborderons les **fonctions** : définition, paramètres avancés, portée des variables, fonctions de première classe et récursion.
