Les listes

Place à présent à un nouveau type de données, les listes, qui vont nous permettre de construire des valeurs plus complexes. Les listes vont en effet nous servir à composer plusieurs valeurs en une seule.

Des séquences de valeurs

Une liste en Python peut être vue comme une séquence de valeurs. Imaginez une simple ligne de tableau avec des cases, chaque case contenant une valeur.

5

3

2

8

6

7

3

Ceci est la représentation d’une liste de 7 nombres entiers. On la noterait en Python de la manière suivante :

numbers = [5, 3, 2, 8, 6, 7, 3]

On utilise donc des crochets pour délimiter la liste, et des virgules pour séparer les valeurs les unes des autres.

Chaque case de la liste est associée à une position (ou index). Ainsi la case en première position contient la valeur 5, celle en deuxième position contient la valeur 3, etc. L’ordre des éléments dans une liste est donc important, et celui-ci est libre (mes valeurs n’ont par exemple pas besoin d’être rangées en ordre croissant).

>>> [1, 2, 3]
[1, 2, 3]
>>> [2, 3, 1]
[2, 3, 1]

On note que la case en septième (dernière) position contient aussi la valeur 3. Une même valeur peut être présente dans la liste à plusieurs positions.

La liste peut être vue comme une généralisation des chaînes de caractères : là où la chaîne est une séquence de caractères, la liste peut contenir des valeurs de tous types. L’exemple précédent ne montre qu’une liste composée de nombres entiers (int), mais n’importe quelle valeur peut être contenue dans une liste.

>>> ['abc', 'def']
['abc', 'def']
>>> [4.5, 1.8, -3.2]
[4.5, 1.8, -3.2]

Il faut voir les listes comme des ensembles de valeurs distinctes les unes des autres mais qui forment un tout. Elles sont le reflet même des listes de la vie courante : une liste de courses, une liste d’élèves, une liste de notes, etc.

courses = ['pain', 'œufs', 'lait', 'pâtes', 'tomates']
eleves = ['Julie', 'Martin', 'Sami', 'Natacha']
notes = [12, 9, 16, 13]

On peut aussi construire une liste composée de valeurs de types différents. On verra par la suite que l’important est d’avoir une manière unique de traiter l’ensemble des éléments.

notes = [12, 8.5, 16, 12.5]
items = ['salut', 42, True, 1.5]

Une liste peut aussi ne contenir aucun élément (liste vide), on la définit alors à l’aide d’une simple paire de crochets [].
Un autre cas particulier est celui des listes contenant un seul élément, où la virgule est facultative puisqu’il n’y a pas de valeurs à séparer.

>>> []
[]
>>> [4]
[4]
>>> ['salut',]
['salut']

Quand on initialise une liste avec beaucoup d’éléments, il arrive que la ligne de définition soit assez longue.

words = ['sur', 'zeste', 'de', 'savoir', 'vous', 'pouvez', 'trouver', 'des', 'contenus', 'sur', 'des', 'sujets', 'variés']

Il est alors intéressant d’aérer le tout pour que ça devienne plus lisible. Python permet d’effectuer des retours à la ligne dans la définition d’une liste, entre les valeurs.
Attention, un retour à la ligne ne remplace pas la virgule séparant les valeurs, qui reste obligatoire.

On prendra l’habitude d’indenter les valeurs par rapport à la ligne d’ouverture de la liste.

words = [
    'sur',
    'zeste',
    'de',
    'savoir',
    'vous',
    'pouvez',
    'trouver',
    'des',
    'contenus',
    'sur',
    'des',
    'sujets',
    'variés',
]

Seule la dernière virgule, puisque suivie d’aucune valeur, est facultative. Je la laisse par commodité et pour ne pas faire de différences entre les lignes.

Une liste se définit aussi par le nombre d’éléments qu’elle contient, sa taille. Cette taille sera amenée à évoluer au cours du déroulement du programme, la liste pouvant gagner ou perdre des éléments suivant certaines opérations.

Opérations sur les listes

Opérations élémentaires

Tout comme les chaînes de caractères, les listes possèdent donc une taille. Là encore, il est possible de connaître cette taille à l’aide d’un appel à la fonction len.

>>> len(numbers)
7
>>> len(words)
13

Comme pour les chaînes toujours, il est possible d’accéder aux éléments de la liste à l’aide de l’opérateur [] associé à une position. 0 correspondant à la première position, 1 à la deuxième, etc.

>>> numbers[4]
6
>>> print(words[8])
contenus

Les index négatifs sont aussi acceptés.

