Structures de données 1 : listes et tuples

Dans ce tutoriel, nous allons nous intéresser à des structures de données de base en Python : les listes et les tuples. Les structures de données peuvent être vues comme des conteneurs car ils permettent de stocker, d’organiser et d’accéder à des données. Les listes et les tuples sont des conteneurs séquentiels : les éléments qu’ils contiennent sont ordonnés, et leur position est enregistrée dans un index.

Listes

Définition

Dans le tutoriel précédent, nous avons vu que les chaînes de caractères étaient des séquences de caractères. Les listes sont également des séquences, c’est à dire des suites ordonnées d’éléments, mais plus générales : les éléments peuvent être de différente nature.

Les listes sont construites avec des crochets [], et les éléments de la liste sont séparés par des virgules.

Assignons une première liste à une variable a :

a = [1, 2, 3]
print(a)
[1, 2, 3]

La liste a est constituée d’entiers, mais une liste peut en pratique contenir des objets de tout type.

b = ["une séquence", 56, "d"]
print(b)
['une séquence', 56, 'd']

Il est notamment possible de créer des listes de listes (et ainsi de suite), ce qui permet de créer des structures hiérarchiques de données.

c = ["une séquence", 56, ["cette liste est imbriquée", 75, "o"]]
print(c)
['une séquence', 56, ['cette liste est imbriquée', 75, 'o']]

Une liste imbriquée peut aussi être construite à partir de listes déjà définies.

item1 = ["cafe", "500g"]
item2 = ["biscuits", "20"]
item3 = ["lait", "1L"]
inventaire = [item1, item2, item3]
print(inventaire)
[['cafe', '500g'], ['biscuits', '20'], ['lait', '1L']]

On verra cependant dans le prochain tutoriel que les dictionnaires sont généralement des structures de données souvent plus adaptées que les listes pour représenter des données sous forme hiérarchique.

Longueur d’une liste

Comme les chaînes de caractères, il est possible d’utiliser la fonction len pour compter le nombre d’éléments présents dans une liste.

d = ["ceci", "est", "une", "liste"]
len(d)
4

Indexation

Les listes étant des séquences, elles s’indexent de la même manière que les chaînes de caractères. Il est notamment important de se rappeler que la numérotation des positions commence à 0 en Python.

# Troisième élément de la liste a
print(a[2])
3

Bien entendu, il n’est pas possible de demander un élément qui n’existe pas. Python renvoie une erreur nous indiquant que l’index demandé est hors limites.

print(a[5])
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In[7], line 1
----> 1 print(a[5])

IndexError: list index out of range

Pour indexer une liste contenue dans une autre liste, on utilise une double indexation.

# Premier élément de la sous-liste qui est à la deuxième position de la liste c
print(c[2][0])
cette liste est imbriquée

En termes d’indexation, tout ce qui était possible sur les chaînes caractères l’est également avec les listes.

# Tous les éléments à partir de la 1ère position
print(b[1:])
[56, 'd']
# Inverser une liste
print(a[::-1])
[3, 2, 1]

Modification d’éléments

Il est possible de modifier les éléments d’une liste manuellement, avec une syntaxe similaire à l’assignation de variable.

# Réassignation d'un élément
d = [1, 2, "toto", 4]
d[2] = 3
print(d)
[1, 2, 3, 4]
# Substitution d'un élément
a = [1, 2, 3]
b = ["do", "re", "mi"]
b[0] = a[2]
print(b)
[3, 're', 'mi']

Suppression d’éléments

L’instruction del permet de supprimer un élément par position. Les éléments qui se trouvaient après l’élément supprimé voient donc leur index réduit de 1.

e = [1, "do", 6]
print(e)
print(e[2])

del e[1]
print(e)
print(e[1])
[1, 'do', 6]
6
[1, 6]
6

Quelques propriétés utiles

Là encore, on retrouve des propriétés inhérentes aux séquences.

# Concaténation
[1, 2, 3] + ["a", 12]
[1, 2, 3, 'a', 12]
# Réplication
["a", "b", "c"] * 3
['a', 'b', 'c', 'a', 'b', 'c', 'a', 'b', 'c']

Quelques méthodes utiles

A l’instar des chaînes de caractères, les listes ont de nombreuses méthodes built-in, qui s’utilisent selon le format objet.methode(parametres). Les plus utiles sont présentées ci-dessous ; d’autres méthodes seront utilisées dans le cadre des exercices de fin de section.

