Listes, Boucles & Premiers Algorithmes

Cours 2 — On arrĂȘte de faire simple, on fait des trucs utiles
🐍 Listes · Boucles · PolynĂŽmes · Vecteurs · Signaux

§1 Rappel éclair du cours 1

Dans le cours prĂ©cĂ©dent, vous avez appris les variables, les conditions, les fonctions (rĂ©cursives, et mĂȘme d'ordre supĂ©rieur). Si ce n'est pas encore frais, un petit rappel :

def mention(note):
    if note >= 10:
        return "Validé"
    else:
        return "Non"

print(mention(14))   # Validé

Aujourd'hui, on ajoute deux briques fondamentales : les listes (pour stocker plusieurs valeurs) et les boucles (pour rĂ©pĂ©ter des actions). Avec ça, vous pourrez manipuler des signaux, des polynĂŽmes, des vecteurs — bref, faire de vrais calculs.

Le cours 1, c'était le permis piéton. Le cours 2, c'est le code de la route. AprÚs, vous pourrez conduire une Ferrari algorithmique. (Ou une 2CV, selon votre niveau de caféine.)

§2 Les listes : des boßtes à plusieurs compartiments

Une liste (ou array à une dimension), c'est une séquence ordonnée d'éléments. Imaginez un tiroir à compartiments : chaque compartiment contient une valeur, et chaque compartiment a un numéro (son indice).

# Une liste se crée avec des crochets
notes = [12, 15, 8, 19, 10]

# On accÚde à un élément par son indice (qui commence à 0)
print(notes[0])   # 12 — premier Ă©lĂ©ment
print(notes[2])   # 8  — troisiĂšme Ă©lĂ©ment

# Indices négatifs : on compte depuis la fin
print(notes[-1])  # 10 — dernier Ă©lĂ©ment
print(notes[-2])  # 19 — avant-dernier

# Nombre d'éléments
print(len(notes))  # 5
12
8
10
19
5
Les indices qui commencent à 0 et pas à 1, c'est un sujet de débat ancestral. Les programmeurs comptent à partir de 0. Pourquoi ? Parce que l'indice est en fait un décalage par rapport au début de la liste. Le premier élément est à 0 décalage, le second à 1 décalage... C'est comme ça. Acceptez-le. Vous finirez par compter en base 0 dans la vie de tous les jours.

2.1 Modifier une liste

notes = [12, 15, 8, 19, 10]

notes[2] = 14           # remplacer le 8 par un 14
notes.append(16)    # ajouter 16 Ă  la fin
notes.append(7)     # ajouter 7 Ă  la fin

print(notes)
# [12, 15, 14, 19, 10, 16, 7]
[12, 15, 14, 19, 10, 16, 7]
append est la méthode la plus utilisée de Python. Elle ajoute un élément à la fin de la liste en temps quasi-constant (O(1) pour les intimes). Ne l'oubliez pas : elle sauve des vies. Ou au moins des notes de bas de page.

2.2 Tranches de liste (slicing)

Le slicing permet d'extraire une sous-liste. La syntaxe : liste[début:fin:pas].

nombres = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

print(nombres[2:6])  # [2, 3, 4, 5]     — de l'indice 2 à 5 inclus
print(nombres[:4])   # [0, 1, 2, 3]     — du dĂ©but Ă  l'indice 3
print(nombres[6:])   # [6, 7, 8, 9]     — de l'indice 6 à la fin
print(nombres[::2])  # [0, 2, 4, 6, 8]  — un Ă©lĂ©ment sur deux
print(nombres[::-1]) # [9, 8, 7, ..., 0] — la liste à l'envers !
[2, 3, 4, 5]
[0, 1, 2, 3]
[6, 7, 8, 9]
[0, 2, 4, 6, 8]
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
Le slicing est une des fonctionnalitĂ©s les plus Ă©lĂ©gantes de Python. Peu de langages offrent une syntaxe aussi expressive pour dĂ©couper des tableaux. Elle a Ă©tĂ© inspirĂ©e par le langage APL (1964), un langage tellement compact qu'il utilisait ses propres caractĂšres spĂ©ciaux — y compris des symboles grecs. Les programmeurs APL avaient besoin d'un clavier spĂ©cial. On est bien en Python.

2.3 Listes hétérogÚnes (oui, on peut)

# Une liste peut contenir n'importe quoi
fourre_tout = [42, "Python", 3.14, True, [1, 2, 3]]
print(fourre_tout[1])  # "Python"
print(fourre_tout[4][0])  # 1 — une liste dans une liste
Python
1
En Python, vous pouvez mettre des listes dans des listes. C'est des listes inception. Bwoooong. On appelle ça des listes imbriquées (ou tableaux à 2 dimensions). Mais ça, c'est pour le cours 3. Pour l'instant, restons à une dimension. On marche avant de courir.

§3 La boucle for : répéter sans se fatiguer

La boucle for permet de parcourir chaque élément d'une séquence (liste, chaßne, etc.) et d'exécuter du code pour chacun. C'est le couteau suisse du programmeur.

notes = [12, 15, 8, 19, 10]

for note in notes:
    print("Note :", note)
Note : 12
Note : 15
Note : 8
Note : 19
Note : 10
Lisez-le Ă  voix haute : « Pour chaque note dans notes, affiche la note ». C'est quasiment du français. Python, parfois, c'est presque trop facile. On se demande oĂč est l'embrouille. (Spoiler : l'embrouille arrive au §4.)

