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 marcheranking[: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

Raisonnementword[::-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 marcheenumerate 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 exercicepositions.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 marcheenumerate 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.