Mutable en Python : les objets qu'on peut modifier sur place

Un objet mutable se modifie sur place, sans qu'un nouvel objet soit créé. C'est ce qui explique les copies qui n'en sont pas.
5 min de lecture
Believemy logo

Définition

Vous affectez une liste à une nouvelle variable, vous la modifiez par cette variable, et la liste d'origine change aussi, sans que vous l'ayez touchée directement. Cette surprise a une cause précise : certains objets se modifient sur place, d'autres non.

Un objet mutable est un objet que Python autorise à changer sans en fabriquer un nouveau. La liste en est l'exemple le plus courant : après un append, c'est le même objet qui a grossi, et non une copie qui aurait pris sa place.

PYTHON
couleurs = ["rouge", "vert"]
avant = id(couleurs)

couleurs.append("bleu")
print(couleurs, id(couleurs) == avant)

# ['rouge', 'vert', 'bleu'] True

Le contraire s'appelle immuable : un tuple ou une chaîne de caractères ne se modifie jamais, toute opération qui semble le faire construit en réalité un objet neuf. La distinction décide de ce qui arrive dès que deux noms désignent le même objet.


Ce qui est mutable, et ce qui ne l'est pas

Une question suit naturellement : quels types se comportent comme la liste, et lesquels comme le tuple ? Le tableau qui suit répond type par type.

TypeModifiable sur place
listOui, par append, insert ou un tri en place
dictOui, par ajout, mise à jour ou suppression d'une clé
setOui, par add et discard
bytearrayOui, octet par octet
Une instance de votre classeOui par défaut, sauf précaution explicite
int, float, bool, str, tupleNon, en aucune circonstance
frozensetNon, c'est la version figée de l'ensemble

Un repère évite d'apprendre le tableau par cœur : les méthodes qui ne renvoient rien ont travaillé sur place, celles qui renvoient un résultat en ont fabriqué un nouveau. couleurs.sort() ne rend aucune valeur, puisqu'elle a trié la liste elle-même, là où sorted(couleurs) en construit une seconde qu'il faut récupérer.


Deux noms pour un même objet

Une affectation comme sauvegarde = panier ne copie rien : elle crée un second nom qui pointe vers le même objet, ce que confirme le test is. Sur un objet immuable, cela ne se remarque jamais. Sur un objet mutable, tout ce qui passe par un nom devient visible depuis l'autre.

PYTHON
panier = ["pain"]
sauvegarde = panier          # le même objet, pas une copie

panier.append("lait")
print(sauvegarde)            # ['pain', 'lait']

La copie se demande explicitement : list(panier), un slicing complet écrit panier[:], ou copy.copy. Ces trois formes produisent une copie de surface : la liste extérieure est neuve, mais les objets rangés dedans restent partagés. Pour une liste de listes, seule copy.deepcopy descend jusqu'au bout.

Le schéma ci-dessous le montre : deux noms sur un même objet, un troisième sur une copie.

Deux noms sur un seul objet, un troisième sur une copie Trois noms Python à gauche. Les traits de panier et de sauvegarde se rejoignent en un point unique et n'atteignent qu'un seul objet, la liste d'identifiant 4402, où l'élément ajouté « lait » ressort en couleur. Le nom copie mène vers un objet distinct, d'identifiant 9187, resté à un seul élément. Les noms Après panier.append("lait") liste (id 4402) "pain" "lait" les deux noms le voient liste (id 9187) "pain" la copie ne change pas panier panier = ["pain"] sauvegarde sauvegarde = panier copie copie = panier[:]

Essayez-le vous-même avec la démonstration ci-dessous.

Modifier par un nom, le voir changer par l'autre
original['a', 'b', 'c']même objet
alias['a', 'b', 'c']même objet
copie['a', 'b', 'c']objet distinct

Au départ, les trois noms affichent le même contenu. Rien ne permet de deviner lequel partage sa mémoire avec les autres.

Un test avec is répond immédiatement à la question : original is alias vaut True, original is copie vaut False. C'est le premier réflexe de diagnostic quand une donnée change sans que rien ne semble y toucher.


Les trois pièges qui coûtent le plus cher

Le premier est la multiplication d'une liste qui contient elle-même des listes. Elle répète la référence, pas le contenu.

PYTHON
grille = [[0] * 3] * 3
grille[0][0] = 1
print(grille)

# [[1, 0, 0], [1, 0, 0], [1, 0, 0]]

Les trois lignes sont la même liste regardée trois fois. La forme correcte passe par une compréhension de liste, [[0] * 3 for _ in range(3)], qui reconstruit une ligne neuve à chaque tour.

Le deuxième est la valeur par défaut mutable d'une fonction, évaluée une seule fois à la définition et non à chaque appel : la même liste sert alors tous les appels et grossit sans fin.

Attention

Écrire def ajouter(article, panier=[]): semble anodin, mais ce panier par défaut n'est créé qu'une fois et partagé par tous les appels. La convention est d'écrire panier=None, puis de créer la liste dans le corps de la fonction, None marquant l'absence.

Le troisième est l'attribut mutable déclaré au niveau de la classe. Une liste écrite là appartient à la classe et non aux objets : tous les exemplaires la partagent, et l'ajout fait pour l'un apparaît chez les autres. Sa place est dans __init__.


Pourquoi un mutable ne peut pas servir de clé

Pourquoi une liste ne peut-elle pas être une clé de dictionnaire, quand un tuple identique le peut ? Un dictionnaire range ses éléments d'après une empreinte calculée à partir de la valeur. Si elle changeait après coup, l'empreinte deviendrait fausse et l'élément introuvable. Python coupe court et refuse l'opération.

PYTHON
index = {}
index[["a", "b"]] = 1

# TypeError: unhashable type: 'list'

La TypeError est une protection, pas un caprice. Le remède consiste à figer la clé : un tuple à la place de la liste, un frozenset à la place de l'ensemble. C'est la meilleure réponse à qui demande pourquoi le tuple existe alors que la liste semble suffire.


Questions fréquentes

Question

Comment savoir si un objet est mutable ?

En essayant hash(objet) dans une console : ce qui refuse d'être haché est mutable, et ce qui accepte ne l'est presque jamais. Le second indice est la présence de méthodes qui ne renvoient aucune valeur, signe qu'elles ont travaillé directement sur l'objet.

Question

Un tuple qui contient une liste est-il modifiable ?

Le tuple reste immuable : aucun de ses emplacements ne peut recevoir autre chose. La liste rangée à l'intérieur continue de vivre sa vie et peut changer. Ce tuple perd du même coup le droit de servir de clé, puisque son empreinte ne serait plus fiable.

Question

Comment rendre mes propres objets non modifiables ?

La voie la plus simple est une dataclass déclarée avec frozen=True, qui refuse toute réaffectation d'attribut après la construction.

Termes connexes

Découvrez notre glossaire Python

Parcourez les termes et définitions les plus couramment utilisés dans le domaine du développement avec Python.

Partager cet article

Tu veux nous aider ? Fais un lien vers cet article sur tes réseaux ou encore mieux : sur ton site, dans un article ou dans ta newsletter.