Terminale · Langages et programmation

Mutabilité, références et effets de bord

Vous modifiez une ligne d’une grille et plusieurs lignes changent en même temps. Le programme ne travaille pas au hasard : plusieurs chemins peuvent conduire au même objet en mémoire. Dessiner les références explique ce comportement bien mieux que compter seulement les noms de variables.

SofienAvec SofienIngénieur et enseignant en informatique
Dans ce chapitre

Un cap pour ce chapitre

Ce que vous saurez faire

  • Distinguer noms, références et objets.
  • Prévoir les effets d’une modification mutable.
  • Reconnaître les limites d’une copie superficielle.
Les bases utiles pour commencer

Deux noms peuvent désigner une même liste

Après a=[1,2] puis b=a, Python ne construit pas une seconde liste indépendante. Les deux noms désignent le même objet mutable. Une modification comme b.append(3) devient visible à travers a et b. En revanche, b=[9] associe le nom b à un nouvel objet et ne modifie pas la liste auparavant partagée.

Il faut donc distinguer mutation et réaffectation. Une mutation change un objet existant ; une affectation change l’association d’un nom à une valeur. Pour prédire un résultat, dessinez les objets une seule fois puis reliez les noms à ces objets. Deux flèches vers une même case rendent le partage immédiatement visible.

La comparaison a == b teste le contenu selon les règles des listes ; a is b teste si les deux noms désignent le même objet. Deux listes distinctes peuvent donc être égales. Dessinez des identifiants d’objets symboliques plutôt que leurs adresses réelles, qui ne sont pas nécessaires pour raisonner.

Copier le conteneur ne copie pas toute sa profondeur

L’expression a.copy() construit une nouvelle liste contenant les mêmes éléments. Pour une liste de nombres, une modification d’une case de cette copie ne modifie pas la liste originale. Mais si les éléments sont eux-mêmes des listes, ils restent partagés : seule la couche extérieure est nouvelle.

a = [[1, 2], [3, 4]]
b = a.copy()
b[0][0] = 9
print(a)  # [[9, 2], [3, 4]]

La mutation atteint une liste intérieure commune. Construire une copie adaptée exige de connaître la structure des données. Une copie profonde peut être utile dans certains cas, mais n’est pas une réponse automatique à toute question de partage.

Le piège des lignes répétées

L’expression [[0,0]]*3 répète trois références vers une seule ligne. Modifier grille[0][1] modifie donc la ligne observée aux trois indices. Pour obtenir trois lignes indépendantes, une compréhension comme [[0,0] for _ in range(3)] construit une nouvelle liste à chaque itération.

Les deux grilles s’affichent initialement de la même façon. Leur égalité de contenu à cet instant ne prouve pas une organisation mémoire identique. Une manipulation ultérieure révèle la différence. Cette distinction apparaît dans les tableaux, les paramètres de fonctions et les objets possédant des attributs mutables.

Identifier les effets d’un appel

Une fonction reçoit des références aux objets passés en arguments. Si elle modifie une liste reçue, cette modification peut être visible pour l’appelant. Si elle réaffecte seulement son paramètre local à une nouvelle liste, cela ne remplace pas automatiquement la variable de l’appelant. Les effets doivent être annoncés dans le contrat.

Éviter un effet de bord peut consister à produire une nouvelle structure, à choisir des données immuables ou à limiter la zone autorisée à modifier un objet. La mutation reste utile et n’est pas une erreur en soi. Le défaut apparaît lorsque le partage et ses conséquences ne correspondent pas à l’intention du programme.

Pour tester l’absence de mutation, gardez un état attendu indépendant avant l’appel, puis comparez-le après l’opération. Si la structure est imbriquée, une copie superficielle de contrôle peut partager précisément les lignes que la fonction modifie : les deux objets changeraient ensemble et le test pourrait manquer le défaut. Écrivez alors directement le contenu attendu sur un petit exemple, ou construisez une copie adaptée aux niveaux concernés.

Exemple suivi : une mutation puis une réaffectation

