Skip to content

Les séquences

Rappel d'ouverture (5 minutes, cours fermé)

Réponds sans rouvrir les pages précédentes, en écrivant tes réponses.

  1. Écris la boucle qui parcourt une chaîne mot par indice plutôt que par élément.
  2. Avec s = "chat", que se passe-t-il si on écrit s[0] = "p" ?
  3. On écrit a = [1, 2], b = a, b.append(3). Que vaut a ? Et si on avait écrit b = a.copy() ?
Corrigé
  1. for i in range(len(mot)): puis mot[i] dans le corps. Les indices vont de 0 à len(mot) - 1, et range s'arrête juste avant sa borne : les deux tombent juste ensemble.
  2. Une TypeError : une chaîne est immuable, on ne peut pas en changer un caractère.
  3. a vaut [1, 2, 3] : les deux noms désignent le même objet. Avec copy(), a vaudrait [1, 2], car on aurait fabriqué une seconde liste.

Ces trois questions portent sur trois types différents. Toute la page d'aujourd'hui consiste à voir ce qui est commun aux trois, et la seule chose qui les sépare vraiment.

Tu connais maintenant trois façons de ranger plusieurs valeurs dans une seule variable : la chaîne de caractères, la liste et le tuple. Tu les as apprises séparément, et tu as pu avoir l'impression d'apprendre trois choses.

C'est faux : tu en as appris une seule, déclinée trois fois. Cette page le montre, et c'est la raison pour laquelle la suite du cours te coûtera moins cher que le début.

Séquence

Une séquence est une suite d'éléments ordonnés, numérotés de 0 à len(seq) - 1, auxquels on accède par leur indice.

En Python, les séquences sont les chaînes (str), les listes (list) et les tuples (tuple).

Ce qui est commun aux trois

Aucune de ces écritures ne dépend du type. Recopie ce tableau, c'est le résumé de trois pages de cours.

Ce qu'on veut Écriture str list tuple
l'élément numéro i seq[i] oui oui oui
le dernier élément seq[-1] oui oui oui
le nombre d'éléments len(seq) oui oui oui
savoir si x s'y trouve x in seq oui oui oui
tout parcourir for e in seq: oui oui oui
parcourir par indice for i in range(len(seq)): oui oui oui
coller deux séquences seq1 + seq2 oui oui oui
répéter seq * n oui oui oui
en extraire un morceau seq[debut:fin] oui oui oui
mot    = "Pikachu"
tokens = ["Une", "suite", "d'éléments"]
point  = (5, 0, 12)

print(mot[0], tokens[0], point[0])              # P Une 5
print(len(mot), len(tokens), len(point))        # 7 3 3
print("k" in mot, "suite" in tokens, 0 in point) # True True True

C'est ce qui rend la suite plus facile

Chaque fois que tu rencontreras une nouvelle structure de données, commence par te demander : est-ce une séquence ? Si oui, tu sais déjà y accéder, la mesurer et la parcourir, sans rien apprendre de neuf. Il ne te restera à apprendre que ce qui lui est propre.

Ce qui les sépare : la mutabilité

C'est la seule différence de fond entre les trois, et c'est celle qui produit le plus de bugs.

Mutable, immuable

  • Une séquence immuable ne peut pas être changée après sa création. Toute opération qui semble la modifier fabrique en réalité une nouvelle séquence. Les chaînes et les tuples sont immuables.
  • Une séquence mutable peut être changée sur place, sans en fabriquer une nouvelle. Les listes sont mutables.

L'analogie, et sa limite

Un tuple ou une chaîne, c'est un texte écrit au stylo : pour corriger, il faut tout réécrire. Une liste, c'est un texte écrit au crayon : on gomme et on modifie sur place.

La limite de l'image : au crayon comme au stylo, il n'y a qu'un seul exemplaire du texte. Si deux noms désignent la même liste, la gommer par l'un la gomme pour l'autre. C'est tout le problème ci-dessous.

Ce qui se passe en mémoire

C'est le point à comprendre, pas à retenir. Une variable ne contient pas la séquence : elle pointe vers elle.

Cas immuable. On ne peut rien changer, donc la seule façon d'obtenir une valeur différente est d'en fabriquer une nouvelle et de faire pointer le nom dessus. L'ancienne est abandonnée.

mot = "pikachu"
mot = "P" + mot[1:]     # fabrique une NOUVELLE chaîne, mot pointe dessus
avant :   mot ──▶ "pikachu"
après :   mot ──▶ "Pikachu"        ("pikachu" est abandonnée)

Cas mutable. On peut changer l'objet lui-même. Le nom continue de pointer au même endroit, mais ce qui s'y trouve a changé.

notes = [16, 12, 18]
notes[1] = 20           # modifie l'objet SUR PLACE, aucun nouvel objet
avant :   notes ──▶ [16, 12, 18]
après :   notes ──▶ [16, 20, 18]   (même objet, contenu changé)

La conséquence, qui est la source de bugs numéro un

Si deux noms pointent vers la même séquence mutable, modifier par l'un se voit par l'autre. Tu l'as vu sur les listes avec b = a.

Sur une séquence immuable, ce piège n'existe pas : puisqu'on ne peut rien modifier sur place, personne ne peut te changer ta chaîne dans le dos.

a = [1, 2]; b = a; b.append(3)
print(a)              # [1, 2, 3]   la liste est partagée

x = "ab";   y = x;   y = y + "c"
print(x)              # "ab"        impossible de toucher à x

C'est pour cela qu'on choisit un tuple plutôt qu'une liste quand une donnée ne doit pas changer : ce n'est pas une coquetterie, c'est une protection.

Prédire avant d'exécuter