>>> words[-2]
'sujets'

On peut tester l’égalité entre deux listes à l’aide des opérateurs == et !=. Deux listes sont égales si elles contiennent les mêmes valeurs dans le même ordre.

>>> [1, 2, 3] == [1, 2, 3]
True
>>> [1, 2, 3] == [3, 2, 1]
False
>>> [1, 2, 3] != [3, 2, 1]
True

Comme les chaînes de caractères, les listes sont aussi concaténables les unes aux autres, permettant de construire une grande liste en agrégeant des plus petites. De même qu’elles sont concaténables par multiplication avec un nombre entier.

>>> [1, 1, 2, 3] + [5, 8, 13] + [21]
[1, 1, 2, 3, 5, 8, 13, 21]
>>> ['ab', 'cd'] * 3
['ab', 'cd', 'ab', 'cd', 'ab', 'cd']

En plus de ça, les listes possèdent aussi différentes méthodes, par exemple pour rechercher et compter les éléments :

  • index renvoie la position d’une valeur dans la liste. Cette position correspond au premier élément trouvé (si la valeur est présente plusieurs fois), et la méthode produit une erreur si la valeur n’est pas trouvée.
>>> numbers.index(2)
2
>>> numbers.index(3)
1
>>> numbers.index(7)
5
>>> numbers.index(9)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: 9 is not in list
>>> words.index('savoir')
3
  • count compte et renvoie le nombre d’occurrences d’un élément dans la liste (donc 0 si l’élément n’est pas présent).
>>> numbers.count(3)
2
>>> numbers.count(8)
1
>>> numbers.count(9)
0
>>> words.count('des')
2
Mutabilité

Les listes sont des objets dits mutables, c’est-à-dire modifiables, ce qui n’est pas le cas des autres types de données que nous avons vus jusqu’ici. En effet, sur les précédentes données que nous manipulions, leur valeur ne pouvait pas changer une fois qu’elles avaient été définies.
Nous pouvions redéfinir une variable vers une nouvelle valeur (a = 10; a += 1), mais la valeur en question restait inchangée (10 valait toujours 10).

Sur les listes, nous pouvons par exemple librement remplacer certains éléments par d’autres, grâce à l’opérateur d’indexation ([]) couplé à une affectation (=).

>>> words = ['salut', 'les', 'amis']
>>> words[2] = 'copains'
>>> words
['salut', 'les', 'copains']

Ici c’est bien la valeur même de la liste qui a été modifiée : on a altéré son contenu pour remplacer un élément, mais words est toujours la même liste.

On peut mettre cet état de fait en évidence si l’on a deux variables qui référencent la même liste.

>>> numbers = copy = [1, 2, 3, 4]
>>> numbers[0] = 10
>>> numbers
[10, 2, 3, 4]
>>> copy
[10, 2, 3, 4]

C’est d’ailleurs un comportement qui est souvent perçu comme une erreur par les débutants, mais il faut bien comprendre que numbers et copy sont deux étiquettes sur une même liste. Ainsi, une modification de numbers est également une modification de copy.

>>> numbers = copy = [1, 2, 3, 4]
Deux étiquettes sur une même liste.
Deux étiquettes sur une même liste.
>>> numbers[0] = 10
Les deux étiquettes sont affectées.
Les deux étiquettes sont affectées.

Nos listes étant modifiables, elles proposent aussi certaines opérations pour insérer ou supprimer des éléments.

La méthode append permet comme son nom l’indique d’ajouter un nouvel élément en fin de liste (à la dernière position), augmentant donc de 1 la taille de la liste.

>>> letters = ['a', 'b', 'c', 'd']
>>> len(letters)
4
>>> letters.append('e')
>>> letters
['a', 'b', 'c', 'd', 'e']
>>> len(letters)
5

Plus généralement, on trouve la méthode insert qui permet d’insérer un élément à une position (un index) particulière dans la liste, décalant ainsi s’il y en a les éléments à sa droite d’un cran.

>>> letters.insert(0, 'à')
>>> letters
['à', 'a', 'b', 'c', 'd', 'e']
>>> letters.insert(6, 'é')
>>> letters
['à', 'a', 'b', 'c', 'd', 'e', 'é']
>>> letters.insert(3, 'ĉ')
>>> letters
['à', 'a', 'b', 'ĉ', 'c', 'd', 'e', 'é']
>>> letters.insert(-2, 'đ')
>>> letters
['à', 'a', 'b', 'ĉ', 'c', 'd', 'đ', 'e', 'é']