Partons de a = [2,4] puis b = a. Un seul rectangle contient deux cases et deux flèches portent les noms a et b. Après b.append(6), ce rectangle contient [2,4,6] : les deux noms voient le changement. Après b = [9], un second rectangle apparaît et seule la flèche b est déplacée. a conserve [2,4,6].

Le même principe s’applique à un paramètre. À l’entrée d’une fonction, son nom local désigne l’objet passé par l’appelant. Une instruction t.append(6) modifie l’objet partagé. Une instruction t = [9] change seulement le nom local t. Pour remplacer la liste utilisée par l’appelant, la fonction peut renvoyer la nouvelle liste, puis l’appelant affecte explicitement ce résultat. Il faut observer quelle flèche ou quel rectangle chaque instruction modifie.

Exemple suivi : une grille copiée à deux profondeurs

Avec a = [[1,2],[3,4]] et b = a.copy(), il existe deux conteneurs extérieurs mais seulement deux lignes intérieures. Chaque conteneur pointe vers les mêmes lignes. b[0][0] = 8 change la première ligne partagée ; b[1] = [9,9] remplace une référence dans b seulement. Les résultats deviennent a=[[8,2],[3,4]] et b=[[8,2],[9,9]].

Si les lignes contiennent uniquement des nombres immuables, [ligne.copy() for ligne in a] suffit à construire une grille dont les lignes sont indépendantes. Si les cases contiennent à leur tour des listes, cette copie laisse encore ces objets plus profonds partagés. La profondeur utile dépend donc de la structure et de l’opération future. Copier davantage que nécessaire consomme du temps et de la mémoire ; partager volontairement peut être pertinent lorsque plusieurs vues doivent représenter la même donnée.

La distinction entre identité et contenu complète le dessin : après une copie superficielle, a is b vaut faux, mais a[0] is b[0] vaut vrai. Après une copie de chaque ligne numérique, les deux tests d’identité valent faux alors que a == b peut rester vrai. Ces observations expliquent pourquoi regarder uniquement le texte imprimé d’une grille ne valide pas son organisation en mémoire.

À vous de faire varier les choses

Une seule case modifiée, combien de lignes touchées ?

Changez la construction de la grille, la ligne modifiée et la valeur. Les groupes de références montrent quels indices partagent une ligne.

Lire le résultat de l’expérience initiale

Les trois indices désignent la même ligne.

La répétition du conteneur répète les références, pas la création de nouvelles lignes.

IndiceObjet désignéContenu
0Ligne A[0,5]
1Ligne A[0,5]
2Ligne A[0,5]

Deux affichages initialement identiques peuvent cacher des organisations mémoire différentes.

De la compréhension à l’autonomie

À vous de résoudre

Cherchez d’abord par vous-même. Vérifiez les résultats demandés, utilisez les indices si nécessaire, puis comparez votre méthode à la correction.

Exercice 1 · Comprendre#

Suivre un alias

Après a=[2,4], b=a et b[0]=7, quelles valeurs observe-t-on avec a et b ?

Indice 1

Aucune copie n’a été demandée.

Indice 2

La mutation concerne un objet commun.

Comprendre la correction

a et b désignent tous deux [7,4]. L’affectation b=a a ajouté un second nom vers la même liste. La modification de la première case est visible par les deux noms.

Exercice 2 · Appliquer#

Séparer mutation et affectation

Après a=[2,4], b=a puis b=[7], quelle valeur conserve a ?

Indice 1

Le dernier = associe b à une nouvelle liste.

Indice 2

L’ancienne liste n’est pas modifiée.

Comprendre la correction

a conserve [2,4], tandis que b désigne [7]. Le dernier ordre n’écrit pas dans une case de l’objet commun ; il change seulement la référence associée au nom b.

Exercice 3 · Corriger#

Réparer une grille

g=[[0,0]]*3 puis g[0][1]=5 produit trois lignes contenant 5. Comment construire trois lignes indépendantes ?

Indice 1

Il faut créer un objet ligne pour chaque itération.

Indice 2

Une compréhension peut répéter la construction plutôt que la référence.

Comprendre la correction

On peut utiliser g=[[0,0] for _ in range(3)]. Chaque itération crée une nouvelle ligne. Après g[0][1]=5, seule la première devient [0,5] ; les deux autres restent [0,0].