3.1 range() — gĂ©nĂ©rer des sĂ©quences numĂ©riques

range(n) génÚre les entiers de 0 à n-1. Utile quand on veut répéter une action un nombre précis de fois, ou quand on a besoin des indices.

# Afficher les nombres de 0 Ă  4
for i in range(5):
    print(i, "au carré =", i**2)
0 au carré = 0
1 au carré = 1
2 au carré = 4
3 au carré = 9
4 au carré = 16

Variantes de range :

range(3, 8)      # 3, 4, 5, 6, 7  (dĂ©but, fin — fin exclue)
range(0, 10, 2)  # 0, 2, 4, 6, 8  (début, fin, pas)
range(5, 0, -1)   # 5, 4, 3, 2, 1  (pas négatif = on descend)
range est un gĂ©nĂ©rateur paresseux (lazy) : il ne calcule pas tous les nombres d'un coup, il les produit un par un quand on les demande. C'est Ă©conome en mĂ©moire. Si vous faites range(10**12), vous ne crĂ©ez PAS une liste de mille milliards d'entiers — vous crĂ©ez un objet qui sait les produire Ă  la demande. Malin.

3.2 Parcourir avec les indices : enumerate

Parfois, on a besoin Ă  la fois de l'indice et de la valeur. enumerate fait les deux :

couleurs = ["rouge", "vert", "bleu"]

for i, couleur in enumerate(couleurs):
    print("Couleur n°", i+1, ":", couleur)
Couleur n° 1 : rouge
Couleur n° 2 : vert
Couleur n° 3 : bleu

3.3 ComprĂ©hensions de listes — la version hipster

Les compréhensions de listes sont une façon concise de créer une nouvelle liste à partir d'une autre. C'est une spécialité Python : élégant, lisible, efficace.

nombres = [1, 2, 3, 4, 5]

# Version longue (for classique)
carrés_long = []
for n in nombres:
    carrés_long.append(n ** 2)

# Version courte (compréhension)
carrés_court = [n ** 2 for n in nombres]

print(carrés_court)  # [1, 4, 9, 16, 25]

# Avec un filtre (if)
pairs = [n for n in nombres if n % 2 == 0]
print(pairs)  # [2, 4]
[1, 4, 9, 16, 25]
[2, 4]
Les compréhensions de listes sont ce qui se rapproche le plus de la poésie en programmation. Si vous réussissez à écrire une compréhension de liste complexe du premier coup sans erreur, vous avez le droit de vous lever et de faire un tour d'honneur. Les collÚgues applaudiront.
Les comprĂ©hensions de listes viennent du langage fonctionnel Haskell (1990), oĂč on les appelle « list comprehensions ». En Haskell, on peut Ă©crire : [x^2 | x <- [1..5]]. La version Python remplace les barres verticales et les flĂšches par des mots-clĂ©s en anglais — plus accessible, moins intimidant. Un bel exemple d'emprunt entre langages.

§4 La boucle while : répéter... jusqu'à ce que

