Leçon 03-2 — Slicing et parcours de listes

1. Objectif

À la fin de cette leçon tu sauras : découper n'importe quelle tranche d'une liste ou d'une chaîne avec [start:stop:step], copier une liste avec [:], parcourir proprement avec for et enumerate, construire une liste en boucle, et éviter le piège du parcours d'une liste qu'on modifie en même temps.

2. Pourquoi c'est important

« Les 3 premiers », « les 5 derniers », « un élément sur deux », « tout sauf le dernier » : le découpage de données est un besoin quotidien — pagination, podiums, extraits, historiques. Le slicing le fait en UNE expression lisible, là où d'autres langages écrivent des boucles. Et le duo parcours + construction de liste est LE motif de transformation de données que tu écriras des milliers de fois (il deviendra encore plus puissant avec les fonctions au niveau 4 et les fichiers au niveau 5).

3. Explication simple

Une slice (tranche), c'est « donne-moi les casiers de start inclus à stop exclu » :

nums = [10, 20, 30, 40, 50]
print(nums[1:4])    # [20, 30, 40]  : casiers 1, 2, 3 — le 4 est EXCLU

Imagine les index comme des coups de couteau entre les casiers, pas sur les casiers :

      0    1    2    3    4
    ┌────┬────┬────┬────┬────┐
    │ 10 │ 20 │ 30 │ 40 │ 50 │
    └────┴────┴────┴────┴────┘
    0    1    2    3    4    5
         ↑couteau      ↑couteau     nums[1:4] → ce qu'il y a entre les deux

Si tu omets un bord, Python prend « depuis le début » ou « jusqu'à la fin » : nums[:3] (les 3 premiers), nums[2:] (de l'index 2 à la fin), nums[:] (tout — une copie). Et le parcours, tu le connais déjà (⏪ niveau 02) : for n in nums: visite chaque casier dans l'ordre.

4. Explication approfondie

a) Pourquoi stop est exclu. Même convention que range (⏪ niveau 02), et elle rend les calculs propres : nums[:k] contient exactement k éléments ; nums[:k] + nums[k:] reconstitue exactement la liste, sans trou ni doublon. Une fois qu'on l'accepte, cette convention élimine des off-by-one au lieu d'en créer.

b) Le troisième nombre : le pas. nums[::2] = un élément sur deux ; nums[::-1] = la liste à l'envers (pas de −1). Les slices marchent sur les chaînes aussi : "python"[1:4]"yth", "python"[::-1]"nohtyp".

c) Une slice ne plante JAMAIS. nums[1:100] sur 5 éléments ne lève pas d'IndexError : Python rabote aux bornes réelles et renvoie ce qui existe ([20, 30, 40, 50]), quitte à renvoyer []. Confort… et piège : une borne fausse ne fait pas de bruit (section 6).

d) Une slice fabrique une NOUVELLE liste. nums[1:4] ne modifie pas nums — c'est une opération « qui renvoie », jamais « sur place » (⏪ la distinction de 03-1). Cas particulier précieux : nums[:] copie toute la liste. ⏩ POURQUOI on a besoin de copier — et pourquoi b = nums ne copie rien — est le sujet de la leçon 03-4, la plus importante du niveau.

e) enumerate : l'index ET la valeur. Quand tu as besoin des deux, n'écris pas for i in range(len(nums)) (lourd, et source d'off-by-one) :

for i, name in enumerate(names):        # i, name : déballage — deux
    print(i, name)                      # variables remplies d'un coup

enumerate(names, start=1) fait démarrer le compte à 1 — parfait pour l'affichage humain. ⏩ Le déballage vient des tuples, détaillé en 03-3.

f) Le piège du niveau : modifier la liste qu'on parcourt. Retirer des éléments PENDANT un for sur cette même liste fait sauter des éléments, sans aucune erreur (démonstration à l'exemple 6). Règle : on parcourt une copie (for n in nums[:]), ou mieux, on construit une nouvelle liste avec ce qu'on garde. Ce piège produit des bugs sournois même chez des développeurs expérimentés — l'exemple 6 est à taper et à retenir.

