Tableaux 2D, Textes & CSV

Cours 3 — On monte d'un cran : matrices, cryptage, fichiers
🧩 Listes 2D · Chaînes · Cryptographie · CSV pur Python

§1 Rappel des acquis

Cours 1 : variables, if, fonctions, récursivité, ordre supérieur.
Cours 2 : listes 1D, for, while, compréhensions, polynômes, vecteurs, signaux.

Aujourd'hui on ajoute trois super-pouvoirs : les tableaux à deux dimensions (matrices, images), la manipulation de texte (découpage, fusion, cryptage), et la lecture de fichiers CSV avec les outils du pauvre (c'est-à-dire sans importer csv ni pandas). Pourquoi ? Parce que comprendre ce qui se cache sous le capot, c'est ça qui fait de vous un vrai ingénieur.

Les vrais ingénieurs n'utilisent pas de bibliothèques. Ils réinventent la roue. En mieux. Et ils mettent des commentaires. (Non, ne faites pas ça en entreprise. Mais au moins une fois dans votre vie, oui.)

§2 Tableaux 2D : des listes dans des listes

Un tableau 2D, c'est une liste dont chaque élément est lui-même une liste (de même taille). Imaginez un tableau Excel, une grille de pixels, ou un échiquier.

# Création manuelle d'une matrice 3×3
matrice = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

# Accès : matrice[ligne][colonne]
print(matrice[0][0])  # 1 — première ligne, première colonne
print(matrice[1][2])  # 6 — deuxième ligne, troisième colonne
print(matrice[2])     # [7, 8, 9] — toute la troisième ligne
1
6
[7, 8, 9]

2.1 Créer une matrice « propre »

On ne peut pas écrire [[0] * 3] * 3 pour créer une grille 3×3 de zéros. Pourquoi ? Parce que les trois lignes seraient en fait la même liste en mémoire. Un bug mémorable.

# BONNE méthode — compréhension de liste
lignes, colonnes = 3, 4
grille = [[0 for _ in range(colonnes)] for _ in range(lignes)]

print(grille)
# [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]

