Définition
Vous appelez une méthode sur une chaîne de caractères, vous relancez le programme, et la chaîne est exactement comme avant. Aucune erreur, aucun avertissement, et pourtant le résultat attendu n'est nulle part. Ce n'est pas un bug : c'est de l'immuabilité.
Un objet immuable est un objet dont le contenu ne peut plus changer une fois qu'il existe. Aucune opération ne le modifie sur place : celles qui en donnent l'impression fabriquent un nouvel objet et laissent l'ancien parfaitement intact. Les nombres, les booléens, les chaînes et les tuples sont de cette famille.
Trois lignes suffisent à voir la mécanique à l'œuvre.
texte = "believemy"
texte.upper() # fabrique "BELIEVEMY" et le rend aussitôt
print(texte) # believemy, rien n'a bougéLa méthode a bien travaillé, mais elle vous a rendu son résultat au lieu de l'écrire dans texte. Comme personne ne l'a rangé, il disparaît dès la ligne suivante ; pour le garder, il faut le nommer, avec texte = texte.upper(). D'où l'habitude à prendre : sur un objet immuable, une méthode dont vous ignorez le résultat est un appel pour rien.
Fabriquer un nouvel objet à chaque opération finit par coûter : concaténer une chaîne dans une boucle de dix mille tours crée dix mille chaînes intermédiaires. La parade tient en un appel, "".join(morceaux).
Réassigner n'est pas modifier
Une objection vient ici à tout le monde : si les chaînes et les entiers ne changent jamais, comment une variable peut-elle changer de valeur à chaque ligne ? Parce qu'un nom, en Python, n'est pas une boîte contenant une valeur : c'est une étiquette posée sur un objet. Le réassigner revient à décoller l'étiquette et à la poser ailleurs, pas à repeindre l'objet.
L'image se vérifie en deux lignes. La fonction id renvoie un numéro propre à chaque objet vivant, une sorte d'adresse en mémoire.
compteur = 1
print(id(compteur))
compteur = compteur + 1
print(id(compteur)) # un autre identifiant, donc un autre objetLe numéro n'est plus le même : l'entier 1 n'a pas été augmenté, Python a fabriqué un entier 2 et déplacé l'étiquette compteur dessus. C'est cette différence que mesure l'opérateur is, qui répond sur l'objet là où == répond sur la valeur.
Les deux camps
Savoir de quel côté tombe un type n'est pas une curiosité de puriste : la question revient dès qu'on range une valeur dans un dictionnaire ou qu'une même donnée est partagée par deux morceaux de code. Voici où tombent les types intégrés.
| Type | Camp |
|---|---|
| Entiers, nombres à virgule, booléens | Immuable |
| str et bytes | Immuable |
| tuple et frozenset | Immuable |
| None | Immuable |
| liste, dictionnaire, ensemble | mutable |
Retenir ce tableau par cœur n'est pas nécessaire : le code vous renseigne de lui-même. Une méthode qui vous renvoie un résultat travaille sur un objet immuable, faute d'autre moyen de vous rendre son travail. Une méthode qui ne renvoie rien, comme append ou sort, a modifié l'objet sur place, donc cet objet est mutable.
Ce que l'immuabilité rend possible
Une règle aussi stricte ressemble à un caprice tant qu'on n'a pas vu ce qu'elle rapporte. Un objet qui ne change jamais peut être résumé par une empreinte stable, ce que Python appelle un hachage, et cette empreinte sert d'adresse de rangement dans un dictionnaire.
positions = {(0, 0): "départ", (3, 4): "arrivée"}
positions[[0, 0]] = "ailleurs"
# TypeError: unhashable type: 'list'Le refus n'est pas une brimade. Si la liste changeait après avoir été rangée, son empreinte changerait avec elle et le dictionnaire irait chercher la clé là où elle ne se trouve plus. La TypeError tombe donc à l'entrée.
Le deuxième bénéfice touche les valeurs par défaut. Une valeur par défaut est fabriquée une seule fois, quand Python lit la définition, et non à chaque appel : immuable, cela ne se remarque jamais ; mutable, cela se remarque très mal.
Une liste vide en valeur par défaut, comme def ajouter(article, panier=[]):, est partagée par tous les appels : le panier du deuxième client contient déjà les articles du premier. Écrivez panier=None, puis créez la liste dans la fonction.
Le troisième bénéfice compte le jour où le programme grossit : un objet immuable circule entre plusieurs fonctions, modules ou fils d'exécution sans copie défensive ni verrou, personne ne pouvant le modifier dans le dos de son créateur.
L'immuabilité s'arrête au premier niveau
Un dernier point fait tomber ceux qui croient la règle acquise. Un tuple garantit que ses cases ne changeront pas de contenu, mais rien sur les objets rangés dedans : si l'une d'elles contient une liste, cette liste reste modifiable.
config = ("prod", ["a", "b"])
config[1].append("c")
print(config) # ('prod', ['a', 'b', 'c'])
config[1] = []
# TypeError: 'tuple' object does not support item assignmentLe tuple refuse qu'on remplace une case, il ne dit rien de ce qui s'y trouve déjà. La conséquence surprend presque tout le monde une fois : un tuple contenant une liste n'est pas hachable et se fait refuser comme clé de dictionnaire.
L'empreinte se calcule sur le contenu, pas sur l'étiquette du contenant. Une garantie complète demande donc un contenu immuable jusqu'au bout : un tuple de tuples, un tuple de chaînes, ou un frozenset.
Questions fréquentes
Pourquoi ma chaîne reste-t-elle identique après un replace ?
Parce que replace fabrique une nouvelle chaîne et vous la rend, sans jamais toucher à l'originale : il faut écrire texte = texte.replace(...) pour conserver le résultat. Toutes les méthodes de chaîne se comportent ainsi.
Comment rendre immuables les objets de mes propres classes ?
Le plus simple est une dataclass déclarée avec frozen=True, ou un namedtuple pour quelques champs nommés. Toute affectation d'attribut après la création lève alors une erreur. La règle du premier niveau reste valable : geler la classe ne gèle pas une liste rangée dans un de ses champs.
L'immuabilité rend-elle un programme plus rapide ?
Rarement de façon spectaculaire, même si Python réutilise en interne certains petits entiers et certaines chaînes courtes. Le vrai gain est ailleurs : un objet qui ne peut pas changer élimine une famille entière de bugs, ceux où une valeur se retrouve modifiée à distance. Le temps économisé l'est au débogage.