Les listes

Une vidéo de présentation d’un premier container, les listes…

Nous avons vu jusqu’à présent deux types d’objets, que l’on peut considérer « atomiques » :

Lorsque l’on veut manipuler un ensemble de ces objets, et même un ensemble d’objets plus complexes, Python dispose de plusieurs containers pour stocker ces objets.

Le plus simple de ces containers est la list qui est une collection ordonnée d’objets (éventuellement hétérogènes) définie avec les opérateurs [ et ]:

>>> l = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
>>> l
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
>>> len(l)
10

Tout est objet en Python et la list n’échappe bien sûr pas à la règle. En tant qu’objet, la list dispose donc de plusieurs méthodes permettant d’effectuer un certain nombre d’opérations courantes sur ce type de structure. Les plus élémentaires sont abordées ci dessous.

Slicing

Comme les chaînes de caractères, une list est une sequence, et à ce titre dispose des mêmes opérations d’indexation et de slicing. Toutes les propriétés décrites dans le paragraphe sur le Slicing sont valables:

>>> l[0] # accès au premier élément de la liste
0
>>> l[-1] # accès au dernier élément de la liste
34
>>> l[::2] # les éléments de rang pair
[0, 1, 3, 8, 21]

A expérimenter…

Mettre en oeuvre quelques opérations de slicing un peu plus complexes que celles qui précèdent, conjecturer le résultat et vérifier dans un interpréteur interactif.

Modification des éléments d’une liste

Contrairement aux chaînes de caractères, la list est une séquence mutable. Il est donc possible de modifier ses éléments:

>>> l = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
>>> l[0] = 'zéro'
>>> l
['zéro', 1, 1, 2, 3, 5, 8, 13, 21, 34] # liste hétérogène

Modification de la structure d’une liste

On peut également modifier la structure de la liste, en ajoutant ou en retirant des éléments, ce qui va modifier sa longueur.

On peut ajouter des éléments n’importe où dans une list en utilisant la méthode insert():

>>> l.insert(2,99)
>>> l
['zéro', 1, 99, 1, 2, 3, 5, 8, 13, 21, 34]

ou les supprimer avec remove():

>>> l.remove(99) # remove(elt) ne retourne rien
>>> l
['zéro', 1, 1, 2, 3, 5, 8, 13, 21, 34]

La méthode pop() permet également de retirer un élément de la liste en passant son index en argument. L’élément retiré est disponible et peut être affecté à une nouvelle variable (référence):

>>> first = l.pop(0) # pop(index) retourne l'élément retiré de la liste
>>> first
'zéro'
>>> l
[1, 1, 2, 3, 5, 8, 13, 21, 34]

L’ajout d’un élément à la fin d’une list est un cas très courant qui dispose d’une méthode particulière append(). Cette méthode est plus rapide que insert() car la liste réserve automatiquement un espace libre à la fin. A contrario, insert() est plus lente car les opérations qu’elle met en oeuvre sont plus complexes:

>>> l.append(55)
>>> l
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Il existe deux façons d’ajouter des éléments à une liste :

  • ajouter chaque élément individuellement avec append() par exemple ;

  • ou ajouter tous les éléments d’une autre liste avec avec extend().

La deuxième méthode s’appelle la concaténation:

>>> l.extend([89, 144])
>>> l
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144]

En Python, la redéfinition d’opérateur est possible comme on l’a vu avec les chaines de caractères. Pour les listes, l’opérateur + a été redéfini pour se comporter (presque) comme extend():

>>> l = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
>>> l = l + [89, 144]
>>> l
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144]

A expérimenter…

Ci dessus il est dit que l’opérateur + a été redéfini pour se comporter (presque) comme extend(). Observer attentivement ce que retournent les opérations ci dessous, effectuées après la création de la liste:

>>> l.extend([89, 144])

et:

>>> l + [89, 144]

Conclusion ?

Utilisation des slices

L’utilisation des slices est une méthode puissante de modification de liste.

La syntaxe s[i:j] = ts et t sont des séquences insère la séquence t dans la portion de liste définie par s[i:j]. La taille des deux séquences peut différer.

On peut s’en servir pour l’insertion:

>>> l = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
>>> l[2:2] = [0, 0, 0]
>>> l
[0, 1, 0, 0, 0, 1, 2, 3, 5, 8, 13, 21, 34]

la suppression:

>>> l[2:5] = []
>>> l
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

la modification:

>>> l[6:] = [6, 7, 8, 9]
>>> l
[0, 1, 1, 2, 3, 5, 6, 7, 8, 9]

Appartenance

Comme chaque sequence, la chaine de caractères et celles que l’on va aborder par la suite, la list dispose d’un mécanisme simple pour tester l’appartenance ou non d’un élément avec l’opérateur in:

>>> 34 in l
True
>>> 63 in l
False

Itérer sur une liste

L’opérateur in est également utilisé pour l’itération:

>>> l = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
>>> for elt in l:
...     print(elt)
...
1
1
2
3
5
8
13
21
34

Il est important de comprendre qu’en Python on itère par défaut sur les éléments de la liste, pas sur les index.

❌ Le code ci dessous produit le même résultat que le code ci dessus, mais est moins concis, donc susceptible de produire plus d’erreurs et plus difficile à comprendre et à maintenir. Il est donc à éviter:

>>> for i in range(len(l)):
...     print(l[i])
...
0
1
1
2
3
5
8
13
21
34

Si l’index est requis, la fonction enumerate() est utilisée:

>>> for i, elt in enumerate(l):
...     print(i, elt)
...
0 0
1 1
2 1
3 2
4 3
5 5
6 8
7 13
8 21
9 34

Autres méthodes

Python dispose de nombreuses méthodes pour manipuler les séquences dont les listes font partie.

On peut par exemple effectuer une recherche avec index():

>>> l.index(89)
10

ou inverser la list avec reverse():

>>> l.reverse()
>>> l
[144, 89, 55, 34, 21, 13, 8, 5, 3, 2, 1, 1]

Listes numériques

Dans le cas particulier où la liste manipulée est uniquement composée de nombres, Python dispose en standard de fonctions permettant de rechercher les valeurs min() et max():

>>> min(l)
1
>>> max(l)
144

Ces fonctions sont dites vectorisées car l’argument est un vecteur de données. Le parcours des données, pour en rechercher le minimum ou le maximum, se fait de façon optimisée dans les fonctions min() et max().

Il est dans la très grande majorité des cas inutile de réécrire sa propre fonction pour réaliser les opérations courantes. Python dispose de très nombreuses fonctions mathématiques, en standard ou dans un module (qu’il convient alors d’importer). Un exemple ci dessous avec les fonctions mean() et median() du module statistics:

>>> from statistics import mean, median
>>> mean(l)
31.333333333333332
>>> median(l)
10.5

Le tri élémentaire

Parmi toutes les méthodes de liste, la méthode sort() permet de trier la liste sur place:

>>> l
[7, 6, 9, 2, 4, 5, 8, 3, 1, 0]
>>> l.sort()
>>> l
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Le paramètre reverse permet de changer l’ordre de tri:

>>> l.sort(reverse=True)
>>> l
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

L’algorithme utilisé est le Timsort, dérivé du merge sort et insertion sort.

Listes de listes

Une list peut contenir n’importe quel objet Python et en particulier d’autres listes:

>>> even = [0, 2, 4, 6, 8]
>>> odd = [1, 3, 5, 7, 9]
>>> fib = [1, 1, 2, 3, 5]
>>> primes = [2, 3, 5, 7, 11]

>>> nums = [even, odd, fib, primes]
>>> type(nums)
<class 'list'>
>>> nums
[[0, 2, 4, 6, 8], [1, 3, 5, 7, 9], [1, 1, 2, 3, 5], [2, 3, 5, 7, 11]]

Les éléments de la liste nums sont eux même des listes. Et donc, nums[1] est aussi une list

>>> type(nums[1])
<class 'list'>
>>> nums[1]
[1, 3, 5, 7, 9]

nums[1] est une liste d’entiers, ce qu’on constate en observant le type de son quatrième élément:

>>> type(nums[1][3])
<class 'int'>

En effet, puisque nums[1] est une liste, on accède à ses éléments avec le même opérateur d’indexation:

>>> nums[1][3]
7

Les list comprehension

On peut construire une list à partir d’une boucle for et de la méthode append():

>>> cubes = []
>>> for i in range(10):
    ...:     cubes.append(i**3)
    ...:

>>> cubes
[0, 1, 8, 27, 64, 125, 216, 343, 512, 729]

Cependant Python possède une syntaxe plus concise et plus élégante, les list comprehension:

>>> cubes = [ i**3 for i in range(10)]
>>> cubes
[0, 1, 8, 27, 64, 125, 216, 343, 512, 729]

Note

La variable utilisée à l’intérieur de la list comprehension est locale à celle ci:

>>> i = 99
>>> [ i**3 for i in range(10)]
[0, 1, 8, 27, 64, 125, 216, 343, 512, 729]
>>> i
99

Il est possible d’ajouter un prédicat à une list comprehension:

>>> [ i**3 for i in range(10) if i%2 == 0]
[0, 8, 64, 216, 512]

ou d’imbriquer des boucles, qui sont exécutées dans l’ordre d’apparition dans la list comprehension:

>>> [i+str(j) for i in 'ab' for j in range(3)]
['a0', 'a1', 'a2', 'b0', 'b1', 'b2']