Comme vous le voyez, les index négatifs sont aussi acceptés. Si la position est plus grande que la taille de la liste, la valeur sera insérée la fin. De même, la valeur sera insérée au début pour une position négative dépassant la limite.

>>> letters.insert(20, 'f')
>>> letters
['à', 'a', 'b', 'ĉ', 'c', 'd', 'đ', 'e', 'é', 'f']
>>> letters.insert(-50, 'å')
>>> letters
['å', 'à', 'a', 'b', 'ĉ', 'c', 'd', 'đ', 'e', 'é', 'f']

La méthode pop sert quant à elle à supprimer un élément de la liste. Utilisée sans argument, elle en supprimera le dernier élément. La méthode renvoie l’élément qui vient d’être supprimé, ce qui permet de le conserver dans une variable par exemple.

>>> letters.pop()
'f'
>>> deleted = letters.pop()
>>> print(deleted, 'a été supprimée')
é a été supprimée
>>> letters
['å', 'à', 'a', 'b', 'ĉ', 'c', 'd', 'đ', 'e']

Mais la méthode peut aussi être appelée avec une position en argument, pour supprimer une valeur à un index particulier.

>>> letters.pop(0)
'å'
>>> letters
['à', 'a', 'b', 'ĉ', 'c', 'd', 'đ', 'e']

On notera aussi l’opérateur del permettant lui aussi de supprimer une valeur mais sans la renvoyer.

>>> del letters[3]
>>> letters
['à', 'a', 'b', 'c', 'd', 'đ', 'e']

L’opérateur del est d’ailleurs un opérateur qui permet de supprimer une variable. del foo revient à désaffecter la variable foo qui n’existe alors plus dans la suite du programme.

>>> foo = 'abc'
>>> del foo
>>> foo
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'foo' is not defined

del ne supprime pas la valeur à proprement parler qui peut toujours être référencée par une autre variable.

>>> foo = bar = [1, 2, 3]
>>> del foo
>>> bar
[1, 2, 3]
Slicing

Nous avons vu pour l’instant comment accéder facilement à un élément d’une liste à partir de son index, grâce à l’opérateur d’indexation ([]). Mais cet opérateur est plus puissant que cela et permet des utilisations plus avancées.

Obtenir une partie d’une liste

Il est en effet possible d’extraire plusieurs éléments en un seul appel, à l’aide d’une syntaxe particulière. Il s’agit de préciser entre les crochets une position de début et une position de fin, séparées par un signe :. On appelle cela le slicing (ou « découpage »).

La valeur renvoyée sera la liste des éléments compris entre ces deux positions (démarrant à la position de début et s’arrêtant juste avant la position de fin).

>>> numbers = [1, 1, 2, 3, 5, 8, 13, 21]
>>> numbers[1:4]
[1, 2, 3]
>>> numbers[0:7]
[1, 1, 2, 3, 5, 8, 13]

On voit bien que numbers[1:4] nous renvoie la liste des éléments d’index compris entre 1 et 3 (inclus). Ces opérations n’affectent pas la liste d’origine qui reste inchangée.

>>> numbers
[1, 1, 2, 3, 5, 8, 13, 21]

Une fois de plus, il est possible d’utiliser des index négatifs pour se positionner à partir de la fin de la liste.

>>> numbers[-5:-1]
[3, 5, 8, 13]
>>> numbers[1:-2]
[1, 2, 3, 5, 8]

Une autre facilité est que l’on peut omettre la position de début ou la position de fin. Sans position de début, on considère que l’on part du début de la liste (index 0), et sans fin, que l’on va jusqu’à la fin (index len(numbers)).

>>> numbers[3:]
[3, 5, 8, 13, 21]
>>> numbers[:-3]
[1, 1, 2, 3, 5]

Si l’on omet le début et la fin, on récupère une liste contenant tous les éléments de la liste d’origine.

>>> numbers[:]
[1, 1, 2, 3, 5, 8, 13, 21]

On peut enfin préciser une troisième valeur qui est le « pas » (par défaut de 1). Ce pas indique combien d’index on passe entre chaque élément. Un pas de 3 signifie que l’on ne considère qu’un élément sur 3.

Ainsi, [1:8:3] correspondra aux index 1, 4 et 7 (3 de différence entre chaque index)

>>> numbers[1:8:3]
[1, 5, 21]

Ou encore [::2] permettra d’extraire un élément sur deux de la liste initiale. En effet cela permet d’extraire l’élément d’index 0, puis 2, puis 4, etc.

>>> numbers[::2]
[1, 2, 5, 13]