Exercice 4 · Justifier#

Évaluer une copie superficielle

a=[[1],[2]], b=a.copy(), puis b.append([3]) et b[0].append(9). Quelles modifications sont visibles dans a ?

Indice 1

Les conteneurs extérieurs sont distincts.

Indice 2

La première liste intérieure est partagée.

Comprendre la correction

L’ajout d’une troisième ligne appartient seulement à b. L’ajout de 9 à la première ligne est partagé. On obtient a=[[1,9],[2]] et b=[[1,9],[2],[3]]. Il faut examiner séparément chaque niveau de conteneur.

Exercice 5 · Problème de synthèse#

Problème : dessiner trois niveaux de références

On exécute a=[[1,2],[3,4]], b=a.copy(), b[0][0]=8, puis b[1]=[9,9]. Dessinez les conteneurs après chaque étape. Donnez les deux listes finales puis comptez les objets listes encore accessibles.

Indice 1

La copie extérieure conserve les références intérieures.

Indice 2

La dernière instruction crée une nouvelle ligne sans supprimer celle de a.

Comprendre la correction

a vaut [[8,2],[3,4]], b vaut [[8,2],[9,9]]. Cinq listes restent accessibles : les deux conteneurs extérieurs, la ligne [8,2] partagée, la ligne [3,4] et la nouvelle ligne [9,9]. Le comptage porte sur les objets, pas sur les chemins permettant de les atteindre.

Exercice 6 · Problème de synthèse#

Problème : choisir une copie adaptée

Une grille contient deux lignes de trois nombres. Une fonction doit modifier une case dans une copie sans changer l’original. Comparez b=a, b=a.copy() et b=[ligne.copy() for ligne in a]. Donnez un test qui distingue les trois constructions et expliquez la limite si les cases étaient des listes.

Indice 1

Modifier une case intérieure ne vise pas le conteneur extérieur.

Indice 2

Réaffecter une ligne permet ensuite de distinguer alias et copie superficielle.

Comprendre la correction

La première construction partage tout ; la deuxième sépare seulement l’extérieur ; la troisième sépare également les lignes et convient pour des cases numériques. Une mutation b[0][0]=9 distingue la troisième des deux autres ; puis b[1]=[7,7,7] distingue l’alias de la copie superficielle. Avec des cases listes, la troisième conserve encore ces cases partagées et doit être réévaluée selon le besoin.

Exercice 7 · Problème de synthèse#

Problème : expliquer deux fonctions

F reçoit t et exécute t.append(5) sans retour explicite. G reçoit t et exécute t=[5] sans retour explicite. On appelle chacune séparément sur une liste initiale [1,2]. Donnez la liste de l’appelant et le résultat renvoyé dans chaque cas. Corrigez G pour permettre un remplacement explicite.

Indice 1

Mutation de l’objet et affectation du paramètre diffèrent.

Indice 2

Une fonction sans return explicite renvoie None.

Comprendre la correction

Après F, l’appelant voit [1,2,5] et reçoit None. Après G, il conserve [1,2] et reçoit également None. G peut renvoyer [5], puis l’appelant écrire a=G(a). Ce retour rend le remplacement explicite et évite de supposer qu’une réaffectation locale change automatiquement une autre variable.

Les erreurs qui méritent un détour

Lire b=a comme une copie.
L’affectation partage la référence à un objet mutable.
Supposer qu’une copie de liste sépare toutes les sous-listes.
Une copie superficielle laisse les éléments intérieurs partagés.

La fiche à garder

L’essentiel à retenir

  • Mutation et réaffectation sont différentes.
  • Une copie superficielle ne duplique pas les objets imbriqués.
  • Les effets d’une fonction dépendent du partage des objets qu’elle modifie.

Cette notion au bac

Retrouvez ces idées dans un sujet complet, avec des indices, une correction expliquée et des ateliers.

Le prochain pas

Retrouver le catalogue de Terminale

Ce chapitre s’appuie sur le programme officiel de Terminale (PDF, nouvel onglet). Les explications et exercices sont proposés pour l’apprentissage.