ou bien encore de construire des listes de listes:

>>> [[i+str(j) for i in 'abcdefgh'] for j in range(1, 9)]

[['a1', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h1'],
 ['a2', 'b2', 'c2', 'd2', 'e2', 'f2', 'g2', 'h2'],
 ['a3', 'b3', 'c3', 'd3', 'e3', 'f3', 'g3', 'h3'],
 ['a4', 'b4', 'c4', 'd4', 'e4', 'f4', 'g4', 'h4'],
 ['a5', 'b5', 'c5', 'd5', 'e5', 'f5', 'g5', 'h5'],
 ['a6', 'b6', 'c6', 'd6', 'e6', 'f6', 'g6', 'h6'],
 ['a7', 'b7', 'c7', 'd7', 'e7', 'f7', 'g7', 'h7'],
 ['a8', 'b8', 'c8', 'd8', 'e8', 'f8', 'g8', 'h8']]

On peut également utiliser une clause else dans le prédicat. Dans ce cas, le test est placé avant for:

>>> marks = [8, 12, 7, 15, 16, 13, 4, 14, 9, 15]
>>> [ 'pass' if x >=10 else 'failed' for x in marks]
['failed', 'pass', 'failed', 'pass', 'pass', 'pass', 'failed', 'pass', 'failed', 'pass']

Chaque fois que ce sera possible cette syntaxe devra être utilisée.

Copies et références

Lorsqu’on affecte un objet à un nom de variable (une référence), Python crée une relation entre les deux. On dit que la référence « pointe » vers l’objet:

>>> l1 = [0, 1, 2, 3, 4]

Ici la référence l1 pointe vers la liste [0, 1, 2, 3, 4]. L’instruction l2 = l1 duplique la référence vers la liste [0, 1, 2, 3, 4], mais pas la liste elle même ! Pour s’en convaincre:

>>> id(l1)
39850888
>>> l2 = l1
>>> id(l2)
39850888

l1 et l2 pointent bien vers le même objet. Ce comportement peut avoir des conséquences inattendues, s’il n’est pas bien compris. En particulier modifier l1 a pour conséquence de modifier l2:

>>> l1.append(5)
>>> l1
[0, 1, 2, 3, 4, 5]
>>> l2
[0, 1, 2, 3, 4, 5]

Pour dupliquer, la liste, plutôt que la référence, il faut forcer la création d’un nouvel objet:

>>> l3 = l1[:] # ou l3 = list(l1)
>>> l3
[0, 1, 2, 3, 4, 5]
>>> l1.append(6)
>>> l1
[0, 1, 2, 3, 4, 5, 6]
>>> l3
[0, 1, 2, 3, 4, 5]

Lorsque la liste est une structure complexe (contenant d’autres objets), il est plus sûr de faire appel à deepcopy():

>>> from copy import deepcopy
>>> a = [[1,2],[3],[4]]
>>> id(a)
1471379026944
>>> b = deepcopy(a)
>>> id(b)
1471379028736

Ce qu’il faut retenir

  • Une liste se délimite avec [ et ]

  • Une liste se délimite avec ( et )

  • Une liste se délimite avec { et }

  • Une liste peut contenir des objets hétérogènes

  • Une liste est une collection ordonnée

  • Les opérations de slicing sur une liste sont légèrement différentes des opérations de slicing sur une chaine de caractères

  • Une liste est mutable

  • Une fois créée, la taille d’une liste est figée

  • est une méthode permettant d’insérer un élément n’importe où dans la liste

  • est une méthode permettant d’insérer un élément à la fin de la liste

  • est une méthode permettant de supprimer un élément de la liste sans le retourner

  • est une méthode permettant de supprimer un élément de la liste en le retournant

  • append est une méthode qui permet d’ajouter plusieurs éléments à la liste en une seule opération

  • extend est une méthode qui permet d’ajouter un seul élément à la liste en une seule opération

  • Le slicing peut être utilisé pour extraire des éléments d’une liste

  • Le slicing peut être utilisé pour insérer des éléments dans une liste

  • Le slicing ne peut pas modifier la taille d’une liste

  • Sur une liste, l’opérateur d’appartenance in ne fonctionne pas tout à fait comme pour une chaine de caractères

  • La façon privilégiée de parcourir une liste est d’itérer sur ses éléments

  • La façon privilégiée de parcourir une liste est d’itérer sur l’index de ses éléments

  • Une liste dispose d’une méthode pour trier ses éléments

  • Une liste peut contenir n’importe quel objet Python

  • On peut construire des listes imbriquées

  • Une est une façon concise de construire une liste

  • Une list comprehension ne contient pas de structure conditionnelle

  • Une list comprehension peut construire une liste unidimensionnelle

  • Une list comprehension doit construire une liste unidimensionnelle