4.7. Types et mutabilité

La mutabilité est une caractéristique des objets qui indique si ceux-ci peuvent être modifiés après leur création. Un objet est dit mutable (ou variable) s’il possède cette propriété, et immuable sinon.

4.7.1. Mutabilité des types en Python

La liste des principaux types et collections de Python est résumée ci-dessous, en précisant lesquels sont mutables.

Mutabilité des principaux types

Type

bool

int

float

str

list

tuple

dict

set

Mutable

Les objets immuables sont moins facile et plus coûteux à manipuler que les objets mutables, car pour la moindre modification il faut reconstruire intégralement la version modifiée. En comparaison, il suffit de modifier la partie souhaitée d’un objet mutable sans avoir à en copier tout le reste, ce qui est bien plus efficace et ne requiert pas de stocker un nouvel objet.

>>> l = ['a', 'b', 'c']
>>> l.append('d')
>>> print(l)
['a', 'b', 'c', 'd']

Lorsque l’on ajoute le caractères 'd' à la liste l (mutable), la liste est modifiée grâce à la méthode append(). Elle s’agrandit et accueille le nouvel élément à l’indice 3. Le reste des éléments n’est pas touché, c’est une opération efficace.

>>> s = "abc"
>>> s = s + 'd'
>>> print(s)
abcd

À l’inverse, la même opération appliquée à une chaîne de caractères (immuable) est relativement peu efficace, car Python doit créer une nouvelle chaîne en copiant la chaîne d’origine et en y ajoutant le caractères 'd'. La chaîne ainsi obtenue remplace ensuite l’ancienne dans la variable s.

4.7.2. Avantages de l’immuabilité

L’immuabilité a toutefois de nombreux avantages. Puisqu’un objet immuable ne peut être modifié, il est garanti que s’il satisfait une condition à un moment donné, cette condition sera alors satisfaite à tout moment de l’exécution du programme. Il en va de même si à un moment du programme cet objet possède une valeur donnée, celle-ci est assurée de ne pas changer.

Considérons un exemple d’erreur classique qui peut survenir de la mauvaise utilisation des objets mutables. On représente Alice et Bob, deux personnes aux passe-temps presque identiques mais toutefois légèrement différents, à l’aide de deux dictionnaires. Pour gagner du temps, on crée une liste hobbies que l’on utilise deux fois, en y apportant simplement quelques modifications mineures.

>>> hobbies = ["vélo", "lecture", "programmation"]
>>> alice = {"nom": "Alice", "hobbies": hobbies}
>>> hobbies[2] = "cuisine"  # on remplace "programmation" par "cuisine"
>>> bob = {"nom": "Bob", "hobbies": hobbies}
>>> print(alice, bob, sep='\n')
{'nom': 'Alice', 'hobbies': ['vélo', 'lecture', 'cuisine']}
{'nom': 'Bob', 'hobbies': ['vélo', 'lecture', 'cuisine']}

Le résultat n’est malheureusement pas celui attendu, car Alice possède maintenant la cuisine comme hobby à la place de la programmation. En effet, la modification de la liste hobbies après la définition du dictionnaires alice a été répercutée sur celui-ci. La raison est que cette liste, stockée dans le dictionnaire immuable, est quant à elle mutable. C’est donc un piège dans lequel il est facile de tomber car, bien que l’état de la liste soit correct au moment de la création du dictionnaire, rien ne garantit que cela restera toujours le cas.

Voyons à présent un exemple similaire, mais où l’on réutilise cette fois une chaîne de caractères à la place d’une liste. Puisque les chaînes sont immuables, on a la garantie que la valeur fournie au moment de la définition du dictionnaire alice ne sera jamais modifiée, même si la variable contenant la chaîne est redéfinie.

>>> birthdate = "10.02.2001"
>>> alice = {"nom": "Alice", "naissance": birthdate}
>>> birthdate = "2" + birthdate[1:-1] + "2"
>>> bob = {"nom": "Bob", "naissance": birthdate}
>>> print(alice, bob, sep='\n')
{'nom': 'Alice', 'naissance': '10.02.2001'}
{'nom': 'Bob', 'naissance': '20.02.2002'}

Quel type ou collection faudrait-il utiliser à la place de la liste hobbies du premier exemple pour obtenir le résultat souhaité ?

Les objets immuables ont donc l’avantage de réduire le risque d’erreurs inattendues qui peuvent être longues à dénicher, car ils nécessitent moins de précautions. Il est plus facile de raisonner et de développer des programmes corrects avec des objets inaltérables qu’avec des objets mutables, pour lesquels il faut prévoir toutes les façons dont ils vont pouvoir être modifiés et garantir que cela soit fait de manière cohérente. Il n’y a notamment jamais besoin de faire une copie d’un objet immuable car on a la garantie que l’objet lui-même représente l’état dans lequel il a été créé et a toujours été.

