a = [1, 2, 3]
b = a
b.append(4)
print(a)[1, 2, 3, 4]
Ce chapitre explique un des concepts les plus déroutants de Python. Les questions TOSA sur ce sujet sont très discriminantes : elles séparent ceux qui ont compris de ceux qui ont juste appris la syntaxe. Prenez votre temps, relisez, et faites tourner tous les exemples sur votre machine.
Pourquoi ce code produit-il un résultat surprenant ?
a = [1, 2, 3]
b = a
b.append(4)
print(a)[1, 2, 3, 4]
La liste a contient maintenant 4 alors qu’on n’a modifié que b. Ce chapitre explique pourquoi, et comment éviter ce piège.
En Python, une variable n’est pas une case mémoire qui contient une valeur. C’est une étiquette qui pointe vers un objet en mémoire.
x = 42Ce qui se passe :
42 en mémoire.x est attachée à cet objet.x ────► [ 42 ]
On peut visualiser cette référence avec la fonction id() qui renvoie l’identifiant unique d’un objet en mémoire :
x = 42
print(f"x pointe vers l'objet {id(x)}")x pointe vers l'objet 140704084258520
Quand on écrit b = a, on ne copie pas les données. On crée une nouvelle étiquette qui pointe vers le même objet.
a = [1, 2, 3]
b = a
print(f"id(a) = {id(a)}")
print(f"id(b) = {id(b)}")
print(f"a et b pointent vers le même objet : {a is b}")id(a) = 1360570124736
id(b) = 1360570124736
a et b pointent vers le même objet : True
a ────┐
├───► [ 1, 2, 3 ]
b ────┘
On dit que a et b sont des alias — deux noms pour le même objet. Toute modification via b est aussi visible par a :
a = [1, 2, 3]
b = a
b.append(4) # on modifie L'OBJET via b
print("a :", a) # a voit la modification : même objet !
print("b :", b)a : [1, 2, 3, 4]
b : [1, 2, 3, 4]
is — identité d’objetL’opérateur is teste si deux variables pointent vers le même objet (identité), alors que == teste si leurs valeurs sont égales (égalité).
a = [1, 2, 3]
b = a # alias
c = [1, 2, 3] # nouvel objet, même contenu
print("a == b :", a == b) # True : même contenu
print("a is b :", a is b) # True : même objet
print("a == c :", a == c) # True : même contenu
print("a is c :", a is c) # False : objets différents !a == b : True
a is b : True
a == c : True
a is c : False
a ────┐
├───► [ 1, 2, 3 ] ← même objet
b ────┘
c ────────► [ 1, 2, 3 ] ← autre objet, même contenu
== vs is
Règle pratique :
is pour tester avec None, True, False.== pour comparer des valeurs.# ✅ Bonnes pratiques
if x is None: # identité avec None
if x == 10: # égalité de valeur
if nom == "Alice": # égalité de chaîne
# ❌ À éviter
if x == None: # fonctionne mais non idiomatique
if nom is "Alice": # peut marcher ou non selon l'implémentation !La dernière ligne est particulièrement piégeuse : Python mutualise certaines petites chaînes et petits entiers, ce qui peut faire que "Alice" is "Alice" est True… mais pas toujours. Ne vous fiez jamais à is pour comparer des valeurs.
Pour obtenir une copie indépendante d’une liste, on a plusieurs options :
[:]a = [1, 2, 3]
b = a[:] # copie par slicing
b.append(4)
print("a :", a) # inchangé
print("b :", b)
print(a is b) # False : objets différentsa : [1, 2, 3]
b : [1, 2, 3, 4]
False
list()a = [1, 2, 3]
b = list(a) # même effet
b.append(4)
print("a :", a)
print("b :", b)a : [1, 2, 3]
b : [1, 2, 3, 4]
.copy()a = [1, 2, 3]
b = a.copy()
b.append(4)
print("a :", a)
print("b :", b)a : [1, 2, 3]
b : [1, 2, 3, 4]
| Méthode | Copie de | Notes |
|---|---|---|
b = a[:] |
liste, tuple, str | Slicing complet |
b = list(a) |
liste | Explicit |
b = a.copy() |
liste, dict, set | Méthode dédiée |
b = dict(a) |
dict | — |
b = set(a) |
set | — |
b = a |
❌ alias, pas une copie ! | Piège |
Les méthodes ci-dessus font une copie superficielle (shallow copy) : le conteneur est copié, mais pas les objets imbriqués.
matrice = [[1, 2], [3, 4]]
copie = matrice[:] # copie superficielle
copie.append([5, 6]) # OK : on modifie la liste copiée
copie[0].append(99) # ⚠️ modifie aussi matrice !
print("matrice :", matrice)
print("copie :", copie)matrice : [[1, 2, 99], [3, 4]]
copie : [[1, 2, 99], [3, 4], [5, 6]]
Pour une copie vraiment indépendante (même en profondeur), il faut copy.deepcopy :
import copy
matrice = [[1, 2], [3, 4]]
profonde = copy.deepcopy(matrice)
profonde[0].append(99)
print("matrice :", matrice) # inchangée
print("profonde :", profonde)matrice : [[1, 2], [3, 4]]
profonde : [[1, 2, 99], [3, 4]]
La différence est surtout importante quand les listes contiennent d’autres listes, dicts ou objets mutables.
Python classe ses types en deux catégories :
| Type | Mutable ? |
|---|---|
int, float, complex |
❌ immuable |
bool |
❌ immuable |
str |
❌ immuable |
tuple |
❌ immuable |
frozenset |
❌ immuable |
NoneType |
❌ immuable |
list |
✅ mutable |
dict |
✅ mutable |
set |
✅ mutable |
| Objets personnels (classes) | ✅ mutables par défaut |
Puisqu’un immuable ne peut pas être modifié, le piège de l’alias ne se produit pas :
a = 10
b = a # "alias" d'un int
b = b + 1 # en fait, b pointe maintenant vers un NOUVEL objet (11)
print("a :", a) # a n'a pas bougé
print("b :", b)a : 10
b : 11
Étape 1 :
a ────┐
├───► [ 10 ]
b ────┘
Étape 2 (après b = b + 1) :
a ────────► [ 10 ]
b ────────► [ 11 ] ← nouvel objet
La réaffectation b = b + 1 ne modifie pas l’objet 10 — elle crée un nouvel entier et attache l’étiquette b à celui-ci.
Les chaînes étant immuables, leurs méthodes renvoient une nouvelle chaîne (jamais modification en place) :
a = "Alice"
b = a
b = b.upper() # crée une NOUVELLE chaîne
print("a :", a) # inchangé
print("b :", b)a : Alice
b : ALICE
Python passe les arguments par référence — mais comme l’affectation ne copie pas, les effets peuvent surprendre.
def incrementer(x):
x = x + 1 # réaffectation → crée un nouvel objet localement
print("Dans la fonction : x =", x)
a = 10
incrementer(a)
print("Après :", a) # inchangéDans la fonction : x = 11
Après : 10
La variable locale x pointe initialement vers le même objet que a. Mais x = x + 1 réaffecte x à un nouvel objet. a reste intact.
def ajouter_element(liste, element):
liste.append(element) # modifie L'OBJET, pas de réaffectation
a = [1, 2, 3]
ajouter_element(a, 4)
print(a) # [1, 2, 3, 4] : modification visible ![1, 2, 3, 4]
liste.append() modifie l’objet en place. Comme liste et a pointent vers le même objet, a en subit les conséquences.
.append, .sort, d[k] = v) : impacte l’original.liste = [...] dans la fonction) : ne modifie pas l’original, crée un nouvel objet local.def vider_incorrect(liste):
liste = [] # ❌ réaffectation locale, ne vide pas l'original
def vider_correct(liste):
liste.clear() # ✅ modifie l'objet en place
a = [1, 2, 3]
vider_incorrect(a)
print("Après vider_incorrect :", a)
vider_correct(a)
print("Après vider_correct :", a)Après vider_incorrect : [1, 2, 3]
Après vider_correct : []
Voyons maintenant un point subtil : le comportement de += sur différents types.
a = 10
b = a
b += 5 # équivalent à b = b + 5 : crée un nouvel objet
print("a :", a) # inchangé
print("b :", b)a : 10
b : 15
a = [1, 2, 3]
b = a
b += [4, 5] # équivalent à b.extend([4, 5]) : modifie l'objet !
print("a :", a)
print("b :", b)a : [1, 2, 3, 4, 5]
b : [1, 2, 3, 4, 5]
x += y n’est PAS toujours équivalent à x = x + y !
Pour les immuables, x += y équivaut à x = x + y : on crée un nouvel objet.
Pour les mutables comme les listes, x += y appelle en fait x.__iadd__(y) qui modifie l’objet en place (comme extend).
# Liste : += modifie en place
a = [1, 2, 3]
id_avant = id(a)
a += [4]
print("Même objet ?", id(a) == id_avant)
# Int : += crée un nouvel objet
n = 10
id_avant = id(n)
n += 1
print("Même objet ?", id(n) == id_avant)Même objet ? True
Même objet ? False
Le TOSA adore cette subtilité.
a = "Alice"
b = a
b += " Dupont" # crée une nouvelle chaîne
print("a :", a) # inchangé
print("b :", b)a : Alice
b : Alice Dupont
On l’a déjà vu en chapitre 1, mais il mérite d’être rappelé ici dans son contexte théorique : c’est une conséquence directe de la mutabilité.
def ajouter_au_panier(article, panier=[]):
panier.append(article)
return panier
print(ajouter_au_panier("pomme"))
print(ajouter_au_panier("lait"))
print(ajouter_au_panier("pain"))['pomme']
['pomme', 'lait']
['pomme', 'lait', 'pain']
Le panier=[] est évalué une seule fois, à la définition de la fonction. Cette liste est partagée entre tous les appels.
La solution canonique :
def ajouter_au_panier(article, panier=None):
if panier is None:
panier = [] # nouvelle liste à CHAQUE appel
panier.append(article)
return panier
print(ajouter_au_panier("pomme"))
print(ajouter_au_panier("lait"))
print(ajouter_au_panier("pain"))['pomme']
['lait']
['pain']
= ne copie pas : il crée un alias (étiquette supplémentaire) vers le même objet.is teste l’identité (même objet), == teste l’égalité (même valeur)..copy(), list(...), ou [:].copy.deepcopy() (quand il y a des imbrications).int, float, str, bool, tuple, frozenset, None. → pas de piège d’alias.list, dict, set. → attention aux modifications partagées.None + test.Que s’affiche-t-il ?
a = [1, 2, 3]
b = a
b.append(4)
print(a)[1, 2, 3][1, 2, 3, 4][4, 1, 2, 3]b) [1, 2, 3, 4] — b = a crée un alias. Les deux variables pointent vers la même liste. b.append(4) modifie l’objet, donc a voit le changement.
a = [1, 2, 3]
b = a
b.append(4)
print(a)[1, 2, 3, 4]
Que s’affiche-t-il ?
a = [1, 2, 3]
b = a[:]
b.append(4)
print(a)[1, 2, 3][1, 2, 3, 4][4, 1, 2, 3]a) [1, 2, 3] — b = a[:] fait une copie par slicing. b est une nouvelle liste, donc modifier b n’affecte pas a.
a = [1, 2, 3]
b = a[:]
b.append(4)
print(a)
print(b)[1, 2, 3]
[1, 2, 3, 4]
Quelle expression teste correctement l’absence de valeur ?
if x == None:if x is None:if not x:if x = None:b) if x is None: — idiome Python correct. L’option a) fonctionne mais n’est pas recommandée. L’option c) est fausse : elle est vraie aussi pour 0, "", [] (valeurs falsy). L’option d) est une affectation (erreur de syntaxe).
Quel type est mutable ?
intstrtuplelistd) list — les listes, dicts et sets sont mutables. Entiers, chaînes, tuples, booléens, None sont immuables.
Que s’affiche-t-il ?
def vider(l):
l = []
a = [1, 2, 3]
vider(a)
print(a)[][1, 2, 3]Noneb) [1, 2, 3] — l = [] est une réaffectation locale : la variable locale l pointe vers une nouvelle liste vide, mais a (extérieur) n’est pas touchée.
Pour vider réellement a, il faudrait l.clear() ou del l[:].
def vider(l):
l = [] # réaffectation locale
def vider_correct(l):
l.clear() # modification en place
a = [1, 2, 3]
vider(a)
print("Après vider :", a)
vider_correct(a)
print("Après vider_correct :", a)Après vider : [1, 2, 3]
Après vider_correct : []
Que s’affiche-t-il ?
a = [1, 2]
b = a
a += [3]
print(b)[1, 2][1, 2, 3][3]b) [1, 2, 3] — pour une liste, += modifie en place (comme extend). Comme a et b sont des alias, b voit la modification.
a = [1, 2]
b = a
a += [3]
print("a :", a)
print("b :", b)a : [1, 2, 3]
b : [1, 2, 3]
À comparer avec une réaffectation complète :
a = [1, 2]
b = a
a = a + [3] # crée une NOUVELLE liste
print("a :", a)
print("b :", b) # inchangéa : [1, 2, 3]
b : [1, 2]
Que s’affiche-t-il ?
def f(x=[]):
x.append(1)
return x
print(f())
print(f())[1] puis [1][1] puis [1, 1][1, 1] puis [1, 1]b) [1] puis [1, 1] — le défaut mutable [] est évalué une seule fois à la définition. Elle est partagée entre tous les appels.
def f(x=[]):
x.append(1)
return x
print(f())
print(f())
print(f())[1]
[1, 1]
[1, 1, 1]
Pour éviter : def f(x=None): puis if x is None: x = [].
Combien d’objets différents existent en mémoire après ce code ?
a = [1, 2, 3]
b = a
c = list(a)
d = a[:]c) 3 — a et b sont alias (même objet). c est une nouvelle liste (via list()). d est encore une autre nouvelle liste (via [:]). Soit 3 objets distincts.
a = [1, 2, 3]
b = a
c = list(a)
d = a[:]
print(f"a et b même objet : {a is b}")
print(f"a et c même objet : {a is c}")
print(f"a et d même objet : {a is d}")
print(f"c et d même objet : {c is d}")a et b même objet : True
a et c même objet : False
a et d même objet : False
c et d même objet : False
Pour chaque paire ci-dessous, dites si a et b pointent vers le même objet.
# Cas 1
a = [1, 2, 3]
b = a
# Cas 2
a = [1, 2, 3]
b = a.copy()
# Cas 3
a = "Bonjour"
b = a
# Cas 4
a = [1, 2, 3]
b = list(a)
# Cas 5
a = (1, 2, 3)
b = aPour chaque cas, prévoyez la valeur de a is b, puis vérifiez.
# Cas 1 : alias
a = [1, 2, 3]
b = a
print("Cas 1 :", a is b) # True
# Cas 2 : copie
a = [1, 2, 3]
b = a.copy()
print("Cas 2 :", a is b) # False
# Cas 3 : str immuable
a = "Bonjour"
b = a
print("Cas 3 :", a is b) # True (mais peu importe, car immuable)
# Cas 4 : copie via list()
a = [1, 2, 3]
b = list(a)
print("Cas 4 :", a is b) # False
# Cas 5 : tuple immuable
a = (1, 2, 3)
b = a
print("Cas 5 :", a is b) # TrueCas 1 : True
Cas 2 : False
Cas 3 : True
Cas 4 : False
Cas 5 : True
Pourquoi ça importe : seuls les mutables aliasés peuvent créer des surprises. Pour les immuables, même s’il y a « alias », rien ne peut être modifié, donc pas de piège.
Voici une fonction qui sabote les données qu’on lui passe :
def doubler_tous(notes):
for i in range(len(notes)):
notes[i] = notes[i] * 2
return notes
notes_originales = [10, 12, 14]
notes_doublees = doubler_tous(notes_originales)
print("Originales :", notes_originales)
print("Doublées :", notes_doublees)Corrigez la fonction pour qu’elle renvoie une nouvelle liste sans modifier l’originale.
Le code ci-dessus. À vous de :
Problème : la fonction modifie notes en place (via notes[i] = ...). Or, comme notes et notes_originales pointent vers la même liste, l’originale est impactée.
Correction 1 — créer une nouvelle liste :
def doubler_tous(notes):
resultat = []
for n in notes:
resultat.append(n * 2)
return resultat
notes_originales = [10, 12, 14]
notes_doublees = doubler_tous(notes_originales)
print("Originales :", notes_originales)
print("Doublées :", notes_doublees)Originales : [10, 12, 14]
Doublées : [20, 24, 28]
Correction 2 — copier d’abord :
def doubler_tous(notes):
notes = notes.copy() # on travaille sur une copie
for i in range(len(notes)):
notes[i] = notes[i] * 2
return notes
notes_originales = [10, 12, 14]
notes_doublees = doubler_tous(notes_originales)
print("Originales :", notes_originales)
print("Doublées :", notes_doublees)Originales : [10, 12, 14]
Doublées : [20, 24, 28]
Les deux versions sont correctes. La version 1 est plus idiomatique (surtout qu’elle se réécrit en une ligne avec une compréhension — vu en Partie 3).
Prédisez la sortie de ce code, puis vérifiez et corrigez-le pour qu’il produise [1] trois fois.
def enregistrer(valeur, historique=[]):
historique.append(valeur)
return historique
print(enregistrer(1))
print(enregistrer(1))
print(enregistrer(1))Code ci-dessus. Expliquez le comportement et corrigez.
Ce que ça affiche (incorrect) :
def enregistrer(valeur, historique=[]):
historique.append(valeur)
return historique
print(enregistrer(1))
print(enregistrer(1))
print(enregistrer(1))[1]
[1, 1]
[1, 1, 1]
La liste [] est créée une seule fois à la définition, et partagée entre les appels.
Version corrigée :
def enregistrer(valeur, historique=None):
if historique is None:
historique = [] # nouvelle liste à chaque appel
historique.append(valeur)
return historique
print(enregistrer(1))
print(enregistrer(1))
print(enregistrer(1))[1]
[1]
[1]
Et si on veut vraiment un historique partagé (cas où c’est volontaire), on peut utiliser une variable globale ou une classe — à voir plus tard.
← Chapitre précédent : Itération avancée • Chapitre suivant : Logique booléenne →