# Ajouter un élément
a = [1, 2, 3]
a.append(4)
print(a)
[1, 2, 3, 4]
# Supprimer un élément par position
b = ["do", "re", "mi"]
b.pop(0)
print(b)
['re', 'mi']
# Supprimer un élément par valeur
b = ["do", "re", "mi"]
b.remove("mi")
print(b)
['do', 're']
# Inverser une liste
l = [1, 2, 3, 4, 5]
l.reverse()
print(l)
[5, 4, 3, 2, 1]
# Trouver la position d'un élément
b = ["a", "b", "c", "d", "e"]
b.index("d")
3

Tuples

Définition

Les tuples sont une autre structure de données basique en Python, semblable à celle des listes dans leur fonctionnement. Il y a cependant une différence fondamentale : là où les éléments d’une liste peuvent être modifiés par position comme on l’a vu précédemment, les tuples sont non-modifiables (immutable). Ainsi, les éléments d’un tuple ne peuvent pas être modifiés sans redéfinir complètement le tuple.

Quand est-il pertinent d’utiliser un tuple plutôt qu’une liste ? En pratique, les tuples sont beaucoup moins fréquemment utilisés que les listes. On utilise généralement les tuples pour stocker des données qui n’ont pas vocation à être modifiées lors de l’exécution de notre programme Python. Cela permet de se prémunir contre des problèmes d’intégrité de données, c’est à dire de modification non-voulue des données d’entrée. On s’évite ainsi parfois de longues et pénibles séances de debugging.

Une autre différence, mineure celle-ci, est que les tuples s’écrivent avec des parenthèses au lieu des crochets. Les différents éléments sont toujours séparés par des virgules.

x = (1, 2, "mi", "fa", 5)
x
(1, 2, 'mi', 'fa', 5)

Afin de bien faire la différence avec l’usage normal des parenthèses (dans les calculs ou pour délimiter les expressions), un tuple à un seul élément se définit avec une virgule après le premier élément.

x1 = ("a", )
x1
('a',)

Vérifions qu’il est impossible de modifier ou d’ajouter un élément à un tuple.

t = ("do", "rez", "mi")
t[1] = "re"
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[23], line 2
      1 t = ("do", "rez", "mi")
----> 2 t[1] = "re"

TypeError: 'tuple' object does not support item assignment
t = ("do", "re", "mi")
t.append("fa")
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[24], line 2
      1 t = ("do", "re", "mi")
----> 2 t.append("fa")

AttributeError: 'tuple' object has no attribute 'append'

Fonctionnement

Les tuples s’indexent comme les listes.

print(x[0])
print(x[3:5])
1
('fa', 5)

Et peuvent également s’utiliser de manière hiérarchique.

t1 = ("a", "b", "c")
t2 = (1, 2, 3)
t3 = (t1, "et", t2)

print(t3)
print(t3[2][1])
(('a', 'b', 'c'), 'et', (1, 2, 3))
2

Les tuples partagent certaines méthodes built-in avec les listes : celles qui ne provoquent pas de modification de l’objet.

t = ("do", "re", "mi")
t.index("do")
0
t = ("do", "re", "mi", "re", "do")
t.count("re")
2

Conversion

Les fonctions list et tuple permettent de convertir une liste en tuple et inversement.

tuple(["do", "re", "mi"])
('do', 're', 'mi')
list((1, 2, 3, 4, 5))
[1, 2, 3, 4, 5]

Ces fonctions ont d’autres usages en pratique, que nous verrons en exercice.

Exercices

Questions de compréhension

  • Pourquoi dit-on des listes et des tuples que ce sont des conteneurs ?

  • Quel est le point commun entre les listes et les chaînes de caractères ?

  • Comment est enregistré l’ordre des éléments dans une séquence en Python ?

  • Quelle est la différence fondamentale entre une liste et un tuple ?

  • Dans quel cas aura-t-on plutôt intérêt à utiliser un tuple qu’une liste ?

  • Peut-on avoir des éléments de types différents (ex : int et string) dans une même liste ? Dans un même tuple ?