5. Exemples commentés

Exemple 1 — slices de base

nums = [10, 20, 30, 40, 50]
print(nums[1:4])     # [20, 30, 40]      : de 1 inclus à 4 exclu
print(nums[:3])      # [10, 20, 30]      : les 3 premiers
print(nums[2:])      # [30, 40, 50]      : de l'index 2 à la fin
print(nums[-3:])     # [30, 40, 50]      : les 3 DERNIERS
print(nums[:-1])     # [10, 20, 30, 40]  : tout sauf le dernier

Exemple 2 — pas, inversion, chaînes

nums = [10, 20, 30, 40, 50]
print(nums[::2])     # [10, 30, 50]          : un sur deux
print(nums[::-1])    # [50, 40, 30, 20, 10]  : à l'envers

word = "python"
print(word[1:4])     # yth      : les chaînes se découpent pareil
print(word[::-1])    # nohtyp   : très utile pour les palindromes

Exemple 3 — la slice renvoie, elle ne modifie pas ; [:] copie

nums = [10, 20, 30]
top = nums[:2]
print(top)           # [10, 20]
print(nums)          # [10, 20, 30] : intact — une slice ne modifie JAMAIS

copy = nums[:]       # copie complète
copy.append(99)
print(nums)          # [10, 20, 30] : l'originale n'a pas bougé
print(copy)          # [10, 20, 30, 99]

⏩ Pourquoi copy = nums (sans [:]) n'aurait PAS protégé l'originale : leçon 03-4.

Exemple 4 — enumerate pour numéroter

podium = ["Ada", "Linus", "Guido"]
for i, name in enumerate(podium, start=1):
    print(f"{i}. {name}")

Sortie :

1. Ada
2. Linus
3. Guido

enumerate fournit à chaque tour une paire (index, valeur) que le for déballe dans i et name. Sans start=1, i commence à 0.

Exemple 5 — construire une liste en boucle (LE motif à automatiser)

squares = []                  # 1. on part d'une liste vide
for n in range(1, 6):         # 2. on parcourt une source
    squares.append(n * n)     # 3. on accumule les résultats
print(squares)                # [1, 4, 9, 16, 25]

C'est l'accumulateur du niveau 02, version liste. Transformation, filtrage, collecte : tout passe par ce motif à trois temps.

Exemple 6 — ⚠️ le piège : retirer pendant qu'on parcourt

nums = [2, 4, 6, 8]
for n in nums:
    if n % 2 == 0:
        nums.remove(n)        # on retire DANS la liste qu'on parcourt
print(nums)                   # [4, 8]  ← et non [] !

Tous les éléments sont pairs, on s'attend à tout retirer… il en reste deux. Pourquoi : quand remove(2) retire le casier 0, tout se décale à gauche ; le curseur du for, lui, avance quand même → le 4 (devenu casier 0) est sauté. Aucune erreur, résultat faux. Les deux corrections :

nums = [2, 4, 6, 8]
for n in nums[:]:             # correction 1 : on parcourt une COPIE figée,
    if n % 2 == 0:            # on modifie l'originale
        nums.remove(n)
print(nums)                   # []

nums = [2, 4, 6, 8]
odds = []                     # correction 2 (préférée) : construire une
for n in nums:                # NOUVELLE liste avec ce qu'on garde
    if n % 2 != 0:
        odds.append(n)
print(odds)                   # []

6. Erreurs fréquentes

1) Croire que stop est inclus

nums = [10, 20, 30, 40, 50]
print(nums[0:2])    # [10, 20] — PAS [10, 20, 30]

« Les 3 premiers » = nums[:3], pas nums[:2]. Astuce anti-doute : stop - start = nombre d'éléments obtenus.

2) Une borne trop grande ne prévient pas

print(nums[10:20])   # []  — pas d'IndexError, juste une liste vide