Si for sert Ă  parcourir une sĂ©quence connue, while sert Ă  rĂ©pĂ©ter tant qu'une condition est vraie. On ne sait pas Ă  l'avance combien d'itĂ©rations seront nĂ©cessaires — on s'arrĂȘte quand la condition devient fausse.

for, c'est le mĂ©tro : vous savez combien de stations. while, c'est la route : vous roulez jusqu'Ă  arriver. Et si vous oubliez la condition d'arrĂȘt, vous faites le tour de la Terre indĂ©finiment. (On appelle ça une boucle infinie. Ça arrive aux meilleurs. Ctrl+C pour vous Ă©chapper.)
# Tant qu'il reste du café, on boit
tasse = 5

while tasse > 0:
    print("☕ Il reste", tasse, "tasses. On boit.")
    tasse = tasse - 1

print("Plus de café. Allons dormir.")
☕ Il reste 5 tasses. On boit.
☕ Il reste 4 tasses. On boit.
☕ Il reste 3 tasses. On boit.
☕ Il reste 2 tasses. On boit.
☕ Il reste 1 tasses. On boit.
Plus de café. Allons dormir.
PiĂšge n°1 : la condition qui ne devient jamais fausse. Si vous oubliez de modifier la variable de la boucle (tasse = tasse - 1 ci-dessus), la boucle tourne Ă  l'infini. Python ne vous arrĂȘtera pas. Votre ordinateur non plus. Les boucles infinies, c'est comme les cours de maths Ă  8h : ça n'en finit pas.

4.1 break et continue — les interrupteurs

# break : on cherche le premier nombre divisible par 7 aprĂšs 50
n = 50
while True:
    if n % 7 == 0:
        print("Trouvé :", n)
        break           # on sort
    n = n + 1
Trouvé : 56
# continue : afficher tous les nombres de 1 Ă  10 sauf les multiples de 3
for i in range(1, 11):
    if i % 3 == 0:
        continue       # on saute ce tour
    print(i, end=" ")

# Sortie : 1 2 4 5 7 8 10
1 2 4 5 7 8 10
RÚgle d'or : n'abusez pas de break et continue. Un break de temps en temps, ça va. Mais si votre code ressemble à un jeu de Tetris avec des break partout, c'est probablement que votre condition de boucle est mal conçue. Les programmeurs débutants abusent de break. Les experts structurent leurs boucles pour ne pas en avoir besoin.

§5 Applications — Manipuler des donnĂ©es rĂ©elles

Assez de théorie. Voici des exemples concrets d'utilisation des listes et des boucles dans des contextes mathématiques et scientifiques.

5.1 PolynĂŽmes : les listes comme coefficients

Un polynĂŽme P(x) = a₀ + a₁x + a₂xÂČ + ... + aₙxⁿ peut ĂȘtre reprĂ©sentĂ© par une liste de coefficients [a₀, a₁, a₂, ..., aₙ]. L'indice dans la liste correspond au degrĂ© du terme.

Les polynÎmes sont étudiés depuis plus de 4000 ans. Les Babyloniens savaient déjà résoudre des équations du second degré (vers 2000 av. J.-C.). Sans notation algébrique, ils les décrivaient en prose : « Ajoute le carré de la moitié du coefficient au terme constant... ». Aujourd'hui, ça tient en une ligne de Python. Le progrÚs.

Évaluation d'un polynîme

def evalue(coeffs, x):
    """Évalue le polynĂŽme reprĂ©sentĂ© par coeffs au point x."""
    resultat = 0
    for i, a in enumerate(coeffs):
        resultat = resultat + a * (x ** i)
    return resultat

# P(x) = 1 + 2x + 3xÂČ   (coefficients [1, 2, 3])
p = [1, 2, 3]

print(evalue(p, 0))   # 1
print(evalue(p, 2))   # 1 + 4 + 12 = 17
print(evalue(p, 10))  # 1 + 20 + 300 = 321
1
17
321

Variation : méthode de Horner (plus maligne)

Au lieu de calculer xi Ă  chaque itĂ©ration (ce qui est coĂ»teux), on peut utiliser la mĂ©thode de Horner : on factorise le polynĂŽme pour n'utiliser que des multiplications et des additions. Pour P(x) = 1 + 2x + 3xÂČ, on Ă©crit P(x) = 1 + x(2 + 3x).

