Imbrication de données
Les éléments que l'on peut stocker dans les séquences, les ensembles et les dictionnaires peuvent être de n'importe quel type, sauf pour les ensembles et pour les clés des dictionnaires qui doivent être non modifiable. On peut imbriquer une structure de données dans une autre, pour construire des structures plus complexes. On a déjà vu quelques exemples dans les sections précédentes, et on va maintenant analyser cette possibilité dans le détail.
Liste à deux dimensions
Commençons avec les listes imbriquées, c'est-à-dire une liste dont les éléments sont eux-mêmes des listes. On va commencer par créer deux listes de nombres entiers, puis utiliser ces deux listes comme éléments d'une troisième liste :
Lorsqu'on exécute ces instructions, on obtient le résultat suivant qui montre bien que les éléments de la liste L
sont des listes :
[[1, 2], [3, 4, 5]]
On n'est pas obligé de préalablement créer les listes qui seront imbriquées, et on peut directement écrire :
La figure 3 montre tout ce qui est créé en mémoire lors de l'exécution de cette instruction. On voit d'abord que la variable L
est une référence vers une liste à deux éléments. Le premier élément de cette liste est lui-même une référence vers une autre liste, qui contient trois nombres entiers tandis que le second élément référence une liste à deux éléments.
Si on parcourt les éléments de la liste L
, et que l'on affiche leur type, on se rend compte directement qu'il s'agit bien de listes comme le montre l'exemple suivant et le résultat de son exécution :
<class 'list'> <class 'list'>
On peut donc accéder aux deux listes imbriquées avec les notations L[0]
et L[1]
. Comment ensuite accéder aux éléments de ces listes imbriquées ? Il suffit en fait simplement de faire un accès multiple en rajoutant encore des crochets. Ainsi, la notation L[1][2]
permettra d'accéder au troisième élément (celui d'indice $2$) de la deuxième liste imbriquée (celle d'indice $1$), à savoir la valeur $5$.
Lorsque toutes les listes imbriquées ont la même taille, on dit que l'on a créé une liste à deux dimensions. On peut comparer une telle structure aux matrices utilisées en mathématiques. Une telle structure est caractérisée par un nombre de lignes et de colonnes. Analysons l'exemple suivant :
La variable M
contient une référence vers une liste à deux éléments, ces derniers étant tous deux des listes de nombres entiers à trois éléments. On représente en fait la matrice suivante :
qui possède deux lignes et trois colonnes. On peut obtenir le nombre de lignes avec len(M)
et le nombre de colonnes avec len(M[0])
, pour autant que la matrice ne soit pas vide. Chaque élément de la liste à deux dimensions est accessible avec la notation M[i][j]
où l'indice $i$ représente celui de la ligne et l'indice $j$ celui de la colonne.
Pour parcourir tous les éléments d'une liste à deux dimensions, il faudra utiliser deux boucles imbriquées, que ce soit une while
ou une for
. Voici comment on peut parcourir la liste M
avec les deux types de boucle :
Ces boucles vont simplement afficher tous les éléments de la liste à deux dimensions, l'un en dessous de l'autre, en la parcourant ligne par ligne.
Liste multidimensionnelle
On peut aisément aller plus loin et augmenter la dimension d'une liste. Par exemple, une liste à trois dimensions est simplement une liste de listes de listes. Pour accéder aux valeurs de la liste tridimensionnelle, il faudra dès lors spécifier trois indices. Par exemple, pour la liste suivante :
On accède à la valeur $1$ avec data[0][0][0]
et à la valeur $6$ avec data[1][0][2]
. De manière générale, on évite toutefois de monter en dimensions car cela rend les listes plus complexes, notamment pour accéder à leurs éléments et les manipuler. Autant les listes à deux dimensions sont encore assez répandues, celles à trois dimensions sont déjà plus rares et ne parlons même pas des dimensions supérieures.
Structure imbriquée
Ce que l'on vient de voir avec les listes peut s'appliquer aux autres structures de données que l'on a vues jusqu'à présent, en respectant certaines contraintes.
Tout d'abord, tout ce que l'on peut imbriquer dans une liste, on peut également le faire avec des tuples. On a déjà vu que l'on pouvait imbriquer des listes dans une liste, et on peut également y imbriquer des tuples. On pourrait, par exemple, stocker une liste de coordonnées dans le plan comme suit :
On peut également imbriquer des ensembles dans une liste. Par exemple, on pourrait vouloir stocker une liste de lunches, par ordre de préférence :
Remarquez que la liste a été déclarée sur plusieurs lignes, afin de la rendre plus lisible. Une telle notation est autorisée et même recommandée lorsque vous déclarez des structures complexes.
Enfin, il n'y a aucun problème à stocker des dictionnaires dans une liste. L'exemple suivant montre comment on pourrait stocker ses contacts par ordre alphabétique des prénoms :
Concernant les ensembles, on a vu que l'on ne pouvait y stocker que des valeurs non modifiable. On ne va donc pas pouvoir y imbriquer des listes, des ensembles (on néanmoins peut imbriquer des ensembles non modifiables, obtenus avec frozenset
, dans des ensembles) ou des dictionnaires. On peut, par exemple, imbriquer des tuples dans un ensemble. Voici, par exemple, un ensemble reprenant, pour différents athlètes, les résultats réalisés en mètres pour trois lancers de javelot consécutifs :
La même restriction s'applique aux clés d'un dictionnaire qui doivent donc être des valeurs non modifiables. On peut, par exemple, utiliser un tuple comme clé d'un dictionnaire. L'exemple suivant associe une personne à une série de coordonnées dans le plan :
Si on désire pouvoir placer plusieurs personnes à une même coordonnée, et se rappelant que les clés d'un dictionnaire doivent être uniques, on va devoir imbriquer une liste dans le dictionnaire :
Comme on le verra à la section suivante, on va aussi pouvoir imbriquer des dictionnaires ensemble afin de construire des structures complexes permettant de représenter de véritables bases de données.
Copie
Terminons cette section en s'intéressant à la copie de structures de données, qui peut parfois se révéler délicate sachant qu'une variable ne contient pas toute la structure de données, mais uniquement une référence vers l'emplacement en mémoire où elle est stockée. En effet, comme on l'a vu à la section 5.1, lorsqu'on affecte à une variable une autre variable qui contient une référence vers une liste, on crée un alias. Les deux variables contiennent une référence vers la même zone mémoire, contenant la liste, et permettent donc d'agir sur la même liste. Revoyons cela avec l'exemple suivant :
Alors que l'on pourrait croire, de prime abord, que la variable A
contient une copie de la liste référencée par la variable L
, ce n'est effectivement pas le cas. Les variables A
et L
sont des alias vers la même liste, qui n'existe donc qu'une seule fois en mémoire. On peut le constater avec le résultat de l'exécution :
[42, 2, 3]
Pour faire une réelle copie, il faut passer par la fonction prédéfinie list
qui va, elle, créer une nouvelle liste en mémoire, avec le même contenu que celle passée en paramètre. Pour faire une véritable copie, l'exemple précédent doit donc se réécrire comme suit :
Cette fois-ci, le code affiche bien ce que l'on attend :
[1, 2, 3]
Ce comportement est exactement le même pour toutes les autres structures de données que l'on a vues jusqu'à présent. Il ne pose évidemment aucun soucis pour les structures de données non modifiables comme les chaines de caractères, les tuples ou les intervalles.
Pour les autres structures, il faudra à chaque fois passer par la fonction prédéfinie correspondant à la structure de données pour en faire une copie, à savoir list
, set
et dict
.
Module copy
La méthode de copie que l'on vient de voir permet en réalité de faire une copie en surface. Pour comprendre cette notion, attardons-nous un moment sur l'exemple suivant :
Bien que l'on ait fait une copie de la liste référencée par la variable L
dans la variable A
, après avoir modifié cette copie à partir de la variable A
, on observe le résultat suivant après exécution :
[[1, 2], [42, 4, 5]]
Pour comprendre ce qui s'est passé, il faut examiner la situation en mémoire, après exécution des deux premières instructions, illustrée à la figure 4. On y voit donc que la liste directement référencée par la variable L
a bel et bien été copiée, cette copie étant référencée par la variable A
. La subtilité, c'est que les listes qui sont référencées par ces deux listes sont, elles, uniques et c'est dans ces listes que se trouvent les valeurs de la liste à deux dimensions.
Si on le souhaite, il est également possible de faire une copie en profondeur, c'est-à-dire que tous les niveaux de la structure de données seront copiés, même les imbriquées.
Pour cela, on va faire appel au module copy
qui propose deux fonctions :
copy
permet de faire une copie en surface ;deepcopy
permet de faire une copie en profondeur.
L'exemple précédent avec une copie en profondeur se réécrit simplement comme suit, sans oublier d'importer le module copy
évidemment :
Le résultat de l'exécution montre que l'on a bel et bien une copie intégrale de la liste L
:
[[1, 2], [3, 4, 5]]
La copie en profondeur copie donc intégralement toutes les structures imbriquées. Cela a deux conséquences immédiates : la copie prendra plus de temps et la consommation mémoire globale du programme sera plus grande. Lorsqu'on fait une copie, il est dès lors important de se poser la question de savoir ce que l'on veut en faire. Si c'est uniquement pour la consulter, une simple alias suffira, et si c'est pour la modifier, selon la profondeur des modifications envisagées, le choix se portera vers une copie en surface ou en profondeur.