Solutions — série 03-2 (Slicing et parcours de listes)
Rappel de la règle d'or : on n'ouvre ce fichier qu'après avoir terminé (ou sérieusement séché sur) la série. Compare le raisonnement, pas seulement le code : une solution différente qui marche est souvent valable.
Exercice facile a — quatre slices du classement
Raisonnement — Chaque demande se traduit mot à mot en slice : « les 3 premiers » = du début à 3 exclu ; « les 2 derniers » = depuis −2 ; « tout sauf le premier » = depuis 1 ; « inversé » = pas de −1.
Solution
# ex-03-2-a.py
ranking = ["Ada", "Linus", "Guido", "Grace", "Tim"]
print(ranking[:3]) # podium
print(ranking[-2:]) # les 2 derniers
print(ranking[1:]) # tout sauf le premier
print(ranking[::-1]) # inversé
Sortie :
['Ada', 'Linus', 'Guido']
['Grace', 'Tim']
['Linus', 'Guido', 'Grace', 'Tim']
['Tim', 'Grace', 'Guido', 'Linus', 'Ada']
Pourquoi ça marche — ranking[:3] contient exactement 3 éléments (stop - start = 3 - 0). [-2:] se lit « du deuxième depuis la fin, jusqu'au bout ». Aucune de ces lignes ne modifie ranking : les slices renvoient des listes neuves.
Erreur classique sur cet exercice — Le podium en ranking[:2] (stop exclu → 2 éléments seulement) et les 2 derniers en ranking[-2] sans les deux-points : sans :, c'est un simple index, tu obtiens 'Grace' toute seule, pas une liste.
Variante plus difficile — Extrais « le podium inversé » (['Guido', 'Linus', 'Ada']) en UNE slice composée… puis constate que ranking[:3][::-1] (deux slices enchaînées) est plus lisible que ranking[2::-1] — les slices très malignes se paient en relecture.
Exercice facile b — prédire cinq slices
Raisonnement — On applique mécaniquement : start inclus, stop exclu, bornes rabotées si trop grandes, pas de 2 = un élément sur deux.
Solution — Sur nums = [10, 20, 30, 40, 50] :
[20, 30] nums[1:3] : index 1 et 2
[10, 20] nums[:2] : les 2 premiers
[40, 50] nums[3:] : de l'index 3 à la fin
[20, 30, 40, 50] nums[1:100] : 100 est raboté à 5 — pas d'erreur !
[10, 30, 50] nums[::2] : index 0, 2, 4
Pourquoi ça marche — La seule ligne piégeuse est nums[1:100] : une slice ne lève jamais d'IndexError, elle rend ce qui existe. C'est un choix de conception de Python : pratique pour « les 100 premiers » d'une liste qui en a 30, dangereux quand la borne fausse est un bug.
Erreur classique sur cet exercice — Prédire un crash sur nums[1:100] — ou, inversement, en déduire que nums[100] (accès simple) serait toléré aussi : non, LUI plante. Slices tolérantes, index stricts.
Variante plus difficile — Prédis nums[3:1], nums[-100:2] et nums[::-2]. (Réponses : [] — bornes croisées ; [10, 20] — −100 raboté au début ; [50, 30, 10] — un sur deux, en partant de la fin.)
Exercice facile c — palindrome
Raisonnement — word[::-1] fabrique la chaîne inversée ; un palindrome est un mot égal à son inverse — une comparaison == suffit. On stocke l'inverse dans une variable pour l'afficher ET le comparer sans le recalculer.
Solution
# ex-03-2-c.py
word = input("Un mot : ")
reversed_word = word[::-1]
print(f"A l'envers : {reversed_word}")
if word == reversed_word:
print("Palindrome !")
else:
print("Pas un palindrome.")
Exemples d'exécution :
Un mot : kayak
A l'envers : kayak
Palindrome !
Un mot : python
A l'envers : nohtyp
Pas un palindrome.
Pourquoi ça marche — Les chaînes se découpent comme les listes ; le pas −1 parcourt de la fin vers le début. == compare les contenus caractère par caractère.
Erreur classique sur cet exercice — Tenter word.reverse() : cette méthode n'existe que sur les listes (et modifie sur place) — AttributeError: 'str' object has no attribute 'reverse'. Les chaînes sont immuables : on ne les retourne pas, on en crée une retournée.
Variante plus difficile — Rends le test robuste : Kayak doit passer (.lower() d'abord), puis engage le jeu que je le gagne (ignorer les espaces : word.replace(" ", "")).
Exercice moyen a — le filtrage qui saute des mots
Raisonnement — On déroule en suivant DEUX choses : la liste (qui rétrécit) et le curseur du for (qui avance quoi qu'il arrive).
départ : ["le", "python", "genial", "est", "ok"]
i=0 → "le" court, gardé
i=1 → "python" long, retiré → ["le", "genial", "est", "ok"]
"genial" glisse à l'index 1... déjà visité par personne !
i=2 → "est" court, gardé ← "genial" a été SAUTÉ
i=3 → "ok" court, gardé
Solution — Le programme affiche réellement :
['le', 'genial', 'est', 'ok']
"genial" (6 lettres) survit : quand remove("python") a décalé la liste vers la gauche, le curseur est passé par-dessus lui. Les deux corrections :
# ex-03-2-d.py — correction 1 : parcourir une COPIE
words = ["le", "python", "genial", "est", "ok"]
for word in words[:]: # la copie fige la "photo" à parcourir
if len(word) > 3:
words.remove(word) # on modifie l'originale, sans risque
print(words) # ['le', 'est', 'ok']
# correction 2 (préférée) : construire une NOUVELLE liste
words = ["le", "python", "genial", "est", "ok"]
short_words = []
for word in words:
if len(word) <= 3: # on décrit ce qu'on GARDE (condition inversée)
short_words.append(word)
print(short_words) # ['le', 'est', 'ok']
Pourquoi ça marche — Correction 1 : le for parcourt un objet que personne ne modifie ; les remove touchent l'autre. Correction 2 : aucune suppression du tout — filtrer = décrire ce qu'on garde. La version 2 est préférée : plus simple à relire, pas de remove (donc pas de parcours caché de la liste à chaque suppression), et la condition dit positivement le critère.
Erreur classique sur cet exercice — Prédire ['le', 'est', 'ok'] (le résultat voulu) : c'est le piège — le programme buggé donne un résultat différent selon la position des mots longs. Preuve : avec l'ordre ["le", "python", "est", "genial", "ok"], le même code donne le bon résultat par accident. Un bug qui dépend de l'ordre des données est le pire à attraper en vrai code.
Variante plus difficile — Trouve une liste d'entrée pour laquelle le code buggé ne retire AUCUN mot long. (Piste : deux mots longs consécutifs, le deuxième en dernière position… expérimente.)
Exercice moyen b — les index du 12
Raisonnement — On veut des index, pas des valeurs : c'est le signal enumerate. Motif construction de liste + condition sur la valeur, append de l'index.
Solution
# ex-03-2-e.py
temps = [12, 19, 12, 7, 12]
positions = []
for i, t in enumerate(temps):
if t == 12:
positions.append(i) # on accumule l'INDEX, pas la valeur
print(positions)
for i in positions:
print(f"Jour {i + 1} : 12°C") # jour humain = index + 1
Sortie :
[0, 2, 4]
Jour 1 : 12°C
Jour 3 : 12°C
Jour 5 : 12°C
Pourquoi ça marche — enumerate fournit (index, valeur) à chaque tour ; on filtre sur la valeur et on stocke l'index. La conversion « humain » (+1) est faite UNIQUEMENT à l'affichage : les index stockés restent de vrais index utilisables (temps[positions[0]] marche).
Erreur classique sur cet exercice — positions.append(t) au lieu de i : on obtient [12, 12, 12] — relis ce que tu accumules. Ou stocker directement i + 1 « pour gagner du temps » : les positions ne sont alors plus des index valides de la liste, et le prochain usage plantera ou mentira.
Variante plus difficile — Trouve l'index du maximum de la liste avec le même motif (une seule passe, deux variables best et best_index) — puis compare avec temps.index(max(temps)) : combien de parcours de liste chaque version fait-elle ?
Exercice moyen c — listing numéroté
Raisonnement — Le listing de 03-1 affichait - cafe ; on remplace le tiret par un compteur : enumerate(sorted(items), start=1) fournit directement le bon numéro.
Solution
# extrait de shopping_list.py — le choix "3" révisé
elif choice == "3":
if len(items) == 0:
print("Liste vide.")
else:
# start=1 : enumerate compte lui-même a partir de 1 ;
# sans lui, il faudrait ecrire {i + 1} dans la f-string,
# et ce "+1" oublie un jour est un off-by-one garanti.
for i, item in enumerate(sorted(items), start=1):
print(f"{i}. {item}")
Affichage obtenu :
1. beurre
2. cafe
Pourquoi ça marche — enumerate accepte le point de départ du comptage : autant lui faire porter la responsabilité du « numéro humain » plutôt qu'à une arithmétique répétée dans la f-string.
Erreur classique sur cet exercice — Le double décalage : start=1 ET {i + 1} dans la f-string — le listing commence à 2. Choisis UN endroit pour le décalage, et un seul.
Variante plus difficile — Aligne les numéros sur deux caractères pour les listes de plus de 9 articles ({i:>2}. {item}) — teste avec 12 articles et compare l'alignement avec et sans le format.
Exercice difficile a — rotation de liste
Raisonnement — Faire tourner de k vers la gauche = les k premiers partent à la fin, le reste passe devant. Ces deux blocs sont exactement nums[:k] et nums[k:] : la rotation est leur recollage inversé. Pour la droite, même découpe mais depuis la fin : nums[-k:] (les k derniers) passent devant.
Solution
# ex-03-2-g.py
nums = [1, 2, 3, 4, 5]
k = 2
rotated_left = nums[k:] + nums[:k]
print(rotated_left) # [3, 4, 5, 1, 2]
rotated_right = nums[-k:] + nums[:-k]
print(rotated_right) # [4, 5, 1, 2, 3]
Avec k = 0 : nums[0:] = tout, nums[:0] = [] → la liste inchangée. Avec k = 5 : nums[5:] = [], nums[:5] = tout → inchangée aussi. Un tour complet ramène au départ — la formule gère les bords sans aucun if.
Pourquoi ça marche — C'est la propriété de recollage de la leçon (nums[:k] + nums[k:] reconstitue la liste) appliquée dans l'autre sens : les deux blocs couvrent la liste exactement, sans trou ni doublon, donc leur recollage inversé est une permutation propre.
Erreur classique sur cet exercice — Inverser les blocs (nums[:k] + nums[k:] — la liste ne tourne pas du tout), ou pour la rotation droite écrire nums[k:] + nums[:k] avec un k « négatif dans sa tête » : pose les deux blocs sur papier AVANT d'écrire la slice.
Variante plus difficile — Et si k = 7 sur 5 éléments ? La formule casse (nums[7:] = [], nums[:7] = tout : plus de rotation). Corrige avec le modulo : k = k % len(nums) — 7 tours sur 5 éléments = 2 tours. (Le modulo est un vieil ami du niveau 01.)
Exercice difficile b — un sur deux, sans le pas
Raisonnement — Reproduire nums[::2] = garder les index pairs. Le test de parité d'un index demande cet index : enumerate, filtre i % 2 == 0, accumulation.
Solution
# ex-03-2-h.py
nums = [10, 20, 30, 40, 50]
every_other = []
for i, n in enumerate(nums):
if i % 2 == 0: # index PAIR : 0, 2, 4...
every_other.append(n) # ...mais on accumule la VALEUR
print(every_other) # [10, 30, 50]
print(nums[::2]) # [10, 30, 50] — même résultat
# Comparaison : la slice dit QUOI ("un sur deux"), la boucle dit COMMENT.
# Pour un besoin aussi standard, la slice gagne : une expression, zero
# variable intermediaire, zero risque d'off-by-one. La boucle reprend
# l'avantage des que le critere se complique (un sur deux MAIS seulement
# les valeurs > 15, par exemple) : une slice ne sait pas filtrer sur la valeur.
Pourquoi ça marche — Le test porte sur i (la position), l'accumulation sur n (la valeur) : enumerate donne les deux sans range(len(...)).
Erreur classique sur cet exercice — Tester n % 2 == 0 (la parité de la valeur) au lieu de i % 2 == 0 (celle de l'index) : sur cette liste tout est pair, le bug est invisible… jusqu'à la première liste de valeurs impaires. Teste toujours avec des données qui séparent les deux interprétations ([1, 2, 3, 4] : index pairs → [1, 3] ; valeurs paires → [2, 4]).
Variante plus difficile — Écris « un sur trois, en commençant au deuxième » dans les deux styles (nums[1::3] contre la boucle). Puis mélange : « un sur deux, parmi les valeurs > 15 seulement » — et constate que seule la boucle y arrive proprement.
Mini-projet — « pagination »
Raisonnement — L'état de la pagination tient en UNE variable : start, l'index du premier titre de la page courante. Chaque page est items[start:start + PAGE_SIZE] ; on avance de PAGE_SIZE ; on s'arrête quand start dépasse la fin. La numérotation continue est le rôle de enumerate(..., start=start + 1) : le compteur repart de l'index global, pas de 1.
Solution
# paginate.py
PAGE_SIZE = 3
items = [
"Dune", "Alien", "Brazil", "Stalker", "Metropolis",
"Gattaca", "Interstellar", "Primer", "Moon", "Solaris",
]
start = 0
page_number = 1
while start < len(items):
print(f"--- Page {page_number} ---")
page = items[start:start + PAGE_SIZE] # rabote toute seule en fin
for i, title in enumerate(page, start=start + 1):
print(f"{i}. {title}") # numerotation CONTINUE
start += PAGE_SIZE
page_number += 1
if start < len(items): # pas de pause apres la fin
answer = input("[Entree pour la suite, q pour quitter] ")
if answer == "q":
break
Exemple de session (extrait) :
--- Page 1 ---
1. Dune
2. Alien
3. Brazil
[Entree pour la suite, q pour quitter]
--- Page 2 ---
4. Stalker
5. Metropolis
6. Gattaca
[Entree pour la suite, q pour quitter] q
Avec 10 titres et PAGE_SIZE = 3, la page 4 n'affiche qu'un titre (10. Solaris) — la slice a raboté, aucun IndexError, aucun if spécial.
Pourquoi ça marche — Tout le projet repose sur la tolérance des slices (section 4.c de la leçon) : la dernière page incomplète est un cas particulier PARTOUT ailleurs, sauf dans une slice. Le duo start/start + PAGE_SIZE garantit des pages jointives sans trou ni doublon — c'est la propriété de recollage, encore elle.
Erreur classique sur cet exercice — Numéroter chaque page de 1 à 3 (oubli du start=start + 1) : les pages semblent correctes isolément, mais « le titre n° 2 » devient ambigu — bug d'interface, pas de code. Autre classique : while start <= len(items): (off-by-one) qui affiche une dernière page… vide.
Variante plus difficile — (1) Affiche Page 2/4 : le nombre total de pages se calcule en divisant en arrondissant vers le haut — trouve la formule avec // (piste : (len(items) + PAGE_SIZE - 1) // PAGE_SIZE). (2) Ajoute p pour revenir à la page précédente (garde-fou : pas avant la page 1). (3) ⏩ Au niveau 4, tu transformeras ce script en fonction paginate(items, page_size) réutilisable.