4.7.3. Égalité structurelle et égalité par référence

La différence entre les opérateurs == et is est assez subtile à saisir. En effet, ils se comportent de manière identique dans de nombreuses situations.

>>> "mouton" == "mouton"
True
>>> "mouton" is "mouton"
True
>>> "mouton" == "loup"
False
>>> "mouton" is "loup"
False

En réalité, l’opérateur == teste l’égalité structurelle entre deux objets, c’est-à-dire si leur contenu est équivalent. L’opérateur is, en revanche, teste l’égalité par référence entre ceux-ci, c’est-à-dire si les deux objets n’en sont en réalité qu’un seul, et pas seulement des clones l’un de l’autre. Concrètement, is teste si les deux opérandes désignent le même objet dans la mémoire de l’ordinateur, c’est-à-dire s’ils possèdent la même adresse mémoire.

Voici un exemple qui illustre cette nuance.

>>> a = [1, 2, 3]
>>> b = [1, 2, 3]
>>> a == b
True
>>> a is b
False

On commence par créer deux listes au contenu identique, que l’on mémorise dans les variables a et b. La comparaison structurelle == confirme que les deux objets ont le même contenu, mais la comparaison par référence renvoie False car il s’agit en effet de deux objets distincts.

On peut faire une analogie avec deux moutons d’apparence identique, Billy et Dolly. Ils sont pareils dans la mesure où il s’agit d’animaux de la même espèce et qui ont la même apparence (égalité structurelle), mais ils sont toutefois bien distincts l’un de l’autre (égalité par référence). En effet, tondre Billy ne retire pas sa laine à Dolly.

Poursuivons avec notre l’exemple.

>>> b = a
>>> a is b
True
>>> a.append(4)
>>> print(b)
[1, 2, 3, 4]

On assigne à b la valeur de a (qui ici est en réalité une référence vers l’objet mémorisé, et non pas l’objet lui-même). Les deux variables font maintenant référence au même objet et sont donc égales par référence. Par conséquent, toute modification effectuée sur l’objet via l’une ou l’autre des variables a et b sera répercutée sur l’autre. Ainsi, lorsque l’on ajoute 4 à la liste a, la liste b subit la même modification. C’est naturel, puisqu’il s’agit en réalité de deux références à la même liste.

Afin de poursuivre notre analogie avec les moutons, si le mouton Billy se fait également appeler le roublard, alors tondre le roublard retirera sa laine à Billy, et vice-versa.

Une observation intéressante entre les deux types d’égalité est que, dans le cas de l’égalité par référence, le résultat de la comparaison ne changera jamais au cours de l’exécution du programme (tant que les variables ne sont pas redéfinies, bien entendu). Que les deux objets soient identiques par référence ou non, ils le resteront peut importe les modifications apportées. La comparaison structurelle, quant à elle, est éphémère et son résultat peut changer en cas de modification des objets comparés.

>>> a = [1, 2, 3]
>>> b = [1, 2, 3]
>>> a == b
True
>>> a.append(4)
>>> a == b
False

Les listes a et b, qui étaient égales à l’origine, ne le sont plus après la modification de la première.

4.7.4. Égalité par référence et immuabilité

Dans notre premier exemple, nous observions que deux chaînes de caractères similaires sont égales par référence de la manière suivante.

>>> a = "mouton"
>>> b = "mouton"
>>> a is b
True

Cela ne contredit-il pas la définition de l’égalité par référence ? En réalité non, car les chaînes de caractères sont immuables et ne peuvent pas être modifiées. Python fait donc en sorte que la chaîne "mouton" n’apparaisse qu’une seule fois dans la mémoire, et toutes les variables qui sont initialisées avec cette chaîne font effectivement référence au même objet.

4.7.5. Comparaison des types composés

La table ci-dessous résume les principales propriétés et différences des 5 types composés que nous avons vu dans ce chapitre : les listes, les chaînes de caractères, les tuples, les dictionnaires et les ensembles. Les critères de comparaison sont la syntaxe pour la déclaration et l’accès aux éléments, la mutabilité, l’ordre éventuel dans lequel les éléments sont rangés (type ordonné) ainsi que la possibilité d’avoir ou non plusieurs copies de la même valeur (doublons).

Comparaison des principaux types composés

Type

Déclaration

Accès

Mutable

Ordonné

Doublons

str

"...", '...'

[idx]

list

[...]

[idx]

tuple

(...)

[idx]

dict

{...}

[key]

set

{...}