Objectifs du chapitre
À la fin de ce chapitre, vous serez capable de:
- construire une liste par littéral, par
list(range(...))ou par répétition, et dire pourquoi une liste hétérogène est légale mais rarement souhaitable; - indexer et découper une liste comme vous le faisiez avec une chaîne, et exploiter la différence décisive: une liste est modifiable;
- expliquer, dessin à l'appui, pourquoi
liste2 = liste1ne fabrique pas une deuxième liste, et copier une liste de trois façons correctes; - utiliser
append,insert,extend,remove,pop,index,count,sortetreverseen sachant lesquelles modifient la liste sur place et ce qu'elles retournent; - parcourir une liste avec
for, avecrange(len(...))ou avecenumerate, et éviter le piège de la modification pendant l'itération; - écrire une liste en compréhension, manipuler une liste de listes et savoir quand un tuple vaut mieux qu'une liste.
Dix variables, ou une seule liste
Au chapitre 3, pour calculer la moyenne des dix notes du cours, vous avez écrit dix variables et vous les avez additionnées à la main. Voici ce programme, tel qu'il était.
# Chapitre 3: dix notes, dix variables
note1 = 4.5
note2 = 5.0
note3 = 3.5
note4 = 6.0
note5 = 4.0
note6 = 5.5
note7 = 4.5
note8 = 3.0
note9 = 5.0
note10 = 4.5
somme = (note1 + note2 + note3 + note4 + note5
45.5
4.55
Le résultat est juste, et le programme est mauvais. Il est mauvais pour une raison que l'on peut énoncer précisément: le nombre de lignes du programme dépend du nombre de notes. Ajoutez un onzième élève et il faut modifier trois endroits — la déclaration, l'addition, le diviseur. Passez à cent élèves et le programme fait cent vingt lignes; à mille, personne ne l'écrit. Pire: les dix noms note1, note2, …, note10 ressemblent à une suite, mais Python ne le sait pas. Pour lui ce sont dix variables sans rapport, exactement comme prenom, masse et couleur. Aucune boucle ne peut les parcourir, car il n'existe aucune façon de dire «la variable dont le nom se termine par le numéro du tour».
Ce que nous voulons, c'est un objet unique qui contienne les dix valeurs dans un ordre et qui sache combien il en contient. Cet objet existe et s'appelle une liste.
Voici les mêmes dix notes, écrites une fois pour toutes.
# Les memes dix notes, dans une seule liste
notes = [4.5, 5.0, 3.5, 6.0, 4.0, 5.5, 4.5, 3.0, 5.0, 4.5]
print(notes)
print(len(notes))
print(sum(notes))
print(sum(notes) / len(notes))
[4.5, 5.0, 3.5, 6.0, 4.0, 5.5, 4.5, 3.0, 5.0, 4.5]
10
45.5
4.55
Quatre lignes au lieu de quinze, et surtout: aucune ligne ne dépend du nombre de notes. Remplacez le littéral par mille valeurs, le reste du programme ne bouge pas. len(notes) compte pour vous, sum(notes) additionne pour vous. C'est le premier bénéfice de la structure de données: elle déplace la répétition du texte du programme vers l'exécution du programme.
Gardez cette liste en tête, c'est le fil rouge du cours: [4.5, 5.0, 3.5, 6.0, 4.0, 5.5, 4.5, 3.0, 5.0, 4.5], dix notes fictives d'une classe imaginaire, notées sur l'échelle suisse de 1 à 6 où l'on réussit à partir de 4. Au chapitre 3 elles étaient dix variables; ici elles deviennent une liste; au chapitre 7 elles seront associées aux noms des élèves. Nous les reprendrons à la fin du chapitre pour en calculer toutes les statistiques d'un coup.
Construire une liste
Le littéral
La façon la plus directe consiste à énumérer les éléments entre crochets. Une liste peut être vide, auquel cas les crochets sont vides. Attention: une liste vide n'est pas «rien»; c'est une liste parfaitement utilisable, dont la longueur vaut zéro, et c'est de très loin le point de départ le plus fréquent d'un programme qui construit un résultat.
À partir d'un intervalle ou d'une chaîne
Au chapitre 3, range(5) vous a servi à compter les tours d'une boucle. Un range n'est pas une liste — print(range(5)) affiche range(0, 5) et non les valeurs — mais la fonction list le transforme en liste. De même, list appliquée à une chaîne de caractères produit la liste de ses caractères, un par case. Enfin, l'opérateur * répète une liste, ce qui est la manière habituelle de fabriquer une liste de compteurs tous à zéro.
vide = []
entiers = list(range(5))
pairs = list(range(0, 11, 2))
lettres = list("Python")
zeros = [0] * 5
print(vide, len(vide))
print(entiers)
print(pairs)
[] 0
[0, 1, 2, 3, 4]
[0, 2, 4, 6, 8, 10]
['P', 'y', 't', 'h', 'o', 'n']
[0, 0, 0, 0, 0]
Observez la troisième ligne de la sortie: list(range(0, 11, 2)) s'arrête à 10 et non à 12, parce que range exclut sa borne de fin, comme au chapitre 3. Observez la quatrième: Python affiche les caractères entre apostrophes, parce que ce sont des chaînes; les nombres, eux, s'affichent sans apostrophes. Cette différence d'affichage est votre premier outil de diagnostic: si vous voyez ['4.5', '5.0'] au lieu de [4.5, 5.0], vous avez une liste de chaînes qui ressemblent à des nombres, et aucune addition ne fonctionnera dessus.
Listes hétérogènes
Rien n'oblige les éléments d'une liste à être du même type. Une liste peut contenir un entier, une chaîne, un flottant, un booléen et même une autre liste.
melange = [42, "Alice", 4.5, True, [1, 2]]
print(melange)
print(len(melange))
print(type(melange[0]), type(melange[1]))
[42, 'Alice', 4.5, True, [1, 2]]
5
<class 'int'> <class 'str'>
Notez que len(melange) vaut 5 et non 6: la sous-liste [1, 2] compte pour un élément, comme un carton fermé compte pour un colis quel que soit son contenu. Nous reviendrons longuement sur ces listes de listes.
Légal, donc. Souhaitable, presque jamais. Une liste sert à représenter plusieurs choses de même nature — dix notes, sept températures, les mots d'une phrase — sur lesquelles on veut faire le même traitement. Dès que les éléments ne sont plus de même nature, le traitement uniforme devient impossible, et c'est Python qui vous le rappelle:
melange = [42, "Alice", 4.5]
print(sum(melange))
Traceback (most recent call last):
File "notes.py", line 2, in <module>
print(sum(melange))
~~~^^^^^^^^^
TypeError: unsupported operand type(s) for +: 'int' and 'str'
Lisons cette trace ligne par ligne, comme au chapitre 1. La première annonce qu'il y a eu une erreur et que ce qui suit est la pile des appels. La deuxième situe le coupable: fichier notes.py, ligne 2, dans le corps principal du programme (<module> signifie «pas à l'intérieur d'une fonction»). La troisième recopie la ligne fautive, et la quatrième la souligne: les tildes indiquent l'expression concernée, les accents circonflexes la partie exactement responsable. La dernière ligne donne le type de l'erreur, TypeError, et son message: l'opérateur + ne sait pas additionner un entier et une chaîne. sum additionne les éléments un par un; arrivé à "Alice", il tombe sur une addition impossible.
Quand vous avez vraiment plusieurs informations de natures différentes à propos d'un même objet — un nom, un âge, une note — ce n'est pas une liste qu'il vous faut. Ce sera un tuple, à la fin de ce chapitre, ou un dictionnaire, au chapitre 7.
Que vaut len(list(range(2, 12, 3)))?
Indexation et tranches
Les mêmes règles qu'au chapitre 5
Tout ce que vous avez appris au chapitre 5 sur l'indexation et le découpage des chaînes de caractères s'applique aux listes, sans la moindre exception. Plutôt que de le répéter, rappelons-en la liste et faisons-la fonctionner sur les notes:
liste[i]donne l'élément d'indicei, le premier ayant l'indice0;- un indice négatif compte depuis la fin,
liste[-1]désignant le dernier élément; liste[a:b]donne la tranche des éléments d'indiceainclus àbexclu;- une borne omise vaut «depuis le début» ou «jusqu'à la fin»;
liste[a:b:p]avance depenp, etliste[::-1]renverse;- une tranche dont les bornes sont incohérentes donne non pas une erreur, mais une liste vide.
notes = [4.5, 5.0, 3.5, 6.0, 4.0, 5.5, 4.5, 3.0, 5.0, 4.5]
print(notes[0])
print(notes[3])
print(notes[-1])
print(notes[-2])
print(notes[0:3
4.5
6.0
4.5
5.0
[4.5, 5.0, 3.5]
[4.5, 5.0, 3.5, 6.0, 4.0]
[5.5, 4.5, 3.0, 5.0, 4.5]
[4.5, 3.5, 4.0, 4.5, 5.0]
[4.5, 5.0, 3.0, 4.5, 5.5, 4.0, 6.0, 3.5, 5.0, 4.5]
[]
Une seule différence de nature avec les chaînes: notes[0] est un nombre, alors que chaine[0] était une chaîne d'un caractère. Une tranche, en revanche, reste du même type que l'objet découpé: notes[0:3] est une liste, même quand elle ne contient qu'un élément.
Retenez la règle de longueur, qui évite la moitié des erreurs d'indice: la tranche liste[a:b] a b - a éléments quand les bornes sont dans la liste. notes[0:3] en a trois, notes[:5] en a cinq, notes[7:3] en aurait -4, donc zéro.
L'erreur d'indice
Un indice hors des bornes ne donne pas une valeur par défaut: il arrête le programme.
notes = [4.5, 5.0, 3.5]
print(notes[3])
Traceback (most recent call last):
File "notes.py", line 2, in <module>
print(notes[3])
~~~~~^^^
IndexError: list index out of range
«list index out of range»: l'indice de liste est hors de l'intervalle. La liste a trois éléments, donc les indices valides sont 0, 1 et 2 — et, du côté négatif, -1, -2 et -3. L'indice 3 désigne le quatrième élément d'une liste qui n'en a que trois. C'est l'erreur dite off-by-one, l'erreur d'un cran, rencontrée au chapitre 3 avec range: le dernier indice d'une liste de n éléments est n - 1, jamais n.
La différence décisive: une liste est modifiable
Au chapitre 5, vous avez appris qu'une chaîne est immuable: on peut la lire caractère par caractère, mais pas en remplacer un.
chaine = "Python"
chaine[0] = "J"
Traceback (most recent call last):
File "notes.py", line 2, in <module>
chaine[0] = "J"
~~~~~~^^^
TypeError: 'str' object does not support item assignment
Avec une liste, exactement la même instruction fonctionne.
notes = [4.5, 5.0, 3.5]
notes[0] = 5.5
print(notes)
[5.5, 5.0, 3.5]
Cette phrase — «cela change celle qui existe, à la place où elle est» — a l'air anodine. C'est la source de la moitié des bugs de ce chapitre, et nous allons y consacrer la section suivante.
On peut aussi affecter à une tranche entière, ce qui permet de remplacer plusieurs éléments d'un coup, par un nombre différent d'éléments, voire par aucun.
notes = [4.5, 5.0, 3.5, 6.0, 4.0]
notes[1:3] = [1.0, 2.0, 3.0]
print(notes)
notes[0:2] = []
print(notes)
[4.5, 1.0, 2.0, 3.0, 6.0, 4.0]
[2.0, 3.0, 6.0, 4.0]
Deux éléments ont été remplacés par trois, et la liste s'est allongée; puis deux éléments ont été remplacés par zéro, et elle a raccourci. C'est puissant et c'est illisible: en pratique, préférez les méthodes de la section «Les méthodes des listes», dont le nom dit ce qu'elles font.
Soit notes = [4.5, 5.0, 3.5, 6.0, 4.0, 5.5, 4.5, 3.0, 5.0, 4.5]. Que vaut notes[2] + notes[-1]?
Mutabilité et aliasing: le cœur du chapitre
Un nom n'est pas une valeur
Depuis le chapitre 1, vous vous représentez une variable comme une étiquette collée sur une valeur. Tant que les valeurs sont des nombres, cette image ne peut pas vous tromper: écrire b = a puis b = b + 1 laisse a intact, parce que l'addition fabrique un nouveau nombre et que l'étiquette b déménage dessus.
Avec une liste, l'image reste juste mais ses conséquences changent, parce que la liste, elle, peut être modifiée sans être remplacée. Lisez ce programme, et prédisez sa sortie avant de la regarder.
liste1 = [1, 2, 3]
liste2 = liste1 # un deuxieme nom, pas une deuxieme liste
liste2.append(4)
print(liste1)
print(liste2)
print(liste1 is liste2)
[1, 2, 3, 4]
[1, 2, 3, 4]
True
liste1 a changé alors que le programme ne la nomme jamais entre son initialisation et son affichage. Rien n'est cassé: c'est le comportement normal et documenté de Python. L'affectation liste2 = liste1 ne copie pas la liste. Elle colle une deuxième étiquette sur la liste qui existe déjà. Il y a deux noms et un seul objet; append modifie cet objet; les deux noms le désignent toujours.
La distinction se voit d'un coup avec deux littéraux écrits séparément:
liste1 = [1, 2, 3]
liste2 = [1, 2, 3] # deux listes distinctes, de meme contenu
liste2.append(4)
print(liste1)
print(liste2)
print(liste1 is liste2)
print([1, 2, 3] == [1, 2, 3
[1, 2, 3]
[1, 2, 3, 4]
False
True
Ici chaque littéral a construit sa propre liste, liste1 ne bouge pas, et is répond False bien que les contenus aient été identiques au départ.
Ce dessin est à faire vous-même, sur papier, chaque fois qu'un programme vous surprend. Une flèche par nom, une boîte par objet: si deux flèches arrivent dans la même boîte, toute modification de la boîte se voit par les deux noms.
Quand cela devient un bug
Tant que l'on écrit liste2 = liste1 sur deux lignes voisines, l'alias saute aux yeux. Le vrai piège est ailleurs: passer une liste à une fonction crée exactement le même alias. Le paramètre est un nouveau nom collé sur la liste de l'appelant, pas une copie.
def ajoute_bonus(liste, bonus):
"""Version fautive: elle modifie la liste recue."""
for i in range(len(liste)):
liste[i] = liste[i] + bonus
return liste
notes = [4.5, 5.0, 3.5]
gonflees = ajoute_bonus(notes, 0.5)
print(gonflees)
print(notes)
print(gonflees is notes)
[5.0, 5.5, 4.0]
[5.0, 5.5, 4.0]
True
L'appelant croyait demander une nouvelle liste augmentée du bonus; il a reçu sa propre liste, abîmée. Les notes d'origine sont perdues, et le is final le confirme: gonflees et notes sont un seul objet. Une fonction qui modifie l'objet qu'on lui confie doit l'annoncer dans son nom et dans sa docstring — et, dans ce cas, ne rien retourner, pour ne pas laisser croire qu'elle fabrique un résultat neuf.
Les trois façons de copier
import copy
original = [1, 2, 3]
copie_a = original[:] # une tranche complete
copie_b = list(original) # le constructeur
copie_c = copy.copy(original) # le module copy
copie_a.append(4)
copie_b.append(5)
copie_c.append(6)
print(original)
print(copie_a, copie_b, copie_c)
print(original is
[1, 2, 3]
[1, 2, 3, 4] [1, 2, 3, 5] [1, 2, 3, 6]
False False False
Les trois procédés donnent le même résultat et original ne bouge pas. Lequel utiliser? liste[:] est le plus court et le plus fréquent dans le code existant, mais il est cryptique pour qui débute; list(liste) dit ce qu'il fait et c'est celui que je vous recommande; copy.copy demande un import, donc le chapitre 8, et ne se justifie que dans un programme qui copie déjà d'autres sortes d'objets. Autrement dit: pour copier une liste de nombres ou de chaînes, vous n'avez besoin d'aucun module — list(liste) suffit et suffira toute l'année.
Une copie de surface ne suffit pas toujours
Les trois procédés ci-dessus font une copie de surface (shallow copy): ils construisent une nouvelle liste dont les cases contiennent les mêmes objets que l'originale. Quand les éléments sont des nombres, cela ne se remarque pas, puisqu'un nombre est immuable et qu'on ne peut donc pas le modifier. Quand les éléments sont eux-mêmes des listes, cela se remarque brutalement.
matrice = [[1, 2], [3, 4]]
copie = matrice[:] # copie de surface
copie[0][0] = 99
print(matrice)
print(copie)
print(matrice[0] is copie[0])
[[99, 2], [3, 4]]
[[99, 2], [3, 4]]
True
La liste extérieure a bien été dupliquée — matrice is copie serait False — mais ses deux cases pointent vers les mêmes sous-listes que l'originale. Modifier copie[0][0], c'est modifier une sous-liste partagée. Il faut alors une copie en profondeur (deep copy), qui duplique récursivement tout ce qu'elle rencontre.
import copy
matrice = [[1, 2], [3, 4]]
copie = copy.deepcopy(matrice) # copie en profondeur
copie[0][0] = 99
print(matrice)
print(copie)
print(matrice[0] is copie[0])
[[1, 2], [3, 4]]
[[99, 2], [3, 4]]
False
copy.deepcopy est la bonne réponse quand vous voulez une copie vraiment indépendante d'une structure imbriquée. Elle coûte plus cher que list(...), puisqu'elle recopie tout: ne l'appelez pas dans une boucle serrée sans y avoir réfléchi. C'est la deuxième et dernière ligne de ce chapitre qui emprunte au chapitre 8; retenez son nom plutôt que son mécanisme, et sachez surtout reconnaître la situation qui l'exige: une liste dont les éléments sont eux-mêmes modifiables.
Laquelle de ces quatre instructions ne crée pas une nouvelle liste?
La sauvegarde est un alias: la modification l'atteint aussi. Faites-en une vraie copie, pour que le programme affiche la liste d'origine, puis la liste modifiée, puis False.
Les méthodes des listes
Une méthode (method) est une fonction attachée à un objet et appelée avec la notation pointée, objet.methode(...), comme les méthodes de chaîne du chapitre 5. La différence tient à la mutabilité: les méthodes de chaîne ne pouvaient que retourner une nouvelle chaîne, alors que la plupart des méthodes de liste modifient la liste sur place.
Ajouter
append(x) ajoute x à la fin; insert(i, x) l'insère à l'indice i, en décalant d'un cran tout ce qui suivait; extend(autre) ajoute tous les éléments d'une autre liste, un par un.
notes = [4.5, 5.0, 3.5]
notes.append(6.0) # ajoute a la fin
print(notes)
notes.insert(1, 4.0) # insere a l'indice 1
print(notes)
notes.extend([5.5, 3.0]) # ajoute tous les elements
print(notes)
print(len(notes))
[4.5, 5.0, 3.5, 6.0]
[4.5, 4.0, 5.0, 3.5, 6.0]
[4.5, 4.0, 5.0, 3.5, 6.0, 5.5, 3.0]
7
Ne confondez pas append et extend: notes.append([5.5, 3.0]) aurait ajouté un élément, qui se trouve être une liste, et len vaudrait 6 avec une sous-liste à la fin. extend ajoute deux éléments.
Retirer
remove(x) retire la première occurrence de la valeur x — pas toutes, et par valeur, pas par indice. pop() retire le dernier élément et le retourne; pop(i) retire et retourne celui d'indice i.
notes = [4.5, 4.0, 5.0, 3.5, 6.0, 5.5, 3.0]
notes.remove(3.5) # retire la premiere occurrence de 3.5
print(notes)
derniere = notes.pop() # retire et retourne le dernier
print(derniere, notes)
premiere = notes.pop(0) # retire et retourne celui d'indice 0
print(premiere, notes)
[4.5, 4.0, 5.0, 6.0, 5.5, 3.0]
3.0 [4.5, 4.0, 5.0, 6.0, 5.5]
4.5 [4.0, 5.0, 6.0, 5.5]
pop est la seule méthode de retrait qui rende quelque chose d'utile: elle sert à consommer une liste élément par élément, ce qui en fait l'outil naturel d'une pile (chapitre 9) ou d'une file d'attente.
Chercher et compter
index(x) donne l'indice de la première occurrence de x; count(x) compte les occurrences; l'opérateur in, déjà rencontré au chapitre 5 sur les chaînes, teste l'appartenance.
notes = [4.5, 5.0, 4.5, 6.0, 4.5]
print(notes.index(6.0))
print(notes.count(4.5))
print(4.5 in notes)
print(2.0 in notes)
3
3
True
False
index sur une valeur absente ne retourne pas -1, comme dans d'autres langages: elle lève une erreur.
notes = [4.5, 5.0, 4.5, 6.0, 4.5]
print(notes.index(2.0))
Traceback (most recent call last):
File "notes.py", line 2, in <module>
print(notes.index(2.0))
~~~~~~~~~~~^^^^^
ValueError: 2.0 is not in list
ValueError signifie: le type de l'argument est bon, mais sa valeur ne convient pas. La parade tient en une ligne: testez if x in notes: avant d'appeler index.
Trier et renverser
sort() trie la liste sur place, dans l'ordre croissant; sort(reverse=True) dans l'ordre décroissant; reverse() renverse l'ordre des éléments sans les comparer.
notes = [4.5, 5.0, 3.5, 6.0, 4.0]
notes.sort()
print(notes)
notes.sort(reverse=True)
print(notes)
notes.reverse()
print(notes)
[3.5, 4.0, 4.5, 5.0, 6.0]
[6.0, 5.0, 4.5, 4.0, 3.5]
[3.5, 4.0, 4.5, 5.0, 6.0]
Et voici, très exactement, le programme qui détruit les données d'un étudiant sur deux, chaque année, dans chaque cours de programmation.
notes = [4.5, 5.0, 3.5, 6.0, 4.0]
notes = notes.sort() # ERREUR: sort() ne retourne rien
print(notes)
print(len(notes))
None
Traceback (most recent call last):
File "notes.py", line 4, in <module>
print(len(notes))
~~~^^^^^^^
TypeError: object of type 'NoneType' has no len()
Décortiquons. notes.sort() a bel et bien trié la liste, sur place. Puis elle a retourné None, la valeur spéciale qui signifie «rien à retourner» (chapitre 4). L'affectation notes = ... a alors collé l'étiquette notes sur ce None — et la liste triée, que plus aucun nom ne désigne, a été effacée de la mémoire. La liste n'est pas «devenue None»: elle a disparu, et notes désigne autre chose. La ligne suivante demande la longueur de None, d'où le TypeError final, dont le message est presque littéral: un objet de type NoneType n'a pas de longueur.
Quand vous voulez une liste triée sans toucher à l'originale, c'est la fonction native sorted qu'il faut, et elle, elle retourne une nouvelle liste.
notes = [4.5, 5.0, 3.5, 6.0, 4.0]
triees = sorted(notes) # une nouvelle liste
print(triees)
print(notes) # l'originale est intacte
print(sorted(notes, reverse=True))
[3.5, 4.0, 4.5, 5.0, 6.0]
[4.5, 5.0, 3.5, 6.0, 4.0]
[6.0, 5.0, 4.5, 4.0, 3.5]
Le tableau ci-dessous résume les neuf méthodes à connaître. La colonne «retourne» est celle qu'il faut apprendre par cœur.
| Appel | Effet sur la liste | Retourne |
|---|---|---|
l.append(x) | ajoute x à la fin | None |
l.insert(i, x) | insère x à l'indice i, décale la suite | None |
l.extend(autre) | ajoute tous les éléments de autre | None |
l.remove(x) | retire la première occurrence de x | None |
l.pop() / l.pop(i) | retire le dernier / celui d'indice i | l'élément retiré |
l.index(x) | aucun | l'indice de la première occurrence |
l.count(x) | aucun | le nombre d'occurrences |
l.sort() | trie sur place | None |
l.reverse() | renverse sur place | None |
La liste de départ est toujours notes = [4.5, 5.0, 3.5, 6.0, 4.0]. Choisissez une opération et son argument: les cases qui ont changé de contenu sont en couleur, et le readout «valeur retournée» donne ce que l'appel rend à l'appelant — None pour toutes les méthodes qui modifient la liste sur place.
Prenez deux minutes avec cet explorateur: faites défiler les six opérations et regardez le readout «valeur retournée». Il affiche None cinq fois sur six, et c'est là toute la leçon de cette section. Poussez ensuite l'indice à 5 avec pop, ou la valeur à 5.5 avec remove: la liste de départ ne contient ni l'indice 5 ni la valeur 5,5, et l'explorateur affiche l'erreur que Python lèverait, mot pour mot.
Parcourir une liste
Les trois formes de boucle
La forme la plus simple, et celle qu'il faut utiliser par défaut, énumère directement les éléments.
notes = [4.5, 5.0, 3.5, 6.0, 4.0, 5.5, 4.5, 3.0, 5.0, 4.5]
total = 0.0
for note in notes: # note prend successivement chaque valeur
total = total + note
print(total)
print(total / len(notes))
45.5
4.55
Comparez avec le programme du début du chapitre: l'accumulateur du chapitre 3 est resté le même, mais les dix lignes d'addition ont disparu. La variable note prend successivement chaque valeur de la liste; il n'y a plus aucun indice à gérer, donc plus aucune erreur d'indice possible.
La deuxième forme parcourt les indices et non les valeurs. Elle est indispensable dès que l'on veut écrire dans la liste, car une case ne peut être changée que par son indice.
notes = [4.5, 5.0, 3.5]
for i in range(len(notes)): # i vaut 0, puis 1, puis 2
notes[i] = notes[i] + 0.5 # on ecrit dans la liste: il faut l'indice
print(notes)
[5.0, 5.5, 4.0]
Pourquoi l'indice est-il indispensable ici? Parce que la première forme ne donne accès qu'à une copie du lien vers la valeur, et qu'une affectation à la variable de boucle déplace simplement cette variable:
notes = [4.5, 5.0, 3.5]
for note in notes:
note = note + 0.5 # on modifie la variable, pas la liste
print(notes)
[4.5, 5.0, 3.5]
Rien n'a changé, et aucun message d'erreur ne vous a prévenu. note = note + 0.5 fabrique un nouveau nombre et y colle l'étiquette note; au tour suivant, cette étiquette est de toute façon replacée sur l'élément suivant. La liste n'a jamais été touchée. C'est le même phénomène qu'au début du chapitre, vu dans l'autre sens: un nombre est immuable, donc on ne peut pas le modifier, seulement le remplacer — et remplacer une variable ne remplace pas la case de la liste.
La troisième forme sert quand vous avez besoin des deux à la fois, la valeur et sa position. enumerate(liste) produit à chaque tour un couple (indice, valeur), que l'on récupère par dépaquetage dans deux variables.
notes = [4.5, 5.0, 3.5]
for rang, note in enumerate(notes, start=1):
print(f"Copie {rang}: {note}")
Copie 1: 4.5
Copie 2: 5.0
Copie 3: 3.5
L'argument optionnel start=1 fait commencer le compteur à 1, ce qui convient quand le nombre est destiné à un humain — une copie, un rang, une ligne de tableau — alors que les indices, eux, commencent toujours à 0.
Le piège de la modification pendant l'itération
Voici un programme parfaitement lisible, qui a l'air de faire ce qu'il dit: retirer les notes insuffisantes.
notes = [4.5, 3.0, 3.5, 2.5, 5.0]
for note in notes:
if note < 4:
notes.remove(note) # PIEGE: on modifie la liste parcourue
print(notes)
[4.5, 3.5, 5.0]
Il reste 3.5, qui est pourtant insuffisante. Le programme n'a pas planté, n'a affiché aucun avertissement, et a produit un résultat faux. C'est la pire espèce de bug.
L'explication tient à la façon dont for parcourt une liste: il maintient un indice interne, invisible, qu'il incrémente à chaque tour, et il s'arrête lorsque cet indice atteint la longueur courante de la liste. Quand on retire un élément, tout ce qui suit glisse d'un cran vers la gauche — mais l'indice interne, lui, avance quand même. L'élément qui vient de prendre la place du supprimé est donc sauté. Instrumentons la boucle pour le voir:
notes = [4.5, 3.0, 3.5, 2.5, 5.0]
for indice, note in enumerate(notes):
print(indice, note, notes)
if note < 4:
notes.remove(note)
print(notes)
0 4.5 [4.5, 3.0, 3.5, 2.5, 5.0]
1 3.0 [4.5, 3.0, 3.5, 2.5, 5.0]
2 2.5 [4.5, 3.5, 2.5, 5.0]
[4.5, 3.5, 5.0]
La trace complète, tour par tour:
| Tour | Indice interne | Élément examiné | Liste au moment du test | Action |
|---|---|---|---|---|
| 1 | 0 | 4.5 | [4.5, 3.0, 3.5, 2.5, 5.0] | gardée |
| 2 | 1 | 3.0 | [4.5, 3.0, 3.5, 2.5, 5.0] | retirée; tout glisse d'un cran |
| 3 | 2 | 2.5 | [4.5, 3.5, 2.5, 5.0] | retirée; 3.5 n'a jamais été examinée |
| — | 3 | — | [4.5, 3.5, 5.0] | la liste n'a plus que 3 éléments: la boucle s'arrête |
Au tour 3, l'indice 2 ne désigne plus 3.5 mais 2.5, parce que la liste a raccourci: 3.5 est passée de l'indice 2 à l'indice 1, derrière le curseur, et ne sera plus jamais vue. Puis l'indice 3 dépasse la nouvelle longueur, 3, et la boucle s'arrête: 5.0 n'est même pas examinée — sans conséquence ici, puisqu'elle devait être gardée, mais le hasard n'est pas une méthode.
Première correction, la copie. Le [:] du for fabrique une liste temporaire figée: le parcours ne dépend plus des retraits.
notes = [4.5, 3.0, 3.5, 2.5, 5.0]
for note in notes[:]: # on parcourt une copie
if note < 4:
notes.remove(note) # on modifie l'originale
print(notes)
[4.5, 5.0]
Deuxième correction, la construction — celle que je vous recommande, car elle n'exige de retenir aucune subtilité. On ne supprime rien: on garde.
notes = [4.5, 3.0, 3.5, 2.5, 5.0]
reussites = []
for note in notes:
if note >= 4:
reussites.append(note)
print(reussites)
print(notes)
[4.5, 5.0]
[4.5, 3.0, 3.5, 2.5, 5.0]
Le résultat est le même, et la liste d'origine est conservée, ce qui est souvent un avantage en soi. Si vous voulez vraiment que le nom notes désigne la liste filtrée, ajoutez notes = reussites à la fin: cette fois l'affectation est correcte, puisqu'elle place l'étiquette sur un objet qui existe bel et bien.
Remettez dans l'ordre les étapes de la méthode sûre pour supprimer des éléments d'une liste.
Glissez les éléments pour les mettre dans le bon ordre
- Tester si l'élément courant doit être conservé
- Parcourir la liste d'origine, élément par élément
- Faire désigner par le nom d'origine la liste résultat, si besoin
- Créer une liste vide destinée à recevoir le résultat
- Ajouter avec append les éléments conservés à la liste résultat
Le programme veut retirer les notes insuffisantes et en oublie une, sans la moindre erreur. Réparez-le pour qu'il affiche exactement les notes suffisantes.
Listes en compréhension
Le schéma «liste vide, boucle, append» est si fréquent que Python offre une écriture condensée qui tient sur une ligne: la liste en compréhension (list comprehension).
carres = [n * n for n in range(1, 6)]
print(carres)
notes = [4.5, 5.0, 3.5, 6.0, 4.0, 5.5, 4.5, 3.0, 5.0, 4.5]
bonus = [note + 0.25 for note in
[1, 4, 9, 16, 25]
[4.75, 5.25, 3.75, 6.25, 4.25, 5.75, 4.75, 3.25, 5.25, 4.75]
[4.5, 5.0, 6.0, 4.0, 5.5, 4.5, 5.0, 4.5]
8
La dernière ligne donne au passage un des chiffres du cours: huit élèves sur dix ont la moyenne. Remarquez que [note for note in notes if note >= 4] est exactement la «deuxième correction» de la section précédente, en une ligne au lieu de quatre — même résultat, même sécurité, puisqu'on construit une liste neuve sans toucher à l'ancienne.
L'expression n'est pas obligée d'être arithmétique: elle peut appeler une méthode ou une fonction.
mots = ["python", "liste", "tuple"]
initiales = [mot[0].upper() for mot in mots]
print(initiales)
longueurs = [len(mot) for mot in mots]
print(longueurs)
longs = [mot for mot in mots if len(mot) > 5]
print
['P', 'L', 'T']
[6, 5, 5]
['python']
Combien d'éléments contient la liste [n for n in range(1, 31) if n % 3 == 0]?
Listes de listes
Une matrice
Un élément d'une liste pouvant être n'importe quoi, il peut être une liste. On obtient ainsi un tableau à deux dimensions: une liste de lignes, chaque ligne étant une liste de valeurs. L'accès se fait par double indexation: matrice[i] donne la ligne d'indice i, qui est une liste, et matrice[i][j] donne son élément d'indice j.
matrice = [[1, 2, 3], [4, 5, 6]]
print(matrice)
print(matrice[0]) # la premiere ligne, qui est une liste
print(matrice[0][2]) # le troisieme element de la premiere ligne
print(len(matrice)) # nombre de lignes
print(len(matrice[0])) # nombre de colonnes
matrice[1
[[1, 2, 3], [4, 5, 6]]
[1, 2, 3]
3
2
3
[[1, 2, 3], [40, 5, 6]]
Notez len(matrice) qui vaut 2, le nombre de lignes, et len(matrice[0]) qui vaut 3, le nombre de colonnes. L'ordre des indices est toujours le même: d'abord la ligne, ensuite la colonne, parce que matrice[i][j] se lit de gauche à droite comme «prends la ligne i, puis dans cette ligne prends l'élément j».
Le parcours d'une liste de listes se fait par deux boucles imbriquées: l'extérieure sur les lignes, l'intérieure sur les éléments de la ligne courante.
matrice = [[1, 2, 3], [4, 5, 6]]
total = 0
for ligne in matrice:
for valeur in ligne:
total = total + valeur
print(total)
for i in range(len(matrice)):
for j in range
21
matrice[0][0] = 1
matrice[0][1] = 2
matrice[0][2] = 3
matrice[1][0] = 4
matrice[1][1] = 5
matrice[1][2] = 6
Les deux formes coexistent ici pour la comparaison: la première somme les valeurs sans jamais nommer d'indice, la seconde a besoin des indices puisqu'elle les affiche. len(matrice[i]) plutôt que len(matrice[0]) est une précaution utile: rien n'oblige les lignes d'une liste de listes à avoir la même longueur.
Le piège de la ligne partagée
Pour fabriquer une grille de zéros, la répétition [0] * 3 a bien marché tout à l'heure. On est tenté de répéter une deuxième fois pour obtenir deux lignes.
grille = [[0] * 3] * 2 # PIEGE: deux fois la MEME ligne
print(grille)
grille[0][0] = 1
print(grille)
print(grille[0] is grille[1])
[[0, 0, 0], [0, 0, 0]]
[[1, 0, 0], [1, 0, 0]]
True
L'affichage initial est parfait, ce qui rend le piège redoutable: on croit avoir deux lignes. Une seule affectation révèle la vérité. [x] * 2 construit une liste de deux cases contenant le même objet x, pas deux copies de x; ici x est la liste [0, 0, 0], et grille[0] is grille[1] le confirme. Modifier «la première ligne» modifie la seule ligne qui existe, vue deux fois. C'est exactement l'aliasing de la figure 6.1, caché dans un opérateur qui a l'air innocent.
La construction correcte utilise une compréhension, qui réévalue [0] * 3 à chaque tour et fabrique donc une nouvelle ligne à chaque fois.
grille = [[0] * 3 for numero in range(2)] # une nouvelle ligne par tour
print(grille)
grille[0][0] = 1
print(grille)
print(grille[0] is grille[1])
[[0, 0, 0], [0, 0, 0]]
[[1, 0, 0], [0, 0, 0]]
False
La variable numero ne sert à rien d'autre qu'à compter les tours; les programmeurs expérimentés écrivent souvent _ à sa place, par convention, pour signaler qu'elle est ignorée.
Mettre un 1 dans la première case en met trois: les trois lignes sont la même liste, répétée. Construisez la grille autrement, pour que le programme affiche une seule case modifiée.
Les tuples
Une liste qu'on ne peut pas modifier
Indexation, tranches, len, in, count, index et le parcours par for fonctionnent sur un tuple exactement comme sur une liste.
point = (3, 4)
print(point)
print(point[0], point[1])
print(len(point))
print(point[0:1])
(3, 4)
3 4
2
(3,)
Ce qui ne fonctionne pas, c'est la modification:
point = (3, 4)
point[0] = 5
Traceback (most recent call last):
File "notes.py", line 2, in <module>
point[0] = 5
~~~~~^^^
TypeError: 'tuple' object does not support item assignment
Comparez ce message avec celui de la chaîne, plus haut: 'str' object does not support item assignment. Mot pour mot le même, au type près. Les trois types immuables du cours — chaîne, tuple, nombre — répondent de la même façon, et il n'y a donc qu'une phrase à reconnaître.
Pourquoi s'en priver serait dommage
À quoi bon un objet qui sait faire moins qu'une liste? Pour trois raisons, dans l'ordre d'importance.
La première: un tuple protège vos données. Si une valeur ne doit pas changer — les coordonnées d'un point de référence, les jours de la semaine, les bornes d'un intervalle — l'écrire dans un tuple transforme une erreur de logique silencieuse en une erreur bruyante, arrêtée sur la ligne fautive. C'est exactement ce que vous voulez.
La deuxième: un tuple est la façon naturelle de retourner plusieurs valeurs. Une fonction ne retourne qu'une seule valeur (chapitre 4); en faisant de cette valeur un tuple, elle en retourne plusieurs d'un coup, et l'appelant les récupère en une ligne.
def extremes(valeurs):
"""Retourne (minimum, maximum) d'une liste non vide."""
return min(valeurs), max(valeurs)
notes = [4.5, 5.0, 3.5, 6.0, 4.0, 5.5, 4.5, 3.0, 5.0, 4.5]
print(extremes(notes))
plus_basse, plus_haute = extremes(notes)
print(plus_basse, plus_haute)
(3.0, 6.0)
3.0 6.0
La ligne return min(valeurs), max(valeurs) n'a pas de parenthèses: la virgule suffit à fabriquer le tuple. La ligne plus_basse, plus_haute = extremes(notes) fait l'opération inverse, appelée dépaquetage (unpacking): elle distribue les éléments du tuple dans autant de variables. Le nombre de noms à gauche doit être exactement le nombre d'éléments à droite, faute de quoi Python lève un ValueError.
La troisième: un tuple peut servir de clé. Nous verrons au chapitre 7 que les clés d'un dictionnaire doivent être immuables — une liste ne peut donc pas être une clé, un tuple le peut. C'est ce qui permet d'indexer une valeur par un couple, par exemple une distance par une paire de villes.
Dépaquetage et échange
Le dépaquetage marche partout, et donne à Python un de ses idiomes les plus élégants: l'échange de deux variables sans variable temporaire.
a = 1
b = 2
a, b = b, a # echange par tuples
print(a, b)
sommet = (2.0, 3.0)
x, y = sommet # depaquetage
print(x, y)
liste = list(sommet)
print(liste, tuple(liste))
2 1
2.0 3.0
[2.0, 3.0] (2.0, 3.0)
Le membre de droite de a, b = b, a est d'abord évalué entièrement, produisant le tuple (2, 1); celui-ci est ensuite dépaqueté dans a et b. C'est parce que l'évaluation précède l'affectation que l'échange fonctionne sans perdre de valeur. Les deux dernières lignes montrent que list et tuple convertissent dans les deux sens, ce qui est la sortie de secours quand on a un tuple et qu'on a besoin de modifier: on convertit, on modifie, on reconvertit.
La virgule du tuple à un élément
solo = (4.5) # ce sont de simples parentheses
duo = (4.5,) # la virgule fait le tuple
print(type(solo))
print(type(duo))
print(len(duo))
sans_parentheses = 4.5, 5.0
print(sans_parentheses, type(sans_parentheses))
<class 'float'>
<class 'tuple'>
1
(4.5, 5.0) <class 'tuple'>
(4.5) n'est pas un tuple: les parenthèses y jouent leur rôle habituel de groupement, comme dans (2 + 3) * 4, et la valeur reste un flottant. Pour un tuple d'un seul élément il faut une virgule finale, (4.5,) — c'est d'ailleurs ainsi que Python l'affiche, comme on l'a vu avec point[0:1] plus haut. Et la dernière ligne le confirme dans l'autre sens: sans aucune parenthèse, 4.5, 5.0 est déjà un tuple. C'est la virgule qui fait le tuple, pas les parenthèses.
Quel est le type de la valeur de (4.5) en Python?
Liste ou tuple: comment choisir
Une règle pratique, qui vaut pour la plupart des programmes que vous écrirez cette année: une liste pour une collection homogène et variable, un tuple pour un enregistrement hétérogène et fixe. Dix notes forment une liste: elles sont toutes de même nature, on en ajoutera peut-être une onzième, on voudra les trier. Un point du plan est un tuple: l'abscisse et l'ordonnée ne sont pas interchangeables, on n'en ajoutera jamais une troisième au même point, et son identité tient à ses deux composantes.
Le carnet de notes, au complet
Réunissons tout. Le programme suivant calcule, sur les dix notes du cours, ce que l'on attend d'un tel jeu de données: effectif, somme, moyenne, médiane, minimum, maximum, et proportion d'élèves ayant la moyenne. Rappel: ces dix notes sont fictives et servent de fil rouge à tout le cours.
Voici la première moitié, avec les deux fonctions.
# Le carnet de notes du cours (donnees fictives)
notes = [4.5, 5.0, 3.5, 6.0, 4.0, 5.5, 4.5, 3.0, 5.0, 4.5]
def moyenne(valeurs):
"""Moyenne arithmetique d'une liste non vide."""
return sum(valeurs) / len(valeurs)
def mediane(valeurs):
"""Mediane d'une liste non vide, sans modifier la liste recue."""
triees =
La fonction mediane mérite qu'on s'y arrête, car elle utilise trois notions du chapitre. Elle appelle sorted et non sort: la liste de l'appelant ne doit surtout pas être réordonnée à son insu — c'est l'aliasing des arguments, vu plus haut. Elle utilise la division entière // du chapitre 1 pour trouver le milieu. Et elle distingue les deux cas: pour un effectif impair, la médiane est l'élément du milieu; pour un effectif pair, comme ici avec dix notes, c'est la moyenne des deux éléments centraux, d'indices milieu - 1 et milieu, c'est-à-dire 4 et 5.
Voici la seconde moitié, qui affiche le bulletin.
reussites = [note for note in notes if note >= 4]
taux = 100 * len(reussites) / len(notes)
print(f"Effectif : {len(notes)}")
print(f"Somme : {sum(notes)}")
print(f"Moyenne : {moyenne(notes)
Effectif : 10
Somme : 45.5
Moyenne : 4.55
Mediane : 4.5
Minimum : 3.0
Maximum : 6.0
Ont la moyenne : 8 sur 10 (80 %)
Liste triee : [3.0, 3.5, 4.0, 4.5, 4.5, 4.5, 5.0, 5.0, 5.5, 6.0]
Liste d'origine : [4.5, 5.0, 3.5, 6.0, 4.0, 5.5, 4.5, 3.0, 5.0, 4.5]
Si votre propre programme affiche autre chose que 45,5 · 4,55 · 4,5 · 3,0 · 6,0 · 8 sur 10, ce n'est pas le cours qui a raison contre vous: c'est qu'une de ces six valeurs est fausse quelque part, et il faut trouver laquelle. Le premier réflexe est de faire afficher sorted(notes) et len(notes).
Synthèse
- Une liste est une suite ordonnée d'éléments, écrite entre crochets, construite aussi par
list(range(...)),list("chaine")ou[0] * n. Elle rend le programme indépendant du nombre de données:lenetsumtravaillent pour vous. Elle peut être hétérogène, mais ne l'est presque jamais utilement. - L'indexation et les tranches obéissent aux règles des chaînes du chapitre 5 — indices à partir de 0, indices négatifs depuis la fin,
[a:b]avecbexclu,[::-1]pour renverser. La différence décisive est que la liste est modifiable:notes[0] = 5.5fonctionne là oùchaine[0] = "A"lève unTypeError. liste2 = liste1ne copie rien: c'est un alias, deux noms pour un objet, et cela vaut aussi pour un argument de fonction. Copier se demande explicitement —liste1[:],list(liste1)oucopy.copy(liste1)— et une copie de surface ne suffit pas pour une liste de listes, où il fautcopy.deepcopy.- Les méthodes
append,insert,extend,remove,pop,sortetreversemodifient la liste sur place; toutes retournentNonesaufpop, qui retourne l'élément retiré.notes = notes.sort()détruit donc les données; c'estsorted(notes)qu'il faut pour obtenir une nouvelle liste triée. - On parcourt avec
for element in listepour lire,for i in range(len(liste))pour écrire,enumeratequand on a besoin des deux. Ne jamais ajouter ni retirer pendant le parcours de la liste elle-même: le résultat est faux sans message d'erreur. On parcourt une copie, ou mieux, on construit une liste neuve — ce que la compréhension[x for x in liste if condition]fait en une ligne. - Une liste de listes se lit par double indexation
matrice[i][j], ligne d'abord;[[0] * 3] * 2ne duplique pas la ligne mais la référence, et se remplace par[[0] * 3 for numero in range(2)]. Un tuple est une liste immuable: il protège les données fixes, permet à une fonction de retourner plusieurs valeurs par dépaquetage, et servira de clé au chapitre 7 — et c'est la virgule, pas la parenthèse, qui le fabrique.
Qu'affiche x = [1, 2, 3]; y = x[:]; y.append(4); print(len(x))?
On reprend les dix notes fictives du cours, rangées dans une liste: notes = [4.5, 5.0, 3.5, 6.0, 4.0, 5.5, 4.5, 3.0, 5.0, 4.5]. Construisez, en quatre étapes, le programme qui affiche l'effectif, la moyenne, le nombre d'élèves ayant la moyenne et la médiane. À chaque étape, écrivez le code, exécutez-le, et comparez le résultat à celui qui vous est demandé.
Étape 1 — l'effectif
Écrivez la liste, puis affichez le nombre de notes qu'elle contient. Une seule fonction native suffit, et le programme ne doit pas contenir le nombre 10 en toutes lettres.
Que vaut len(notes)?
Étape 2 — la moyenne
Étape 3 — le nombre d'élèves ayant la moyenne
Étape 4 — la médiane
Exercices
Vous pouvez afficher le corrigé directement sous chaque énoncé après avoir cherché la solution.
Les moyennes journalières fictives d'une semaine, en degrés Celsius, sont [2.4, 5.1, 7.8, 6.2, 3.9, -0.5, 1.7].
Écrivez un programme qui affiche la moyenne de la semaine avec deux décimales, la température maximale, la température minimale et le nombre de jours de gel (température strictement négative). Le programme ne doit contenir aucun des nombres 7, 3.8, 7.8 ni −0.5 en dur.
Solution
temperatures = [2.4, 5.1, 7.8, 6.2, 3.9, -0.5, 1.7]
moyenne = sum(temperatures) / len
- Écrivez une fonction
inverser(liste)qui retourne une nouvelle liste contenant les mêmes éléments en ordre inverse, sans utiliserreverse,reversedni la tranche[::-1]. - Écrivez une fonction
sans_doublons(liste)qui retourne une nouvelle liste où chaque valeur n'apparaît qu'une fois, dans l'ordre de première apparition. - Vérifiez sur les dix notes du cours que la liste d'origine n'a pas bougé.
Solution
def inverser(liste):
"""Retourne une nouvelle liste, en ordre inverse."""
resultat = []
for element in liste:
resultat.insert(0, element)
return resultat
def sans_doublons(liste):
On veut une fonction qui ajoute un bonus à toutes les notes.
- Exécutez mentalement le programme suivant, puis dites ce qu'affichent ses trois
print.
def ajoute_bonus(liste, bonus):
for i in range(len(liste)):
liste[i] = liste[i] + bonus
return liste
notes = [4.5, 5.0, 3.5]
gonflees = ajoute_bonus(notes, 0.5)
print
Une matrice est représentée par une liste de lignes, chaque ligne étant une liste de nombres de même longueur.
- Écrivez une fonction
transposee(matrice)qui retourne la matrice transposée: la ligneide la transposée est la colonneide l'originale. Construisez le résultat correctement, sans tomber dans le piège de la ligne partagée. - Affichez la somme de chaque ligne de la matrice d'origine, en une ligne de code.
- Vérifiez que transposer deux fois redonne la matrice de départ.
Solution
def transposee(matrice):
"""Retourne la transposee d'une matrice rectangulaire non vide."""
lignes = len(matrice)
colonnes = len(matrice[0])
resultat = [[0] * lignes
Sur l'échelle suisse, les notes vont de 1,0 à 6,0 par demi-points: il y a donc onze valeurs possibles.
Écrivez un programme qui, à partir des dix notes du cours, construit une liste effectifs de onze compteurs, où effectifs[k] est le nombre de notes valant , puis affiche un histogramme en étoiles. Vérifiez que la somme des effectifs vaut bien l'effectif de la classe.
Solution
notes = [4.5, 5.0, 3.5, 6.0, 4.0, 5.5, 4.5,
Références
- Downey, A., Think Python, 3e éd., O'Reilly, Sebastopol, chapitres «Lists» et «Tuples» (disponible librement; traduction française Pensez en Python).
- Swinnen, G., Apprendre à programmer avec Python 3, Eyrolles, Paris, chapitre «Les listes» (disponible librement).
- Matthes, E., Python Crash Course, 3e éd., No Starch Press, San Francisco, chapitres 3 et 4 (listes et parcours de listes).
- Guttag, J. V., Introduction to Computation and Programming Using Python, MIT Press, Cambridge, chapitre «Structured Types», section sur la mutabilité et l'aliasing.
- Documentation officielle Python, The Python Tutorial, section «Data Structures» (docs.python.org), pour la liste complète des méthodes de liste et la syntaxe des compréhensions.
- Documentation officielle Python, The Python Standard Library, module
copy, pour la distinction entre copie de surface et copie en profondeur.