17: Is

Dans cette leçon, nous donnons plus de détails sur la façon dont les listes sont stockées Python. Copier et comparer les listes peuvent avoir des conséquences inattendues. Nous allons expliquer comment Python fonctionne en interne à ce niveau, afin de comprendre et éviter les erreurs. Le point clé est que plusieurs variables peuvent "pointer" ou "partager" la même liste. Vers la fin de la leçon, nous décrivons l'opérateur "is" qui indique si deux variables pointent vraiment exactement vers la même liste.

Exemple

Disons que nous voulons créer un fragment de code pour convertir une liste de longueurs exprimées en pouces appelée tailleOriginale, en une liste de même longueur, mais en centimètres appelée tailleConvertie. La façon la plus naturelle de faire est d'utiliser la ligne tailleConvertie = tailleOriginale pour faire une copie, et ensuite passer en revue et modifier toutes les valeurs:

tailleOriginale = ["lettre", 8.5, 11]          # format de papier en pouces
tailleConvertie = tailleOriginale              # faire une copie
tailleConvertie[1] = tailleConvertie[1] * 2.54 # convertir en cm
tailleConvertie[2] = tailleConvertie[2] * 2.54 # convertir en cm
Maintenant, nous allons voir si cela fonctionne réellement. Si vous imprimez tailleConvertie alors il donne ["lettre", 21,59, 27,94] comme prévu. Mais il y a une grosse surprise si vous aussi imprimer tailleOriginale: les valeurs de tailleOriginale ont changé! Nous illustrons celà ci-dessous:

Exemple
La deuxième ligne de sortie n'est pas ce que nous attendons

Nous allons maintenant donner une explication détaillée de ce qui s'est passé, en utilisant des diagrammes. Le principal problème est que tailleConvertie = tailleOriginale n'a pas vraiment copié toute la liste: il a simplement copié une référence (flèche) à cette liste.

Cliquez sur les titres des diapositives de changer les onglets.

Mémoire
Nous allons utiliser une table pour représenter les variables et les valeurs dans la mémoire de Python. Par exemple, après l'exécution du code

ville = "Moose Factory"
population = 2458
employeur = ville
ce tableau (avec une bordure noire) montre que la mémoire Python ressemble à ceci:
 
Le nom de la première variable est la ville et sa valeur est la chaîne "Moose Factory". Le nom de la deuxième variable est la population et sa valeur est l'entier 2458. Le nom de la troisième variable est l'employeur et sa valeur est la chaîne "Moose Factory", la valeur de ville.
Lists en mémoire
Ensuite, montrons à quoi la liste ressemble en mémoire. Par exemple, prenons le fragment de code :

maListe = ["Moose Factory", 2458]
Cela va juste créer une variable, nommée maListe. Une liste est créée, et la valeur de maListe est égale au "pointeur" ou "lien" à cette liste. Nous représentons la liste à l'aide d'une boîte, et les valeurs des entrées de la liste sont indiquées à l'intérieur de la boîte à côté de leurs indices correspondants. La liste est représentée en bleu.

La flèche indique que maListe fait référence à cette nouvelle liste. L'élément à l'index 0 de la liste est la chaîne "Moose Factory", et l'élément à l'index 1 est le nombre entier 2458. Par exemple, si maListe[1] est affiché, alors Python va afficher 2458.
Remplacement d'une valeur de la liste
Maintenant, ajoutons une ligne de plus à l'exemple précédent, ceci à titre d'illustration. Disons qu'un bébé est né, alors exécutons le fragment de code

maListe = ["Moose Factory", 2458]
maListe[1] = maListe[1] + 1
Python calcule 2458 + 1, soit 2459, ce qui remplace la valeur à l'index 1 de la liste. Après la mise à jour, nous avons le schéma ci-dessous :

(La barre 2458 ne fait pas partie de la mémoire de Python, il est seulement indiqué pour souligner le changement.)
tailleOriginale et tailleConvertie
Revenons à l'exemple principal. La première ligne était

tailleOriginale = ["lettre", 8.5, 11]
et la mémoire Python ressemble à la figure ci-dessous après que cette ligne soit exécutée. Nous avons créé une liste de longueur 3.
Le problème principal
Dans notre programme, la deuxième ligne est