Contrairement à nums[10] qui planterait, la slice se tait. Si ton programme « perd » mystérieusement des données, affiche la slice ET ses bornes.

3) start après stop : liste vide silencieuse

print(nums[4:1])     # []  — le couteau de gauche est APRÈS celui de droite

(Avec un pas positif, Python avance de start vers stop ; il n'y a rien à prendre.) Souvent le symptôme de bornes calculées dans le mauvais ordre.

4) Oublier de déballer enumerate

for item in enumerate(["a", "b"]):
    print(item)      # (0, 'a')  puis  (1, 'b')  — des paires entières !

Sans les deux variables (for i, x in ...), tu reçois le tuple complet. Pas d'erreur, mais un affichage (0, 'a') là où tu attendais a.

5) Modifier la liste qu'on parcourt — l'exemple 6. Symptôme : des éléments « survivent » au filtrage, un sur deux en général. Réflexe : dès que tu écris remove/pop/append sur la liste du for, arrête-toi et passe par une copie ou une nouvelle liste.

6) « Copier » avec =

copy = nums          # ce n'est PAS une copie (aucune erreur, hélas)
copy.append(99)      # ... nums vient aussi de "recevoir" 99 !

copy = nums ne crée aucune nouvelle liste — les deux noms désignent la même. La vraie copie s'écrit nums[:] ou nums.copy(). ⏩ L'explication complète (et les schémas) : leçon 03-4. Si tu ne dois retenir qu'un avant-goût : = ne copie jamais une liste.

7. Lire les messages d'erreur

Bonne nouvelle : les slices ne lèvent pratiquement jamais d'exception — leurs bugs sont silencieux (sections 6.2 et 6.3), et se déboguent au print. Les tracebacks de cette leçon viennent donc du parcours.

Se tromper de variable de déballage :

Traceback (most recent call last):
  File "C:\Users\jtron\Claude\learnpython\ranking.py", line 2, in <module>
    for i, n in nums:
        ^^^^
TypeError: cannot unpack non-iterable int object

De bas en haut : cannot unpack non-iterable int object = « tu essaies de déballer un int en deux variables ». Le for i, n in nums: promet des paires, mais nums contient des entiers simples — il manque enumerate : for i, n in enumerate(nums):. Les ^^^^ pointent la cible du déballage.

Indexer avec autre chose qu'un entier :

Traceback (most recent call last):
  File "C:\Users\jtron\Claude\learnpython\ranking.py", line 3, in <module>
    print(nums[choice])
          ~~~~^^^^^^^^
TypeError: list indices must be integers or slices, not str

list indices must be integers or slices, not str = l'index fourni est une chaîne. Cause n° 1 : un input() jamais converti (⏪ niveau 01) — choice = input(...) renvoie "2", pas 2. Correction : choice = int(input(...)).

Et si tu recroises IndexError : c'est un accès simple nums[i] (pas une slice) — relis la section 7 de la leçon 03-1.

8. Exercices — faciles

Règles : un fichier par exercice dans lessons/level-03-structures-donnees/exercices/mes-reponses/, nommé ex-03-2-a.py, etc. Prédis chaque sortie AVANT d'exécuter.

a) Avec ranking = ["Ada", "Linus", "Guido", "Grace", "Tim"], affiche en quatre print et uniquement des slices : le podium (3 premiers), les 2 derniers, tout sauf le premier, le classement inversé.

b) Prédis sur papier les cinq sorties, puis vérifie :

nums = [10, 20, 30, 40, 50]
print(nums[1:3])
print(nums[:2])
print(nums[3:])
print(nums[1:100])
print(nums[::2])

c) Demande un mot avec input() et affiche-le à l'envers. Puis affiche Palindrome ! ou Pas un palindrome. selon le cas (compare le mot et son inverse ; teste avec kayak et python).

9. Exercices — moyens