Afficher la solution
  • 1/ On dit que les listes et les tuples sont des conteneurs parce qu’ils permettent de stocker et d’organiser une collection d’éléments de différente nature dans une unique structure de données.

  • 2/ Les listes et les chaînes de caractères sont toutes deux des séquences ordonnées d’éléments, qui peuvent être requêtées par position. Dans le cas d’une chaîne de caractères, chaque élément est lui même une chaîne de caractères. Dans le cas d’une liste, les éléments peuvent être de différente nature (chaîne de caractère, liste, tuple, etc.)

  • 3/ Chaque élément d’une séquence a une position unique, appelée indice, qui commence à 0 pour le premier élément, 1 pour le second, et ainsi de suite. Les éléments sont stockés dans l’ordre où ils sont ajoutés.

  • 4/ Une liste est un objet mutable : il est possible d’ajouter, supprimer ou modifier des éléments d’une liste après sa création. A l’inverse, les tuples sont immutables : une fois qu’un tuple est défini, on ne peut ni changer ses éléments, ni ajouter ou supprimer des éléments.

  • 5/ En vertu de leur immutabilité, les tuples sont particulièrement adaptés pour stocker des données dont on voudrait s’assurer qu’elle ne seront pas modifiés par erreur. Par exemples, pour stocker des constantes d’un algorithme (paramètres, coordonnées géographiques, chemin de fichier, etc).

  • 6/ Oui, il est tout à fait possible d’avoir des éléments de types différents dans une même liste ou dans un même tuple. Ces éléments peuvent être de types de base (ex : int et string), mais également des conteneurs (ex : liste, tuple, dictionnaire, etc.).

Les 4 saisons

Créez 4 listes portant les noms des 4 saisons, contenant chacune les noms des mois associés (les mois de changement de saison seront attribués à la saison précédente). Puis créez une liste saisons contenant les 4 listes. Essayez de prévoir ce que vont renvoyer (type de l’objet, nombre d’éléments et contenu) les instructions suivantes, puis vérifiez le.

  • saisons

  • saisons[0]

  • saisons[0][0]

  • saisons[1][-1]

  • saisons[2][:3]

  • saisons[1][1:2] + saisons[-1][3:]

  • saisons[2:]

  • saisons + saisons[0]

  • saisons[3][::]

  • saisons[3][::-1]

  • saisons * 3

# Testez votre réponse dans cette cellule
Afficher la solution
printemps = ["avril", "mai", "juin"]
ete = ["juillet", "aout", "septembre"]
automne = ["octobre", "novembre", "decembre"]
hiver = ["janvier", "fevrier", "mars"]

saisons = [printemps, ete, automne, hiver]

l = saisons
print(type(l), len(l), l, "\n")

l = saisons[0]
print(type(l), len(l), l, "\n")

l = saisons[0][0]
print(type(l), len(l), l, "\n")

l = saisons[1][-1]
print(type(l), len(l), l, "\n")

l = saisons[2][:3]
print(type(l), len(l), l, "\n")

l = saisons[1][1:2] + saisons[-1][3:]
print(type(l), len(l), l, "\n")

l = saisons[2:]
print(type(l), len(l), l, "\n")

l = saisons + saisons[0]
print(type(l), len(l), l, "\n")

l = saisons[3][::]
print(type(l), len(l), l, "\n")

l = saisons[3][::-1]
print(type(l), len(l), l, "\n")

l = saisons * 3
print(type(l), len(l), l, "\n")

Faire ses gammes

En ajoutant, supprimant et modifiant des éléments, nettoyez la liste suivante pour qu’elle contienne les notes de musique “do re mi fa sol la si” dans le bon ordre.

l = ["do", "re", "re", "re", "fa", "sol", "solsi", "la"]

# Testez votre réponse dans cette cellule
Afficher la solution
l = ["do", "re", "re", "re", "fa", "sol", "solsi", "la"]

del l[1]  # On aurait aussi pu utiliser : l.pop(1)
l[2] = "mi"
del l[5]
l.append("si")

print(l)

Cet exemple visait simplement à pratiquer la modification et la suppression d’éléments. En pratique, il aurait été bien plus simple de directement créer la liste correcte.

Inversion de liste

Proposez deux méthodes pour inverser la liste ["une", "liste", "quelconque"]. Quelle est la différence majeure entre les deux méthodes ?

# Testez votre réponse dans cette cellule
Afficher la solution
l1 = ["une", "liste", "quelconque"]
l1.reverse()
print(l1)

l2 = ["une", "liste", "quelconque"]
print(l2[::-1])
print(l2)