def evalue_horner(coeffs, x):
    resultat = 0
    for a in reversed(coeffs):
        resultat = resultat * x + a
    return resultat

print(evalue_horner(p, 2))  # 17
17

Pourquoi c'est mieux ? Moins d'opérations, moins d'erreurs d'arrondi. La méthode de Horner date de 1819 mais était déjà connue des mathématiciens chinois au XIIIe siÚcle (Qin Jiushao). Rien de nouveau sous le soleil algorithmique.

Dérivée d'un polynÎme

La dĂ©rivĂ©e de P(x) = a₀ + a₁x + a₂xÂČ + a₃xÂł est P'(x) = a₁ + 2a₂x + 3a₃xÂČ. En termes de listes : on dĂ©cale et on multiplie.

def derive_polynome(coeffs):
    """Retourne la liste des coefficients du polynÎme dérivé."""
    return [i * a for i, a in enumerate(coeffs) if i > 0]

# P(x) = 1 + 2x + 3xÂČ + 4xÂł
p = [1, 2, 3, 4]
p_prime = derive_polynome(p)

print(p_prime)  # [2, 6, 12] → P'(x) = 2 + 6x + 12xÂČ

# Vérifions : P'(1) = 2 + 6 + 12 = 20
print(evalue(p_prime, 1))  # 20
[2, 6, 12]
20
Vous venez de faire de l'analyse mathématique sans papier ni crayon. Votre prof de maths du lycée est soit fier, soit terrifié. Les deux sont acceptables.

Variation : intégration (pour les courageux)

Et si on calculait l'intĂ©grale ? L'intĂ©grale de P(x) est ∫P(x) dx = C + a₀x + (a₁/2)xÂČ + (a₂/3)xÂł + ... En code : on insĂšre un coefficient 0 au dĂ©but (la constante) et on divise chaque terme par son nouveau degrĂ©.

def integre_polynome(coeffs, constante=0):
    return [constante] + [a / (i + 1) for i, a in enumerate(coeffs)]

# P(x) = 2 + 6x + 12xÂČ â†’ ∫P = 0 + 2x + 3xÂČ + 4xÂł
print(integre_polynome([2, 6, 12]))  # [0, 2.0, 3.0, 4.0]
[0, 2.0, 3.0, 4.0]