tailleConvertie = tailleOriginale
et nous voilà devant le point clé : à la deuxième ligne, = ne duplique pas la liste. Au lieu de cela,une nouvelle référence à la même liste a été copiée. Ceci est illustré par deux flèches différentes pointant vers la même boîte de. C'est tout à fait différent de copier les numéros ou les chaînes (comme dans la première diapositive).
Comme le montre le schéma, nous avons deux variables, qui font toutes deux référence à la même liste.
Mise à jour
Ensuite, lorsque le programme atteint la ligne

tailleConvertie[1] = tailleOriginale[1]*2.54
Python considère tailleConvertie, regarde la valeur d'indice 1 (8.5) et le multiplie par 2.54, puis remplace la valeur, comme illustré. Toutefois, étant donné que tailleOriginale référence la même liste, un effet secondaire est que nous avons perturbé tailleOriginale!
 
(Encore une fois 8.5 est représenté ici pour souligner le changement.)
Résultat
La ligne suivante est similaire,

tailleConvertie[2] = tailleConvertie[2]*2.54
qui affecte l'autre valeur de la liste. Après l'éxecution de cette ligne, la mémoire Python ressemble au schéma ci-dessous.

 
Maintenant quand nous imprimons soit tailleConvertie soit tailleOriginale, Python affiche ["lettre", 21.59, 27.94].

Une autre façon de regarder cet exemple est de l'exécuter à travers l'outil de visualisation. Si vous faites cela, au lieu de flèches, vous remarquerez que les listes sont nommées par leur "ID". Chaque liste existe en fait à une «adresse» égale à son identité; une valeur d'adresse est comme une flèche pointant vers la boîte à cette adresse ID.

Comment copier correctement une liste en Python

Bien qu'il soit parfois utile d'avoir de multiples références à la même liste, ici ce n'est pas ce que nous voulions.

Nous allons vous donner trois solutions. Elles sont toutes à peu près équivalentes, mais chacune d'elle va nous apprendre un fait nouveau sur Python, donc les trois méritent d'être lues.

Méthode 1, en utilisant [:]

Exemple
Copiage avec [:]

Ci-dessus, nous démontrons que listeCopiee = listeOriginale[:] crée une vraie copie de l'ancienne liste. Bien que cette syntaxe semble étrange, elle est en relation avec quelque chose que nous avons déjà vu. Dans la leçon sur les chaînes, nous avons introduit une façon d'extraire une sous-chaîne: chaine[premier:arriere] renvoie la sous-chaîne commençant par l'indice premier et se terminant avec un indice arriere-1. Nous avons mentionné que cela peut également s'appliquer à la création de sous-listes de listes. En outre,

  • si vous omettez premier, alors il prend la valeur par défaut 0;
  • si vous omettez arriere, alors il prend la valeur par défaut len (la longueur de la liste ou chaîne).

Alors ce qui se passe réellement, c'est que listOriginale[:] est la création d'une sous-liste, mais contenant toutes les données de la liste originale, c'est donc une nouvelle copie.

Méthode 2, en utilisant copy.copy()

Il existe un module appelé copy, qui contient plusieurs méthodes liées au copiage. Le plus simple est copy.copy(L), qui lorsque vous appelez cela sur une liste L, retourne une copie réelle de L.

Exemple
Copiage avec copy.copy

Le copy.copy() fonctionne aussi pour d'autres types d'objets, mais nous n'en discuterons pas ici.

Méthode 3, en utilisant list()

Le dernier moyen de faire une vraie copie utilise la fonction list(). Ordinairement, list() est utilisé pour convertir les données d'autres types en listes. (Par exemple list("salut") convertit la chaîne "salut" en une liste de 5 éléments, chaque element étant un character.) Mais si vous tentez de convertir quelque chose qui est déjà une liste en une liste, cela donne tout simplement une copie.

Exemple
Copiage avec list()

Les listes comme arguments

