Vue d'ensemble
Une image, un plateau de jeu, un tableur, une grille de Sudoku : dès qu'une information s'organise en lignes et colonnes, on a besoin d'un tableau à deux dimensions. En Python, il n'existe pas de type « matrice » : on utilise une liste de listes. C'est simple… mais une seule ligne de code mal écrite crée le bug le plus classique de toute la première année. Ce chapitre te donne les bons réflexes et démonte ce piège une bonne fois pour toutes.
Prérequis
- Listes Python : indexation
L[i],len(L), opérateur de répétition[0] * n. - Boucles
for i in range(n)et compréhensions de liste[expr for _ in range(n)]. - Notion de complexité temporelle en .
Bloqué sur un bug de matrice qui « se remplit toute seule » ? C'est le piège n°1 de l'info en prépa, et il se règle en cinq minutes avec la bonne image mentale. Nos mentors alumni X · Centrale · Mines t'expliquent le fond, pas juste la recette.
Trouver un mentor →Un tableau 2D, c'est une liste de listes
Un tableau à deux dimensions (ou matrice) est une liste dont chaque élément est lui-même une liste, toutes de même longueur. Si M est un tel tableau, M[i] est la ligne (une liste), et M[i][j] est l'élément situé à la ligne , colonne . Les indices commencent à .
M = [[1, 2, 3],
[4, 5, 6]]
print(M[0]) # la ligne 0 : [1, 2, 3]
print(M[1][2]) # ligne 1, colonne 2 : 6
print(len(M)) # nombre de lignes : 2
print(len(M[0])) # nombre de colonnes : 3M = [[1, 2, 3], [4, 5, 6]]On construit une liste de deux listes. M a 2 lignes et 3 colonnes. Visualise une grille : la première ligne contient 1, 2, 3 ; la seconde 4, 5, 6.M[0]Le premier crochet choisit une ligne entière : ici la liste [1, 2, 3]. Un seul indice = une ligne, pas un nombre.M[1][2]Deux crochets : d'abord la ligne 1 ([4, 5, 6]), puis dans cette ligne la colonne 2, soit 6. Ordre à retenir : [ligne][colonne].len(M)Longueur de la liste extérieure = nombre de lignes (2).len(M[0])Longueur de la première ligne = nombre de colonnes (3). On suppose toutes les lignes de même longueur, donc M[0] suffit.len(M) = lignes, len(M[0]) = colonnes. Beaucoup d'étudiants inversent : la liste extérieure empile les lignes, chaque liste intérieure est une ligne, donc sa longueur donne les colonnes.Créer une matrice de zéros — et le piège de l'aliasing
On a très souvent besoin d'une matrice remplie de qu'on remplira ensuite. La bonne façon utilise une compréhension de liste :
nb_lignes = 2
nb_colonnes = 3
M = [[0] * nb_colonnes for _ in range(nb_lignes)]
M[0][0] = 1
print(M) # [[1, 0, 0], [0, 0, 0]][0] * nb_colonnesCrée une ligne de nb_colonnes zéros : [0, 0, 0]. La répétition * sur des entiers ne pose aucun problème (un entier n'est jamais partagé de façon dangereuse).[ ... for _ in range(nb_lignes)]La compréhension réexécute [0] * nb_colonnes à chaque tour de boucle. On obtient donc nb_lignes listes fraîches et indépendantes. Le _ signifie « je n'utilise pas la variable de boucle ».M[0][0] = 1On modifie une seule case (ligne 0, colonne 0).print(M)Seule la première ligne change : [[1, 0, 0], [0, 0, 0]]. Chaque ligne est un objet distinct — c'est exactement ce qu'on veut.[[0] * c] * l. Il est tentant d'écrire M = [[0] * 3] * 2. Le code ne plante pas, mais toutes les lignes deviennent la même liste : modifier une case en modifie une par ligne. C'est le bug n°1 de l'année. On le dissèque ci-dessous.M = [[0] * 3] * 2 # PIEGE : ne fais jamais ca
M[0][0] = 1
print(M) # [[1, 0, 0], [1, 0, 0]] <-- les DEUX lignes ont change !
print(M[0] is M[1]) # True : c'est le MEME objet liste[0] * 3Crée une ligne [0, 0, 0]. Jusqu'ici tout va bien : il n'existe qu'un seul objet liste en mémoire.[ ... ] * 2La répétition * 2 ne copie pas la ligne : elle met deux fois la même référence dans la liste extérieure. M[0] et M[1] pointent vers le même objet — c'est ce qu'on appelle l'aliasing (deux noms, un seul objet).M[0][0] = 1On modifie « la ligne 0 »… mais comme M[0] et M[1] sont le même objet, M[1][0] vaut désormais 1 aussi.print(M)Sortie [[1, 0, 0], [1, 0, 0]] : la modification s'est propagée à toutes les lignes. Bug silencieux et déroutant.M[0] is M[1]L'opérateur is teste l'identité (même objet en mémoire). Ici True : preuve que les deux lignes ne sont qu'un seul objet partagé.| Étape | Ce qui se passe en mémoire | M[0] | M[1] | Même objet ? |
|---|---|---|---|---|
[0]*3 | un objet liste L = [0, 0, 0] | — | — | — |
[L]*2 | M contient deux références vers ce même L | [0, 0, 0] | [0, 0, 0] | True |
M[0][0]=1 | on écrit dans L (partagé) | [1, 0, 0] | [1, 0, 0] ✓ modifié aussi | True |
- Décide du nombre de lignes
let de colonnesc. - Écris
[[valeur] * c for _ in range(l)]: la compréhension garantit une ligne neuve par itération. - Ne jamais utiliser
[[valeur] * c] * l: la répétition externe partage la même ligne (aliasing). - En cas de doute, teste
M[0] is M[1]: ça doit renvoyerFalse.
« Pourquoi les listes s'aliasent mais pas les entiers ? » Cette question sur la mémoire revient à tous les concours (Python et OCaml). Nos mentors alumni X · Centrale · Mines te donnent le modèle mental qui rend tout limpide.
Trouver un mentor →Parcourir un tableau 2D : la double boucle
Pour visiter toutes les cases, on imbrique deux boucles : une sur les lignes i, une sur les colonnes j. Le squelette universel est :
for i in range(len(M)): # i parcourt les lignes
for j in range(len(M[0])): # j parcourt les colonnes
# ici on traite la case M[i][j]
...for i in range(len(M))Boucle externe : i prend successivement l'indice de chaque ligne, de 0 à len(M) - 1.for j in range(len(M[0]))Boucle interne, imbriquée dans la première : pour chaque ligne i fixée, j balaie tous les indices de colonne, de 0 à len(M[0]) - 1. C'est cette imbrication qui garantit qu'on passe par toutes les cases.# on traite la case M[i][j]Au cœur des deux boucles, le couple (i, j) désigne une case précise : c'est ici qu'on lit ou écrit M[i][j] (somme, maximum, transformation d'un pixel…). Ce corps s'exécute exactement fois.Somme de tous les éléments
def somme(M):
s = 0
for i in range(len(M)):
for j in range(len(M[0])):
s += M[i][j]
return s
print(somme([[1, 2, 3], [4, 5, 6]])) # 21s = 0Accumulateur : on part de 0 et on ajoutera chaque case.for i in range(len(M))Boucle externe : i prend les valeurs 0, 1, … pour chaque ligne.for j in range(len(M[0]))Boucle interne : pour la ligne i fixée, j balaie toutes les colonnes.s += M[i][j]On ajoute la case courante au total. Cette ligne s'exécute une fois par case, donc fois.return sAprès avoir tout visité, on renvoie la somme : pour l'exemple, .| i | j | M[i][j] | s après s += M[i][j] |
|---|---|---|---|
| 0 | 0 | 1 | 1 |
| 0 | 1 | 2 | 3 |
| 0 | 2 | 3 | 6 |
| 1 | 0 | 4 | 10 |
| 1 | 1 | 5 | 15 |
| 1 | 2 | 6 | 21 ✓ |
Recherche du maximum
def maximum(M):
m = M[0][0]
for i in range(len(M)):
for j in range(len(M[0])):
if M[i][j] > m:
m = M[i][j]
return m
print(maximum([[3, 8, 1], [4, 2, 9]])) # 9m = M[0][0]On initialise le maximum avec une case qui existe vraiment (la première). Ne jamais partir de 0 : une matrice peut ne contenir que des négatifs.for i ... for j ...Double boucle : on visite chaque case.if M[i][j] > m: m = M[i][j]Si la case courante dépasse le maximum provisoire, elle devient le nouveau maximum.return mÀ la fin, m contient la plus grande valeur : ici 9.Transposition (échanger lignes et colonnes)
def transpose(M):
l = len(M)
c = len(M[0])
T = [[0] * l for _ in range(c)] # T a c lignes et l colonnes
for i in range(l):
for j in range(c):
T[j][i] = M[i][j]
return T
print(transpose([[1, 2, 3], [4, 5, 6]])) # [[1, 4], [2, 5], [3, 6]]l = len(M) ; c = len(M[0])On mémorise les dimensions de M : l lignes, c colonnes.T = [[0] * l for _ in range(c)]La transposée a les dimensions inversées : c lignes et l colonnes. On la crée proprement (compréhension, pas d'aliasing).T[j][i] = M[i][j]Cœur de la transposition : la case (ligne i, colonne j) de M va en (ligne j, colonne i) de T. Les indices sont échangés.return TRésultat : [[1, 4], [2, 5], [3, 6]]. Ce qui était en ligne devient colonne.Parcourir intégralement un tableau à lignes et colonnes par une double boucle exécute le corps fois. Si chaque traitement de case est en , le coût total est , c'est-à-dire proportionnel au nombre de cases. Pour une image de pixels, on parle de .
Application : les images en niveaux de gris
Une image en niveaux de gris est un tableau 2D d'entiers compris entre et . Chaque case est un pixel : img[i][j] est l'intensité lumineuse à la ligne , colonne . Convention : noir, blanc, les valeurs intermédiaires sont des gris. La ligne numérote les rangées de pixels du haut vers le bas.
Le négatif
Le négatif inverse les intensités : ce qui est clair devient sombre et inversement. Chaque pixel devient (le noir 0 devient blanc 255, le blanc 255 devient noir 0).
def negatif(img):
l = len(img)
c = len(img[0])
res = [[0] * c for _ in range(l)]
for i in range(l):
for j in range(c):
res[i][j] = 255 - img[i][j]
return res
print(negatif([[0, 100, 255], [50, 200, 128]]))
# [[255, 155, 0], [205, 55, 127]]l = len(img) ; c = len(img[0])Dimensions de l'image : hauteur (lignes) et largeur (colonnes) en pixels.res = [[0] * c for _ in range(l)]On crée une nouvelle image vide de même taille (compréhension : pas d'aliasing). On ne modifie pas l'originale.res[i][j] = 255 - img[i][j]Formule du négatif appliquée à chaque pixel. Comme img[i][j] est entre 0 et 255, 255 - img[i][j] reste entre 0 et 255 : le résultat est une image valide.return resOn renvoie l'image inversée. Vérifie : , , .| i | j | img[i][j] | 255 − p |
|---|---|---|---|
| 0 | 0 | 0 | 255 |
| 0 | 1 | 100 | 155 |
| 0 | 2 | 255 | 0 |
| 1 | 0 | 50 | 205 |
| 1 | 1 | 200 | 55 |
| 1 | 2 | 128 | 127 ✓ |
Le seuillage (binarisation)
Le seuillage transforme l'image en noir et blanc pur : on fixe un seuil ; tout pixel supérieur ou égal au seuil devient blanc (255), les autres deviennent noirs (0). Utile pour isoler une forme sur un fond.
def seuillage(img, seuil):
l = len(img)
c = len(img[0])
res = [[0] * c for _ in range(l)]
for i in range(l):
for j in range(c):
if img[i][j] >= seuil:
res[i][j] = 255
else:
res[i][j] = 0
return res
print(seuillage([[30, 120, 200], [90, 128, 255]], 128))
# [[0, 0, 255], [0, 255, 255]]def seuillage(img, seuil)La fonction prend l'image et le seuil de décision (un entier entre 0 et 255).res = [[0] * c for _ in range(l)]Nouvelle image de même taille, initialisée à 0.if img[i][j] >= seuil: res[i][j] = 255Pixel assez clair (≥ seuil) → blanc. Le >= inclut la valeur du seuil elle-même : 128 avec seuil 128 devient blanc.else: res[i][j] = 0Pixel trop sombre (< seuil) → noir. Après seuillage, l'image ne contient plus que 0 et 255.return resRésultat binarisé. Avec seuil 128 : 30→0, 120→0, 200→255, 90→0, 128→255, 255→255.| i | j | pixel | pixel ≥ 128 ? | res[i][j] |
|---|---|---|---|---|
| 0 | 0 | 30 | non | 0 |
| 0 | 1 | 120 | non | 0 |
| 0 | 2 | 200 | oui | 255 |
| 1 | 0 | 90 | non | 0 |
| 1 | 1 | 128 | oui (égalité) | 255 |
| 1 | 2 | 255 | oui | 255 ✓ |
res séparée par sécurité.Exercices corrigés
Écris une fonction compte_zeros(M) qui renvoie le nombre de cases égales à 0 dans le tableau 2D M. Teste-la sur [[0, 3, 0], [1, 0, 5]] (réponse attendue : 3).
Voir la correction détaillée
On parcourt toutes les cases avec la double boucle et on incrémente un compteur quand la case vaut 0.
def compte_zeros(M):
n = 0
for i in range(len(M)):
for j in range(len(M[0])):
if M[i][j] == 0:
n += 1
return n
print(compte_zeros([[0, 3, 0], [1, 0, 5]])) # 3Cases visitées : 0 (compté), 3, 0 (compté), 1, 0 (compté), 5. Trois zéros → n = 3. Coût .
Écris eclaircir(img, k) qui ajoute k à chaque pixel, mais plafonne à 255 (un pixel ne peut pas dépasser 255). Renvoie une nouvelle image. Teste sur [[250, 100], [0, 200]] avec k = 30 (attendu : [[255, 130], [30, 230]]).
Voir la correction détaillée
Créer une image résultat propre (compréhension). Pour chaque pixel, calculer p + k ; si ça dépasse 255, mettre 255. La fonction min exprime exactement ce plafond.
def eclaircir(img, k):
l = len(img)
c = len(img[0])
res = [[0] * c for _ in range(l)]
for i in range(l):
for j in range(c):
res[i][j] = min(img[i][j] + k, 255)
return res
print(eclaircir([[250, 100], [0, 200]], 30))
# [[255, 130], [30, 230]]250 + 30 = 280 > 255 → plafonné à 255. 100 + 30 = 130, 0 + 30 = 30, 200 + 30 = 230 : tous sous 255, inchangés. Le piège serait d'oublier min(..., 255) et de produire des pixels invalides supérieurs à 255.
Écris sommes_lignes(M) qui renvoie une liste contenant la somme de chaque ligne de M. Pour [[1, 2, 3], [4, 5, 6]], le résultat attendu est [6, 15]. Réfléchis bien à la construction de la liste résultat.
Voir la correction détaillée
La sortie est un tableau 1D de longueur « nombre de lignes ». On l'initialise à 0, puis pour chaque ligne on accumule ses colonnes dans res[i].
def sommes_lignes(M):
res = [0] * len(M) # une case par ligne (entiers : pas d'aliasing)
for i in range(len(M)):
for j in range(len(M[0])):
res[i] += M[i][j]
return res
print(sommes_lignes([[1, 2, 3], [4, 5, 6]])) # [6, 15]Ligne 0 : 1+2+3 = 6 ; ligne 1 : 4+5+6 = 15 → [6, 15]. Ici [0] * len(M) est correct car ce sont des entiers (immuables) : le piège d'aliasing ne concerne que la répétition de listes. Coût .
Récap final — Ce qu'il faut absolument retenir
Un tableau 2D est une liste de listes ; l'essentiel tient dans la manière de le créer (sans aliasing) et de le parcourir (double boucle). Vérifie que tu sais répondre à tout ceci.
- Sais-tu dire ce que valent
len(M)etlen(M[0])— lignes ou colonnes ? - Sais-tu accéder à l'élément ligne , colonne avec le bon ordre des crochets ?
- Sais-tu créer une matrice de zéros avec
[[0] * c for _ in range(l)]? - Sais-tu expliquer pourquoi
[[0] * c] * lcrée des lignes aliasées et ce queisrévèle ? - Sais-tu écrire la double boucle qui visite chaque case exactement une fois ?
- Sais-tu calculer la somme et le maximum d'un tableau 2D (en initialisant le max sur une vraie case) ?
- Sais-tu qu'une image en niveaux de gris est une matrice d'entiers 0..255 (0 noir, 255 blanc) ?
- Sais-tu coder le négatif () et le seuillage (≥ seuil → 255 sinon 0) ?
- Sais-tu que parcourir une image coûte ?