Dériver puis intégrer (ou l'inverse) vous redonne le polynÎme d'origine. Vérifiez !

5.2 Vecteurs : des listes pour les opérations linéaires

En IA et en physique, un vecteur est une liste de nombres (ses composantes). Python permet de faire des calculs vectoriels avec des listes et des boucles.

Somme de deux vecteurs

def somme_vecteurs(u, v):
    """Additionne deux vecteurs composante par composante."""
    return [u[i] + v[i] for i in range(len(u))]

u = [1, 2, 3]
v = [4, 5, 6]

print(somme_vecteurs(u, v))  # [5, 7, 9]
[5, 7, 9]
PiÚge n°2 : u + v ne fait PAS l'addition composante par composante en Python ! Il concatÚne les listes : [1, 2, 3, 4, 5, 6]. C'est une source d'erreur classique. N'écrivez jamais u + v pour additionner des vecteurs. Utilisez votre fonction ou, mieux, NumPy (bibliothÚque qu'on verra plus tard).

Produit scalaire

def produit_scalaire(u, v):
    """Calcule le produit scalaire de u et v : Σ u[i] × v[i]"""
    total = 0
    for i in range(len(u)):
        total = total + u[i] * v[i]
    return total

u = [1, 2, 3]
v = [4, 5, 6]

print(produit_scalaire(u, v))  # 1×4 + 2×5 + 3×6 = 4 + 10 + 18 = 32
32
Le produit scalaire est partout en IA : il mesure la similarité entre deux vecteurs. Dans les réseaux de neurones, chaque neurone calcule un produit scalaire entre ses entrées et ses poids. C'est littéralement l'opération la plus importante du machine learning. Sans produit scalaire, pas d'IA. Merci Pythagore (oui, c'est lié).

Norme d'un vecteur

import math

def norme(v):
    """Retourne la norme euclidienne ‖v‖ = √(ÎŁ v[i]ÂČ)"""
    somme = 0
    for composante in v:
        somme = somme + composante ** 2
    return math.sqrt(somme)

v = [3, 4]
print(norme(v))  # 5.0  (triangle 3-4-5, vous vous souvenez ?)
5.0
3-4-5, le triangle rectangle préféré des professeurs de maths. Pythagore serait fier. Mais il serait surtout choqué qu'on calcule ça en 3 lignes de Python alors qu'il a dû le faire sur du papyrus.

5.3 Signaux et courbes : normalisation, lissage

Un signal (audio, bourse, tempĂ©rature) peut ĂȘtre reprĂ©sentĂ© par une liste de valeurs Ă©chantillonnĂ©es dans le temps. On applique des transformations dessus.

Normalisation (mettre entre 0 et 1)

def normalise(signal):
    """Met le signal à l'échelle [0, 1]."""
    min_val = min(signal)
    max_val = max(signal)
    if max_val == min_val:
        return [0] * len(signal)  # signal constant → tout à 0
    return [(v - min_val) / (max_val - min_val) for v in signal]

signal = [2, 5, 3, 8, 1]
print(normalise(signal))
# [(2-1)/(7), (5-1)/7, (3-1)/7, (8-1)/7, (1-1)/7]
# ≈ [0.143, 0.571, 0.286, 1.0, 0.0]
[0.14285714285714285, 0.5714285714285714, 0.2857142857142857, 1.0, 0.0]
La normalisation est une Ă©tape obligatoire en IA avant de donner des donnĂ©es Ă  un algorithme. Pourquoi ? Parce que si une feature (ex: Ăąge, 0–100) a des valeurs beaucoup plus grandes qu'une autre (ex: salaire en k€, 20–200), l'algorithme va considĂ©rer que la premiĂšre est plus importante, juste Ă  cause de l'Ă©chelle. La normalisation Ă©galise les chances. Équitable. đŸ€

Moyenne mobile (lissage)

def moyenne_mobile(signal, fenetre):
    """Lisse un signal par moyenne mobile sur 'fenetre' points."""
    resultat = []
    for i in range(len(signal) - fenetre + 1):
        morceau = signal[i:i + fenetre]
        moyenne = sum(morceau) / fenetre
        resultat.append(moyenne)
    return resultat

signal = [1, 3, 2, 8, 5, 7, 6, 4]
print(moyenne_mobile(signal, 3))
# [2.0, 4.33, 5.0, 6.67, 6.0, 5.67]
[2.0, 4.333333333333333, 5.0, 6.666666666666667, 6.0, 5.666666666666667]
La moyenne mobile est l'équivalent informatique du lissage à main levée sur un graphique. Les traders l'utilisent pour repérer des tendances boursiÚres. Les météorologues pour lisser les températures. Les photographes pour réduire le bruit. Et vous, pour impressionner vos amis en transformant des listes de nombres en courbes.

Variation : centrer-réduire (standardisation)

La normalisation [0, 1] n'est pas la seule méthode. On peut aussi centrer-réduire (soustraction de la moyenne, division par l'écart-type). C'est ce qu'on utilise en statistiques et en IA quand on veut que les données aient une moyenne nulle et une variance de 1.

def centre_reduit(signal):
    mu = sum(signal) / len(signal)
    variance = sum((x - mu) ** 2 for x in signal) / len(signal)
    ecart_type = math.sqrt(variance)
    if ecart_type == 0:
        return [0] * len(signal)
    return [(x - mu) / ecart_type for x in signal]

signal = [2, 4, 6, 8]
print(centre_reduit(signal))
# moyenne=5, variance=5, Ă©cart-type≈2.236
# [-1.34, -0.45, 0.45, 1.34]
[-1.3416407864998738, -0.4472135954999579, 0.4472135954999579, 1.3416407864998738]

5.4 Calcul de frĂ©quences — initialiser un tableau Ă  partir d'un autre

Une opération courante : compter combien de fois chaque valeur apparaßt dans une liste. Le résultat est un tableau de fréquences (ou histogramme). On crée une nouvelle liste, initialisée à zéro, qu'on remplit au fur et à mesure.

def frequences(notes, note_max=20):
    """Calcule le nombre d'étudiants ayant obtenu chaque note de 0 à note_max."""
    # On crée un tableau de (note_max+1) zéros
    histo = [0] * (note_max + 1)

    for note in notes:
        histo[note] = histo[note] + 1

    return histo