a) Ce programme doit garder uniquement les mots de 3 lettres ou moins, mais son résultat est faux. Prédis ce qu'il affiche réellement, explique pourquoi le mot fautif survit, puis écris les DEUX corrections (copie, puis nouvelle liste) :

words = ["le", "python", "genial", "est", "ok"]
for word in words:
    if len(word) > 3:
        words.remove(word)
print(words)

b) Avec temps = [12, 19, 12, 7, 12] : construis (en boucle, avec enumerate) la liste des index où apparaît la valeur 12. Sortie attendue : [0, 2, 4]. Puis affiche chaque position sur sa ligne au format Jour 1 : 12°C (attention : jour = index + 1).

c) Reprends la liste de courses du mini-projet 03-1 : ajoute au listing un numéro devant chaque article (1. cafe) avec enumerate(..., start=1) — et explique en commentaire pourquoi start=1 évite un + 1 dans la f-string.

10. Exercices — difficiles

a) Rotation de liste. Écris un programme qui fait « tourner » une liste de k positions vers la gauche : [1, 2, 3, 4, 5] avec k = 2 devient [3, 4, 5, 1, 2]. En UNE ligne de slices (pas de boucle). Puis la rotation vers la droite. Vérifie avec k = 0 et k = 5 et explique ce que tu observes.

b) Un sur deux, sans le pas. Reconstruis nums[::2] sans utiliser de slice à pas : en boucle avec enumerate, garde les éléments d'index pair. Compare les deux versions dans un commentaire : laquelle préfères-tu, et pourquoi ?

11. Mini-projet lié — « pagination »

Écris paginate.py : le programme contient une liste d'au moins 10 titres (films, morceaux, livres…) et l'affiche page par page, 3 titres à la fois :

--- Page 1 ---
1. Dune
2. Alien
3. Brazil
[Entree pour la suite, q pour quitter]

Contraintes : la taille de page est une variable PAGE_SIZE = 3 (le programme doit marcher en la passant à 4) ; chaque page est extraite avec UNE slice (items[start:start + PAGE_SIZE]) ; la numérotation est continue d'une page à l'autre (la page 2 commence à 4 — enumerate a un start pour ça) ; la dernière page peut être incomplète et ne doit pas planter (relis la section 4.c : les slices rabotent). Durée cible : 30–45 min.

12. Correction / méthode de correction

Les solutions détaillées sont dans solutions/serie-03-2-solutions.md.

Méthode : (1) tous les exercices d'abord ; (2) compare le raisonnement, pas seulement le code ; (3) note chaque écart dans le journal ; (4) pour 9-a, si ta prédiction était fausse, déroule l'exécution en dessinant la liste ET la position du curseur à chaque tour — c'est le seul moyen de VOIR le saut.

13. À retenir

14. Questions de révision

  1. Combien d'éléments contient lst[2:7] (si la liste est assez longue) ? Quelle formule le donne ?
  2. Pourquoi lst[:k] + lst[k:] reconstitue-t-il toujours la liste exacte ?
  3. Que renvoie nums[10:20] sur une liste de 5 éléments, et pourquoi est-ce à la fois pratique et dangereux ?
  4. Quelle est la différence entre for x in lst et for i, x in enumerate(lst) ?
  5. Décris le motif en trois temps pour construire une liste en boucle.
  6. Pourquoi retirer des éléments pendant un for sur la même liste saute-t-il des éléments ? Quelles sont les deux parades ?
  7. Qu'affiche print(word[::-1]) pour word = "kayak", et à quoi ce genre de slice sert-il ?
  8. Que reproche TypeError: list indices must be integers or slices, not str, et quelle en est la cause la plus fréquente ?

15. Checklist de compréhension

Si une case reste vide : refais les exercices de la section correspondante demain, AVANT d'ouvrir la leçon suivante.

16. Commit conseillé

git add lessons/level-03-structures-donnees/exercices/mes-reponses/
git commit -m "exercises: complete lesson 03-2 slicing and iteration"

Puis, si c'est ta fin de session : entrée de journal + git push.