Notez qu'en raison de la façon dont les listes sont manipulées, une fonction qui accepte une liste d'argument peut réellement changer le contenu de la liste. (Vous avez vu ce déjà dans l'exercice remplace.)

Exercice à réponse courte : Argument de liste
Quel nombre est affiché par le programme suivant ?

def fonc(liste):
    liste[0] = liste[0] + liste[1]
    liste[1] = liste[1] + liste[0]
data = [3, 4]
fonc(data)
print(data[0]*data[1])
Correct ! Dedans fonc, nous changeons la valeur à l'indice 0 à 7, et la valeur à l'indice 1 à 7+4=11. Alors nous avons la sortie 7*11.

Comparaison de listes en utilisant is

Quand deux variables de liste L1 et L2 sont elle égales? Il a deux façons d'interpréter cette question :

  • Même identité: Est-ce que L1 et L2 pointent ou se réfèrent au même objet mémoire de type liste?
  • Mêmes valeurs: Est-ce que le contenu de la liste L1 est égal au contenu de la liste L2?

Il se trouve que, en Python, l'opérateur d'égalité standard == a le sens de Mêmes valeurs, comme le montre l'exemple ci dessous.

Exemple
Le sens de ==

Pour tester le cas de Même identité, Python a l'opérateur is. Il s'utilise la même manière que ==: la syntaxe

«list1» is «list2»

renvoie True si les listes pointent vers la même liste, et False si elle pointent vers des listes différentes (même si elles ont un contenu identique).

Exemple
Le sens de is

Exercice à réponse courte : Compte exact
Combien de fois la valeur True s'affiche dans ce programme? (Faites un schéma pour suivre la trace du calcul)

liste1 = [9, 1, 1]
liste3 = list(liste1)
liste2 = liste1
liste4 = liste3[:]
liste1[0] = 4
liste5 = liste1
print(liste1 is liste2, liste2 is liste3, liste3 is liste4, liste4 is liste5)
print(liste1 == liste2, liste2 == liste3, liste3 == liste4, liste4 == liste5)
Correct! The output is True False False False True False True False

Il ne faut pas utiliser is avec des chaînes ou des nombres car == teste déjà l'égalité, et le comportement de is est difficile à prédire avec des chaînes ou des nombres.

Listes imbriquées

Nous avons déjà vu la plupart des informations importantes, mais il y a une autre situation usuelle qui est digne d'attention. Une liste imbriquée est une liste dont les valeurs sont elle-mêmes une liste, par exemple

exemple = [365.25, ["premier", 5]]

montre une liste imbriquée. La liste externe, vers laquelle pointe sample, a deux éléments; l'élément à l'index 0 est une nombre flottant et l'élément l'index 1 est une liste interne. Cette liste interne est ["premier", 5]. (Il peut y avoir plus de niveaux d'imbrication, bien entendu.) Lors de l'utilisation des listes imbriquées, gardez à l'esprit que :

  • L'application des trois méthodes ci-dessus à exemple ne copierar que la liste externe, mais non la liste interne. Ainsi copy(exemple)[1] is exemple[1], signifie que la copie a toujours une référence vers une partie de la liste originale. Ce n'est probablement pas ce que vous souhaitez. C'est la méthode copy.deepcopy() qui effectue la copie à tous les niveaux d'imbrication.
  • Tester des listes avec l'opérateur == est intuitif: Python applique récursivement == sur chaque élément de la liste. Par exemple [[1, 2], 3]==[[1, 2], 3] a la valeur True, et [[1, 2], 3]==[1, 2, 3] a la valeur False puisque les premiers éléments sont différents ([1, 2] != 1).

Exemple
deepcopy et l'égalité récursive

Vous avez maintenant terminé cette leçon! Le matériel supplémentaire suivant est facultatif.


Tuples: ("listes", "immuables")

Nous avons mentionné ci-dessus que lorsque vous appelez une fonction sur une liste, il peut modifier la liste. Parfois, vous voulez rendre cela impossible! Une solution en Python est de créer des tuples, qui sont les mêmes objets que les listes, sauf qu'ils ne peuvent jamais être changé. Nous disons que les listes sont mutables et les tuples sont immuables. (Les chaînes et les nombres sont également immuables.) Cela peut être un moyen utile pour éviter toute erreur de programmation, de se prémunir contre la modification intenpestive de listes. Les tuples sont presque identiques aux listes, sauf qu'ils utilisent des parenthèses rondes () au lieu de crochets []. Vous pouvez convertir des tuples et des listes à l'aide de la construction tuple() et list().

Exemple
Tuples tubulaires

Vers  et au-delà: Auto-confinement

Il est possible d'avoir une liste qui se contient elle-même ! Il suffit de faire une liste, puis rediriger un de ses éléments pour qu'il pointe vers la liste elle-même :

Exemple
Une référence circulaire

Notez que la sortie du moteur Python est assez intelligent pour reconnaître que la liste reboucle sur elle-même: il affiche "..." au lieu de l'impression detout L, pour éviter une boucle infinie.