Le pas est calculé à partir de l’index de départ, le résultat sera donc différent avec [1::2] qui considérera en premier l’élément d’index 1, puis 3, puis 5, etc.

>>> numbers[1::2]
[1, 3, 8, 21]
Modifier une partie d’une liste

Voilà pour ce qui est des accès en lecture, mais ces opérations sont aussi possibles pour la modification.

>>> numbers[:2] = [2, 0]
>>> numbers
[2, 0, 2, 3, 5, 8, 13, 21]

La liste que l’on assigne n’a pas besoin de faire la même taille que le nombre d’éléments concernés par le slicing, ce qui peut alors modifier la longueur de la liste d’origine.

>>> numbers[-1:] = [21, 34, 55]
>>> numbers
[2, 0, 2, 3, 5, 8, 13, 21, 34, 55]
>>> numbers[1:5] = []
>>> numbers
[2, 8, 13, 21, 34, 55]

Et ces opérations concernent aussi l’opérateur del.

>>> del numbers[1:-1]
>>> numbers
[2, 55]

Enfin, l’opération de slicing (en lecture seulement) est aussi disponible sur les chaînes de caractères, renvoyant donc une chaîne composée des caractères aux positions comprises dans l’intervalle..

>>> 'pouetpouet'[3:-2]
'etpou'

Pour plus d’informations sur le slicing en Python, je vous invite à découvrir ce tutoriel : Les slices en Python.

Listes à plusieurs dimensions

Je présentais en introduction les listes comme des séquences, des lignes d’éléments. L’analogie est bonne, d’autant que nos listes précédentes ne contenaient que des types de données simples : nombres ou chaînes de caractères.

Mais les listes peuvent contenir toutes sortes de données, même des plus complexes comme… d’autres listes.

>>> items = [1, 2, [3, [4]]]
>>> items
[1, 2, [3, [4]]]

Pour accéder aux éléments des sous-listes, on pourra simplement chaîner les opérateurs [].

>>> items[2][1][0]
4
>>> items[2][0] = 5
>>> items
[1, 2, [5, [4]]]

Quand une liste est composée uniquement de sous-listes, elle peut alors prendre la forme d’un tableau. Comme ici avec une liste représentant un plateau de morpion.

morpion = [
    ['x', ' ', ' '],
    ['o', 'o', ' '],
    ['x', ' ', ' '],
]

Que l’on peut représenter sous la forme du tableau suivant.

'x'

' '

' '

'o'

'o'

' '

'x'

' '

' '

Il s’agit ici d’un tableau à deux dimensions (lignes et colonnes). Mais les listes n’ont pas de limite et l’on pourrait alors voir d’autres subdivisions s’il y avait un niveau supplémentaire de listes.

Problème de la multiplication

Je vous parlais de l’opérateur de multiplication des listes pour les concaténer, mais que se passe-t-il si on l’utilise sur des listes à plusieurs dimensions ?

Eh bien ça ne fonctionne pas comme prévu !
En effet, cet opérateur ne crée pas de copies mais duplique les références à une même valeur. La même sous-liste est alors répétée plusieurs fois dans la liste, provoquant des comportements inattendus en cas de modifications.

>>> grid = [[1, 2, 3]] * 2
>>> grid
[[1, 2, 3], [1, 2, 3]]
>>> grid[0].append(4)
>>> grid
[[1, 2, 3, 4], [1, 2, 3, 4]]

Le code précédent étant en fait équivalent à :

>>> line = [1, 2, 3]
>>> grid = [line, line]
>>> grid
[[1, 2, 3], [1, 2, 3]]
>>> line.append(4)
>>> grid
[[1, 2, 3, 4], [1, 2, 3, 4]]
Étiquettes dupliquées entre les lignes.
Étiquettes dupliquées entre les lignes.

Ce comportement de duplication des références n’est pas propre aux listes multidimensionnelles.
Un code tel que [0] * 10 duplique aussi 10 fois la référence à la valeur 0, mais cela ne pose pas de problème particulier car les nombres ne sont pas des valeurs modifiables. Le comportement apparaît donc problématique dans le cas des sous-listes en raison de leur mutabilité.

Nous verrons dans le chapitre prochain comment contrer ce problème en construisant nos listes itérativement, en attendant je vous conseille de simplement ne pas utiliser la multiplication dans des cas comme celui-ci.

>>> grid = [[1, 2, 3], [1, 2, 3]]
>>> grid[0].append(4)
>>> grid
[[1, 2, 3, 4], [1, 2, 3]]