grille[0][0] = 42  # ne modifie que la première ligne
print(grille)
# [[42, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]] ✓
[[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
[[42, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
grille = [[0] * 4] * 3 crée UNE seule ligne [0,0,0,0] et la répète 3 fois (même référence). Modifier grille[0][0] modifierait toutes les lignes. C'est le piège numéro 1 des tableaux 2D en Python. Souvenez-vous-en.

2.2 Parcourir une matrice

def affiche_matrice(m):
    """Affiche une matrice ligne par ligne."""
    for ligne in m:
        print(ligne)

def parcours_indices(m):
    """Parcourt avec indices (utile pour modifier)."""
    for i in range(len(m)):
        for j in range(len(m[i])):
            print("m[", i, "][", j, "] =", m[i][j])

affiche_matrice(matrice)
# [1, 2, 3]
# [4, 5, 6]
# [7, 8, 9]
[1, 2, 3]
[4, 5, 6]
[7, 8, 9]

2.3 Opérations sur les matrices

Addition de deux matrices

def addition_matrice(A, B):
    """Additionne deux matrices de mêmes dimensions."""
    lignes = len(A)
    colonnes = len(A[0])
    return [[A[i][j] + B[i][j] for j in range(colonnes)] for i in range(lignes)]

A = [[1, 2], [3, 4]]
B = [[5, 6], [7, 8]]

affiche_matrice(addition_matrice(A, B))
# [6, 8]
# [10, 12]
[6, 8]
[10, 12]

Transposition

def transpose(m):
    """Transpose une matrice : lignes ↔ colonnes."""
    lignes = len(m)
    colonnes = len(m[0])
    # On crée une matrice colonnes×lignes
    return [[m[i][j] for i in range(lignes)] for j in range(colonnes)]

M = [[1, 2, 3],
     [4, 5, 6]]

affiche_matrice(transpose(M))
# [1, 4]
# [2, 5]
# [3, 6]
[1, 4]
[2, 5]
[3, 6]
La transposition est une opération fondamentale en algèbre linéaire. En IA, on transpose tout le temps : les images (pour changer l'orientation), les matrices de poids des réseaux de neurones, les tables de données. Sans transposition, pas de deep learning.

Produit matriciel

def produit_matrice(A, B):
    """Multiplie deux matrices A (n×p) et B (p×m)."""
    n, p = len(A), len(A[0])
    p2, m = len(B), len(B[0])
    if p != p2:
        raise ValueError("Dimensions incompatibles pour le produit")
    
    # Résultat : n × m
    C = [[0 for _ in range(m)] for _ in range(n)]
    for i in range(n):
        for j in range(m):
            for k in range(p):
                C[i][j] = C[i][j] + A[i][k] * B[k][j]
    return C

A = [[1, 2], [3, 4]]  # 2×2
B = [[2, 0], [1, 2]]  # 2×2

affiche_matrice(produit_matrice(A, B))
# [4, 4]   → 1×2 + 2×1 = 4,  1×0 + 2×2 = 4
# [10, 8]  → 3×2 + 4×1 = 10, 3×0 + 4×2 = 8
[4, 4]
[10, 8]
Trois boucles for imbriquées. On appelle ça un produit matriciel naïf — O(n³). C'est lent, c'est moche, mais ça marche. Les bibliothèques comme NumPy utilisent des algorithmes bien plus performants (Strassen, multiplication par blocs, blas). Mais vous, vous avez le mérite d'avoir écrit les trois boucles vous-même. C'est comme cuisiner un œuf à la coque : tout le monde sait le faire, mais personne ne sait comment fonctionne vraiment une poule.

Variation : matrice identité

def identite(n):
    """Génère la matrice identité n×n."""
    return [[1 if i == j else 0 for j in range(n)] for i in range(n)]

affiche_matrice(identite(4))
# [1, 0, 0, 0]
# [0, 1, 0, 0]
# [0, 0, 1, 0]
# [0, 0, 0, 1]
[1, 0, 0, 0]
[0, 1, 0, 0]
[0, 0, 1, 0]
[0, 0, 0, 1]

La matrice identité, c'est le « 1 » des matrices. Multiplier une matrice par l'identité, c'est comme multiplier un nombre par 1 : ça ne change rien. C'est rassurant. Dans un monde chaotique, l'identité est une constante.

2.4 Images en niveaux de gris

Une image en niveaux de gris (256 nuances), c'est une matrice de nombres : chaque case est un pixel dont la valeur (0–255) représente l'intensité lumineuse. 0 = noir, 255 = blanc.

from random import randint

def image_noir(hauteur, largeur):
    """Image entièrement noire (0 partout)."""
    return [[0 for _ in range(largeur)] for _ in range(hauteur)]

def image_bruit(hauteur, largeur):
    """Image aléatoire (bruit, comme une vieille télé sans antenne)."""
    return [[randint(0, 255) for _ in range(largeur)] for _ in range(hauteur)]

def negatif(image):
    """Inverse les couleurs : 0→255, 255→0."""
    return [[255 - pixel for pixel in ligne] for ligne in image]

def affiche_ascii(image):
    """Affiche une image en ASCII art (approximatif)."""
    for ligne in image:
        ligne_ascii = ""
        for pixel in ligne:
            if pixel > 200:   ligne_ascii = ligne_ascii + "█"
            elif pixel > 100:  ligne_ascii = ligne_ascii + "▓"
            elif pixel > 50:   ligne_ascii = ligne_ascii + "▒"
            else:            ligne_ascii = ligne_ascii + "░"
        print(ligne_ascii)

# Testons avec un petit dégradé
image = [[i * 2 for i in range(20)] for _ in range(8)]
affiche_ascii(image)
░░░░░░░░░░▓▓▓▓▓▓▓▓██████
░░░░░░░░░░▓▓▓▓▓▓▓▓██████
░░░░░░░░░░▓▓▓▓▓▓▓▓██████
░░░░░░░░░░▓▓▓▓▓▓▓▓██████
░░░░░░░░░░▓▓▓▓▓▓▓▓██████
░░░░░░░░░░▓▓▓▓▓▓▓▓██████
░░░░░░░░░░▓▓▓▓▓▓▓▓██████
░░░░░░░░░░▓▓▓▓▓▓▓▓██████
L'ASCII art, c'est l'art de faire des images avec des caractères. Dans les années 1980, avant les écrans couleur, c'était la seule façon d'afficher des graphismes. Les jeux vidéo de l'époque (Rogue, NetHack) utilisaient des caractères ASCII pour représenter monstres, murs et trésors. @ = vous, D = dragon. Tout un univers dans 256 caractères.

Variation : seuillage (binarisation)

def seuillage(image, seuil=128):
    """Transforme en noir & blanc : 0 si < seuil, 255 sinon."""
    return [[255 if pixel >= seuil else 0 for pixel in ligne] for ligne in image]

# Appliquer un seuil, c'est la base du traitement d'image —
# les scanners de documents, la reconnaissance de plaques
# d'immatriculation, et les filtres Instagram les plus basiques
# fonctionnent comme ça.

§3 Le texte, ce n'est pas que des chaînes

Les chaînes de caractères (str), on les utilise depuis le premier cours. Mais on peut faire bien plus que les afficher : découper, fusionner, chiffrer, analyser.

Les chaînes en Python, c'est comme le papier bulle : on peut les découper, les coller, les retourner dans tous les sens. Et contrairement au papier bulle, on peut les utiliser pour crypter des messages secrets. (Mais le papier bulle, c'est plus satisfaisant pour le stress.)

3.1 Découpage et fusion

phrase = "Python est le meilleur langage pour l'IA"

# Découpage : str.split() → liste de mots
mots = phrase.split()
print(mots)
# ['Python', 'est', 'le', 'meilleur', 'langage', 'pour', "l'IA"]

# Découpage avec séparateur personnalisé
data = "2024-03-15;42;3.14;true"
champs = data.split(";")
print(champs)  # ['2024-03-15', '42', '3.14', 'true']

# Fusion : séparateur.join(liste)
phrase_again = " ".join(mots)
print(phrase_again)  # "Python est le meilleur langage pour l'IA"

csv_ligne = ",".join(champs)
print(csv_ligne)  # "2024-03-15,42,3.14,true"
['Python', 'est', 'le', 'meilleur', 'langage', 'pour', "l'IA"]
['2024-03-15', '42', '3.14', 'true']
Python est le meilleur langage pour l'IA
2024-03-15,42,3.14,true
split et join sont deux des méthodes les plus utiles de Python. Retenez-les comme votre prénom et votre mot de passe (sauf que split et join vous ne les oublierez pas). split transforme une chaîne en liste ; join fait l'inverse.

3.2 Cryptage et décryptage — le chiffrement par décalage (César)

Le chiffre de César est l'une des plus anciennes méthodes de cryptographie. Jules César l'utilisait pour correspondre avec ses généraux. Le principe : on décale chaque lettre de l'alphabet d'un certain nombre de positions (ex: 3). A → D, B → E, etc. C'est trivial à casser aujourd'hui, mais à l'époque, les Gaulois ne savaient pas lire, alors ça suffisait.
def cesar(message, decalage):
    """Chiffre (ou déchiffre) un message par décalage de César.
    
    Arguments :
        message — str, le texte à transformer
        decalage — int, positif = chiffrer, négatif = déchiffrer
    Retourne :
        str, le message transformé
    """
    alphabet = "abcdefghijklmnopqrstuvwxyz"
    resultat = ""
    
    for car in message.lower():
        if car in alphabet:
            pos = alphabet.index(car)
            nouvelle_pos = (pos + decalage) % len(alphabet)
            resultat = resultat + alphabet[nouvelle_pos]
        else:
            # On laisse les espaces et la ponctuation inchangés
            resultat = resultat + car
    
    return resultat

message = "Bonjour les futurs ingenieurs"
crypte = cesar(message, 3)
print("Crypté :", crypte)

decrypte = cesar(crypte, -3)
print("Décrypté :", decrypte)
Crypté : erqmrxu ohv ixwxuv lqjhqlhxuv
Décrypté : bonjour les futurs ingenieurs
Vous venez de faire de la cryptographie. Bon, d'accord, c'est le niveau maternelle du chiffrement. Mais c'est la même idée que le RSA : prendre une donnée, la transformer avec une clé, et la rendre incompréhensible. La différence, c'est que César se casse en 5 secondes avec un dictionnaire, alors que RSA tiendrait 10⁹ ans. Les années lumières d'écart.

3.3 Chiffrement par substitution (un peu plus costaud)

def chiffre_substitution(message, cle):
    """Substitution mono-alphabétique.
    cle est une chaîne de 26 lettres représentant l'alphabet permuté.
    """
    alphabet = "abcdefghijklmnopqrstuvwxyz"
    table = str.maketrans(alphabet, cle)
    return message.lower().translate(table)

# On génère une clé aléatoire en mélangeant l'alphabet
import random
alphabet = list("abcdefghijklmnopqrstuvwxyz")
random.shuffle(alphabet)
cle = "".join(alphabet)

print("Clé :", cle)
print(chiffre_substitution("message secret", cle))
Clé : qxvajdwpfbzecygsoihkmlrntu
Texte chiffré : nqbbqcq iqnfig

La substitution mono-alphabétique est plus forte que César (26! ≈ 4×10²⁶ clés possibles), mais elle se casse facilement avec une analyse fréquentielle : dans un texte français, le 'e' est la lettre la plus courante (~14%), suivie du 'a', du 'i', etc. En regardant les lettres les plus fréquentes du texte chiffré, on devine la substitution.

L'analyse fréquentielle a été développée par le mathématicien arabe Al-Kindi (IXe siècle) dans son « Manuscrit sur le déchiffrement des messages cryptographiques ». C'est le premier traité connu de cryptanalyse. Al-Kindi avait compris que dans toute langue, les lettres apparaissent avec des fréquences stables — une idée qui n'a été dépassée qu'avec la machine Enigma (et encore, Turing l'a cassée).

3.4 Cryptage XOR — le « vrai » cryptage moderne

def xor_bytes(donnees, cle):
    """Crypte/décrypte par XOR avec une clé (répétée si nécessaire)."""
    resultat = bytearray()
    for i, octet in enumerate(donnees):
        resultat.append(octet ^ cle[i % len(cle)])
    return bytes(resultat)

# Testons avec un message en bytes
message = "Message ultra secret".encode("utf-8")
cle = "clef".encode("utf-8")

crypte = xor_bytes(message, cle)
print("Crypté (bytes) :", crypte)

decrypte = xor_bytes(crypte, cle)
print("Décrypté :", decrypte.decode("utf-8"))

# Propriété magique du XOR : (m XOR k) XOR k = m
# Le même code sert à chiffrer ET déchiffrer !
Crypté (bytes) : b'\x1d\x0e\x15\x17\x07\x1b\x1a\x07S\x1d\x1a\x11\x1c\x13V\x17\x1d\x1c\x13SS\x01'
Décrypté : Message ultra secret
Le XOR (ou exclusif, noté ^) est l'opération magique de la cryptographie. Si vous faites a ^ b ^ b, vous retrouvez a. C'est pour ça que le XOR est la base de tous les chiffrements modernes (AES, cha-cha, etc.). Et aussi que a ^ a = 0, ce qui en fait un très bon moyen de mettre une variable à zéro (les vieux programmeurs assembleur adorent ça).

Variation : analyse fréquentielle (pour casser les substitutions)

def frequences_lettres(texte):
    """Calcule la fréquence de chaque lettre dans le texte."""
    freq = {}
    for car in texte.lower():
        if car.isalpha():
            freq[car] = freq.get(car, 0) + 1
    return freq

# Test sur un vrai texte
extrait = "L'informatique est la science du traitement automatique de l'information"
freq = frequences_lettres(extrait)

# Trier par fréquence décroissante
trie = sorted(freq.items(), key=lambda x: -x[1])
for lettre, compte in trie[:5]:
    print(lettre, ":", "■" * compte, "(" + str(compte) + ")")
e : ■■■■■■■■■ (9)
i : ■■■■■■■■■ (9)
t : ■■■■■■■ (7)
a : ■■■■■■ (6)
n : ■■■■■■ (6)

Le 'e' et le 'i' dominent. Coïncidence ? Non. La langue française est ainsi faite. Si vous voyez 'z' arriver en tête, vous avez probablement un texte en espéranto ou un message chiffré.


§4 Les fichiers CSV sans bibliothèque

Le format CSV (Comma-Separated Values) est l'un des plus répandus pour échanger des données tabulaires. Chaque ligne est un enregistrement, les champs sont séparés par des virgules (ou points-virgules, selon le pays — merci l'Europe).

CSV, c'est le « Post-it » du data scientist : pas élégant, pas fiable, mais tout le monde l'utilise. Et bien sûr, les exportations Excel ont leurs propres règles (les fameux CSV « dialectes »). Vous allez vite comprendre pourquoi on a inventé les bibliothèques.

4.1 Lire un CSV à la main

def lit_csv(chemin, separateur=","):
    """Lit un fichier CSV et retourne une liste de dictionnaires.
    
    La première ligne est supposée contenir les noms de colonnes.
    """
    with open(chemin, "r", encoding="utf-8") as f:
        lignes = f.read().strip().split("\n")
    
    # La première ligne contient les en-têtes
    en_tetes = lignes[0].split(separateur)
    
    resultat = []
    for ligne in lignes[1:]:
        if ligne.strip() == "":
            continue  # ignorer les lignes vides
        valeurs = ligne.split(separateur)
        # On crée un dictionnaire : {en_tete: valeur, ...}
        dico = {}
        for i, en_tete in enumerate(en_tetes):
            dico[en_tete.strip()] = valeurs[i].strip()
        resultat.append(dico)
    
    return resultat

# Supposons qu'on ait un fichier etudiants.csv :
# nom,note,age
# Alice,14,22
# Bob,8,23
# Charlie,16,21

# Pour tester sans fichier, on simule le contenu :
contenu_csv = """nom,note,age
Alice,14,22
Bob,8,23
Charlie,16,21
"""
with open(...) as f: est la façon propre d'ouvrir un fichier en Python. Le fichier sera automatiquement fermé, même si une erreur survient. C'est comme un majordome qui range après votre départ. Utilisez-le toujours. Jamais f = open(...) sans with.

4.2 Analyser des données CSV sans pandas

# Fonction pour charger depuis une chaîne CSV (pour tester)
def parse_csv_string(contenu, separateur=","):
    lignes = contenu.strip().split("\n")
    en_tetes = lignes[0].split(separateur)
    donnees = []
    for ligne in lignes[1:]:
        if ligne.strip() == "":
            continue
        valeurs = ligne.split(separateur)
        dico = {}
        for i, en_tete in enumerate(en_tetes):
            dico[en_tete.strip()] = valeurs[i].strip()
        donnees.append(dico)
    return donnees

donnees = parse_csv_string(contenu_csv)

# Analysons : moyenne des notes
notes = [int(d["note"]) for d in donnees]
moyenne = sum(notes) / len(notes)
print("Moyenne des notes :", moyenne)

# Étudiants ayant plus de 10
adm = [d["nom"] for d in donnees if int(d["note"]) >= 10]
print("Admis :", adm)
Moyenne des notes : 12.666666666666666
Admis : ['Alice', 'Charlie']
Vous venez de réécrire pandas.read_csv() en 15 lignes. C'est moche, c'est fragile, ça ne gère pas les guillemets ni les virgules dans les champs. Mais ça marche. Et vous comprenez ce qu'il se passe. C'est ça qui compte. Pandas arrive vite, promis. Mais d'abord, il faut savoir marcher sans béquilles.

4.3 Écrire du CSV

def ecrit_csv(donnees, chemin, separateur=","):
    """Écrit une liste de dictionnaires dans un fichier CSV.
    
    Arguments :
        donnees — liste de dict, chaque dict est une ligne
        chemin — str, nom du fichier de sortie
        separateur — str, séparateur (défaut:',')
    """
    if not donnees:
        return
    
    # Récupérer les noms de colonnes (à partir des clés du premier dict)
    colonnes = list(donnees[0].keys())
    
    with open(chemin, "w", encoding="utf-8") as f:
        # Ligne d'en-tête
        f.write(separateur.join(colonnes) + "\n")
        
        # Lignes de données
        for d in donnees:
            ligne = separateur.join(str(d[col]) for col in colonnes)
            f.write(ligne + "\n")

# Testons
sortie = [
    {"nom": "Alice", "note": 14},
    {"nom": "Bob",   "note": 8},
]
ecrit_csv(sortie, "notes.csv")

# Vérifions ce qu'on a écrit
with open("notes.csv", "r") as f:
    print(f.read())
nom,note
Alice,14
Bob,8
Attention : cette version ne gère pas les cas tordus (champs contenant des virgules, des guillemts, des sauts de ligne). En production, utilisez le module csv de la bibliothèque standard. Mais pour comprendre le principe, c'est parfait.

Variation : filtre CSV (comme un mini-SQL)

def filtre_csv(donnees, colonne, valeur_min):
    """Filtre les lignes où la colonne (convertie en int) ≥ valeur_min."""
    return [d for d in donnees if int(d[colonne]) >= valeur_min]

# Qui a au moins 12 ?
bons = filtre_csv(donnees, "note", 12)
for d in bons:
    print(d["nom"], d["note"])
# Alice 14
# Charlie 16
Alice 14
Charlie 16

En faisant ça, vous avez implémenté un SELECT ... WHERE note >= 12 — sans base de données, sans SQL, juste des listes et des boucles.


§5 Pour aller plus loin

5.1 Que faire avec les fichiers en Python ?

Les fichiers sont partout : CSV, JSON, TXT, logs, configs. Python lit tout grâce à open(). Les modes :

5.2 Les images, pour de vrai

Une vraie image (PNG, JPG) n'est pas qu'une matrice de pixels — elle est compressée. Pour la manipuler sans bibliothèque, c'est une autre histoire. Mais le principe est là : une image est une matrice (ou trois matrices pour RGB). Les filtres de base (flou, netteté, détection de contours) ne sont que des opérations sur ces matrices. C'est ce qu'on appelle la convolution — le cœur des réseaux de neurones convolutionnels (CNN).

5.3 Gestion fine des CSV : les guillemets

Vrai problème : les champs qui contiennent des virgules. La solution : entourer le champ de guillemets doubles. Ex : "Dupont, Jean",14,22. Pour parser ça proprement, il faut un petit automate. C'est exactement ce que fait le module csv de la bibliothèque standard. Utilisez-le en production.


§6 Le mot de la fin

Aujourd'hui, vous avez ajouté trois cordes à votre arc : les tableaux 2D (matrices, images), les textes (découpage, fusion, cryptographie), et les CSV (lecture, écriture, filtrage). Sans importer une seule bibliothèque externe. Vous méritez une médaille. (Ou au moins un café.)

Claude Shannon (1916–2001) — père de la théorie de l'information. Dans son article fondateur de 1948, « A Mathematical Theory of Communication », il a jeté les bases de tout ce qu'on a vu aujourd'hui : comment représenter l'information (matrices, textes), comment la compresser, comment la crypter. Shannon a aussi construit un dispositif capable de jouer aux échecs, un labyrinthe mécanique pour souris (« Theseus »), et un ordinateur de poche en chiffres romains. Bref, il s'ennuyait le week-end. Comme vous, maintenant que vous savez manipuler des données en Python.
Bilan du cours 3 : Prochaine étape : NumPy, matplotlib, et les vrais datasets. Vous êtes prêt. Allez, au boulot !
© 2026 — laurent.thiry@uha.fr