Sans lancer le code, écris ce qui s'affiche, puis vérifie.

a = [1, 2, 3]
b = a
c = list(a)
b[0] = 99
print(a, b, c)

s = "abc"
t = s
t = t.upper()
print(s, t)
Réponse

[99, 2, 3] [99, 2, 3] [1, 2, 3] puis abc ABC.

Dans le premier cas, b et a sont un seul objet, c en est une copie faite avant la modification. Dans le second, upper() ne modifie pas s : elle ne peut pas, une chaîne est immuable. Elle en renvoie une nouvelle, qu'on range dans t.

Extraire un morceau : le slice

L'écriture seq[debut:fin] renvoie la portion allant de debut inclus à fin exclus. C'est le même intervalle semi-ouvert que range, pour la même raison : la longueur du morceau vaut exactement fin - debut.

t = ("A", "B", "C", "D", "E", "F")
print(t[1:4])       # ('B', 'C', 'D')   de 1 à 3
print("ABCDEF"[3:]) # 'DEF'             de 3 à la fin
print([0,1,2,3][:2])# [0, 1]            du début à 1

Le slice renvoie toujours une nouvelle séquence, du même type que l'originale. C'est d'ailleurs une façon courante de copier une liste : b = a[:].

Une séquence peut en contenir une autre : l'imbrication

Ce n'est pas une fonctionnalité de plus, c'est une conséquence de ce qui précède. Si tokens[4] vaut "suite", alors tokens[4] est une chaîne, donc une séquence, donc on peut encore lui mettre des crochets : tokens[4][2] vaut "i".

tokens = ["Une", "Séquence", "est", "une", "suite", "d'éléments"]
print(tokens[4][2])     # "i"

C'est ce qui permettra de représenter un tableau de données : une liste de lignes, chaque ligne étant elle-même une liste. Tu t'en serviras dans le thème des données en tables.

Imbrication, exercice guidé

mots = [
    ["bon", "jour"],
    ["mi", "di"],
    ["chou", "ette"]
]
  1. Combien d'éléments a mots ?
  2. Quel est le type complet de mots ?
  3. Affiche le "u" de "jour".
  4. Affiche le "m" de "mi".
  5. Affiche le "u" de "chou".
  6. Affiche la chaîne "chouette" en concaténant deux éléments de mots.
Indice

Lis les crochets de gauche à droite, un par un. mots[0] est une liste. mots[0][1] est une chaîne. mots[0][1][2] est un caractère. À chaque crochet, on descend d'un niveau.

Solutions
  1. 3 éléments (trois listes internes).
  2. list[list[str]], une liste de listes de chaînes.
  3. print(mots[0][1][2])
  4. print(mots[1][0][0])
  5. print(mots[2][0][3])
  6. print(mots[2][0] + mots[2][1])

Imbrication, exercice autonome

struct = (
    "root",
    ("A", ["B", "C"], "E"),
    ("F", (4, 9), "J"),
    ("K", (2, "Mot"), ["N", "O"])
)
  1. Combien d'éléments a struct ?
  2. Quel est le type complet de struct ?
  3. Affiche "root", puis "J", puis "N", puis le "o" de "Mot".
Solutions
  1. 4 éléments.
  2. tuple[str, tuple[str, list[str], str], tuple[str, tuple[int, int], str], tuple[str, tuple[int, str], list[str]]]
  3. struct[0], struct[2][2], struct[3][2][0], struct[3][1][1][1].

Deux pièges à connaître par leur nom

Piège : confondre l'indice et la valeur

L'indice dit est l'élément, la valeur dit ce qu'il est. Les crochets attendent un indice, l'opérateur in une valeur.

notes = [15, 12, 18]
print(notes[15])      # IndexError : 15 est une valeur, pas une position
print(notes[0])       # 15 : accès par indice
print(15 in notes)    # True : test sur une valeur

C'est une confusion classique et tenace, y compris chez des élèves qui savent par ailleurs très bien programmer. Quand une IndexError te surprend, pose-toi d'abord cette question-là.

Piège : utiliser len(seq) comme indice

Les indices vont de 0 à len(seq) - 1. L'indice len(seq) n'existe donc jamais.

lst = [10, 20, 30]
print(lst[len(lst)])       # IndexError
print(lst[len(lst) - 1])   # 30, le dernier
print(lst[-1])             # 30, plus lisible

C'est la même erreur, décalée d'un cran, que celle qu'on fait en écrivant range(1, n) quand on voulait aller jusqu'à n. Le décalage de un est l'erreur la plus fréquente de toute la programmation, et elle ne se corrige pas en faisant attention : elle se corrige en traçant le premier et le dernier tour.

Résumé

Type Exemple Mutable ? Quand l'utiliser
str "Hello" non du texte
tuple (1, 2, 3) non des données qui ne doivent pas changer, un retour multiple
list [1, 2, 3] oui des données qu'on construit ou qu'on modifie

À retenir, et c'est tout :

  • les indices vont de 0 à len(seq) - 1 ;
  • l'accès, la longueur, l'appartenance, le parcours et le slice s'écrivent pareil pour les trois ;
  • seule la liste est modifiable sur place, ce qui fait sa puissance et son seul danger.

Et la suite ?

La structure que tu vas rencontrer juste après, le dictionnaire, n'est pas une séquence. Il n'a pas d'indices, et on n'y accède pas par une position mais par une clé. C'est le premier objet du cours auquel ce tableau ne s'applique pas, et c'est pour cela qu'il faut d'abord bien tenir ce qui précède.

Pour s'entraîner

Les exercices sur les séquences, ainsi que les fonctions à écrire, se trouvent sur la page Exercices sur les séquences.