notes = [12, 15, 8, 12, 15, 19, 12, 10, 15, 8]
histo = frequences(notes)

# histo[8] = 2, histo[10] = 1, histo[12] = 3, histo[15] = 3, histo[19] = 1
for note, effectif in enumerate(histo):
    if effectif > 0:
        print("Note", note, ":", "■" * effectif, "(" + str(effectif) + ")")
Note 8 : ■■ (2)
Note 10 : ■ (1)
Note 12 : ■■■ (3)
Note 15 : ■■■ (3)
Note 19 : ■ (1)
On vient de faire un histogramme avec des bĂątons en ASCII. C'est moche, c'est pixelisĂ©, mais ça marche. En IA, on fait ça avec matplotlib et c'est plus joli. Mais le principe est exactement le mĂȘme : compter les occurrences. Vous venez de rĂ©inventer la table de frĂ©quences. Les statisticiens pleurent de joie.

Variation : normaliser en pourcentages

def frequences_relatives(notes, note_max=20):
    histo = frequences(notes, note_max)
    total = len(notes)
    return [effectif / total for effectif in histo]

rel = frequences_relatives(notes)
print(rel[12])  # 0.3 (30% des étudiants ont eu 12)
0.3

Variation : détection de valeurs aberrantes (outliers)

On peut utiliser les fréquences pour repérer des notes impossibles (ex: 25/20) :

def filtrer_notes(notes, note_max=20):
    return [note for note in notes if 0 <= note <= note_max]

notes_sales = [12, 15, -3, 8, 25, 10]
print(filtrer_notes(notes_sales))  # [12, 15, 8, 10] — les deux intrus ont Ă©tĂ© virĂ©s
[12, 15, 8, 10]

Propre, non ? Les compréhensions de listes avec if sont parfaites pour le filtrage.


§6 Pour aller plus loin

6.1 Listes vs tuples : la guerre des parenthĂšses

Python a aussi les tuples (1, 2, 3) : comme les listes mais immuables (on ne peut pas les modifier). On les utilise pour des données qui ne doivent pas changer (coordonnées GPS, constantes). Les listes sont pour les données qu'on manipule.

6.2 La boucle for avec else ?

Saviez-vous qu'une boucle for peut avoir une clause else ? Elle s'exécute uniquement si la boucle n'a pas été interrompue par break. C'est utile pour les recherches : si on trouve l'élément, break; sinon, on exécute le else. Contre-intuitif mais pratique.

6.3 Listes et mémoire : attention aux copies

Quand on Ă©crit b = a (oĂč a est une liste), b n'est pas une copie : c'est une nouvelle rĂ©fĂ©rence vers la mĂȘme liste. Modifier b modifie aussi a. Pour copier : b = a[:] ou b = a.copy(). Ne l'oubliez pas, ou vous passerez 3 heures Ă  chercher un bug. (Je parle d'expĂ©rience.)

a = [1, 2, 3]
b = a          # b pointe vers la mĂȘme liste que a
b[0] = 999
print(a[0])    # 999  — surprise ! a aussi changĂ©

c = a[:]       # vraie copie
c[0] = 42
print(a[0])    # 999  — a n'a pas changĂ©, ouf
999
999

§7 Le mot de la fin

Vous avez survolé ce qui fait la puissance des langages de programmation moderne : les listes pour structurer les données, les boucles pour les traiter, et tout un tas d'applications (polynÎmes, vecteurs, signaux). Félicitations.

John von Neumann (1903–1957) — mathĂ©maticien hongrois-amĂ©ricain, l'un des pĂšres de l'architecture des ordinateurs modernes (l'architecture dite « de von Neumann »). Il a compris que les donnĂ©es et les instructions devaient ĂȘtre stockĂ©es dans la mĂȘme mĂ©moire, et qu'on pouvait les parcourir sĂ©quentiellement. Sans lui, pas de boucles for. Pas de listes. Pas de programme stockĂ© en mĂ©moire. Chaque fois que vous Ă©crivez for x in liste, vous marchez sur les pas d'un gĂ©nie.
Bilan du cours 2 : Il ne vous reste plus qu'à tout oublier et à utiliser NumPy. Mais ça, c'est le cours 3. En attendant, amusez-vous avec les listes.
© 2026 — laurent.thiry@uha.fr