La méthode reverse modifie la liste “en place” : la liste est durablement inversée après l’avoir exécutée. En revanche, la méthode qui inverse la liste via l’indexation renvoie une nouvelle liste et ne modifie pas l’existante. Pour que ce changement soit durable, il faudrait donc écraser la liste existante, ou bien en créer une nouvelle.

l2 = l2[::-1]
print(l2)

Pop’it

Nous avons vu que l’instruction ma_liste.pop(i) supprimait le i-ème élément de la liste ma_liste. A l’aide de la documentation Python ou d’une recherche sur Google, déterminez le comportement par défaut de cette méthode, c’est à dire ce qu’il se passe lorsqu’on ne donne aucun paramètre à la fonction pop. Vérifiez que vous observez bien ce comportement à l’aide d’un exemple de votre choix.

# Testez votre réponse dans cette cellule
Afficher la solution
l = ["do", "re", "mi"]
l.pop()
print(l)

Min et max de différentes listes

Il existe beaucoup d’autres méthodes built-in pour les listes que celles que nous avons déjà vues. Par exemple : min et max. Vérifiez leur comportement :

  • sur une liste composée uniquement d’objets numériques (int et float) ;

  • sur une liste composée uniquement de chaînes de caractères ;

  • sur une liste composée d’un mélange d’objets numériques et textuels.

# Testez votre réponse dans cette cellule
Afficher la solution
a = [5, 800, 9.92, 0]
b = ["do", "re", "mi", "fa", "sol"]
c = [1, "melange", "des", 2]

print(min(a), max(a))
print(min(b), max(b))
print(min(c), max(c))

La troisième expression renvoie une erreur : il n’existe pas de relation d’ordre pertinente.

Liste vide

Essayer de créer une liste vide. Vérifiez son type. Quel intérêt pourrait avoir un tel objet ?

# Testez votre réponse dans cette cellule
Afficher la solution
l = []
print(l)
print(type(l))

On peut donc effectivement créer une liste vide. Mais quel intérêt ? Un usage très fréquent est d’initialiser une liste, que l’on va ensuite remplir au fur et à mesure des itérations d’une boucle. Les boucles feront l’objet d’un prochain tutoriel ; mais voici un exemple simple d’un tel usage.

for i in range(10):
    l.append(i)
    
print(l)

La fonction list

Dans le tutoriel, nous avons vu les fonctions list et tuple qui permettent de passer d’un type à l’autre. En réalité, le fonctionnement de ces fonctions est plus subtil : le code list(mon_objet) renvoie la “version liste” de cet objet, de la même manière par exemple que str(3) renvoie '3', c’est à dire la version string de l’entier 3.

A l’aide de la fonction list, trouver les “versions liste” des objets suivants :

  • le tuple a = (1, 2, 3) ;

  • la chaîne de caractères b = "bonjour" ;

  • l’entier c = 5

# Testez votre réponse dans cette cellule
Afficher la solution
a = (1, 2, 3)
print(list(a))

b = "bonjour"
print(list(b))

c = 5
print(list(c))

La dernière expression renvoie une erreur : un entier n’est pas une séquence, une “version liste” n’a donc pas sens. On peut par contre bien entendu créer une liste avec pour seul élément 5.

d = [5]
print(d)

Immutabilité des tuples

Nous avons vu que les tuples avaient la particularité d’être non-modifiables. Mais est-ce que cette propriété se transfère de manière récursive ? Par exemple, est-ce qu’une liste contenue dans un tuple est-elle même non-modifiable ? Vérifiez à l’aide d’un exemple de votre choix.

# Testez votre réponse dans cette cellule
Afficher la solution
t = (1, 2, ["une", "liste"])
t[2][0] = 26
print(t)

Verdict : la non-modifiabilité ne s’applique qu’au premier niveau. Elle ne se transfère pas aux sous-éléments.

Dissociation de séquences

Lisez la partie concernant l’agrégation et la dissociation de séquences dans la documentation Python. La dissociation est une propriété souvent utilisée en pratique. Vérifiez qu’elle fonctionne sur les différents objets séquentiels que nous avons vus jusqu’à maintenant (chaînes de caractères, listes et tuples).

# Testez votre réponse dans cette cellule
Afficher la solution
x, y, z = "abc"
print(y)

a, b, c, d = ["do", "re", "mi", "fa"]
print(c)

r, s, t, u = ("un", "tuple", "de", "test")
print(r)