Python & AI Tutorials Logo
Programmation Python

18. Le modèle de données et d’objets de Python : références, comparaisons et copies

Comprendre comment Python stocke et gère les données est essentiel pour écrire des programmes corrects. Dans ce chapitre, nous allons explorer le modèle objet de Python — le système fondamental qui régit le fonctionnement de toutes les données en Python. Vous apprendrez pourquoi certaines affectations créent des copies indépendantes tandis que d’autres créent des références partagées, comment comparer correctement des objets, et comment éviter des pièges courants lorsque vous travaillez avec des collections.

Ces connaissances vous aideront à comprendre des comportements surprenants que vous avez peut-être déjà rencontrés, comme le fait que modifier une liste affecte parfois une autre, ou pourquoi comparer deux listes avec == donne des résultats différents de la comparaison avec is.

18.1) Tout est un objet en Python

En Python, chaque donnée est un objet. Ce n’est pas seulement un concept théorique — cela a des implications pratiques sur le fonctionnement de vos programmes.

Lorsque vous créez un nombre, une chaîne de caractères, une liste, ou toute autre valeur, Python crée un objet en mémoire. Un objet est un conteneur qui contient :

  • Les données réelles (la valeur)
  • Des informations sur le type de données (le type)
  • Un identifiant unique (l’identité)

Voyons cela en pratique :

python
# Création de différents types d'objets
number = 42
text = "Hello"
items = [1, 2, 3]
 
# Chacune de ces variables référence un objet en mémoire
print(number)  # Output: 42
print(text)    # Output: Hello
print(items)   # Output: [1, 2, 3]

Même des valeurs simples comme les entiers sont des objets. Cela signifie qu’ils ont des capacités au-delà du simple stockage d’un nombre :

python
# Les entiers sont des objets avec des méthodes
number = 42
print(number.bit_length())  # Output: 6
 
# Les chaînes sont des objets avec des méthodes
text = "hello"
print(text.upper())  # Output: HELLO
 
# Les listes sont des objets avec des méthodes
items = [3, 1, 2]
items.sort()
print(items)  # Output: [1, 2, 3]

Pourquoi est-ce important ? Parce que lorsque vous affectez une variable ou passez des données à une fonction, vous ne copiez pas l’objet — vous créez une référence vers le même objet. C’est fondamentalement différent de la manière dont fonctionnent certains autres langages de programmation, et comprendre cette distinction évitera de nombreux bugs déroutants.

python
# Création d'un objet liste
original = [1, 2, 3]
 
# Cela ne crée pas une nouvelle liste - cela crée une autre référence
# vers le MÊME objet liste
another_name = original
 
# Modifier via une référence affecte l'autre
another_name.append(4)
 
print(original)      # Output: [1, 2, 3, 4]
print(another_name)  # Output: [1, 2, 3, 4]

original et another_name font tous deux référence au même objet liste en mémoire. Lorsque nous modifions la liste via another_name, nous voyons le changement via original parce qu’ils regardent tous deux le même objet.

Variable: original

Objet liste: 1, 2, 3, 4

Variable: another_name

Ce comportement s’appelle la sémantique de référence, et c’est l’un des concepts les plus importants en programmation Python. Nous allons l’explorer en profondeur tout au long de ce chapitre.

18.2) Identité, type et valeur des objets

Chaque objet en Python a trois caractéristiques fondamentales qui le définissent : identité, type, et valeur. Comprendre ces caractéristiques vous aide à raisonner sur le comportement des objets et à les comparer correctement.

18.2.1) Identité d’objet avec id()

L’identité d’un objet est un nombre unique que Python assigne lorsque l’objet est créé. Cette identité ne change jamais durant la vie de l’objet — c’est comme une adresse permanente en mémoire.

Vous pouvez récupérer l’identité d’un objet en utilisant la fonction id() :

python
# Création d'objets et vérification de leurs identités
x = [1, 2, 3]
y = [1, 2, 3]
z = x
 
print(id(x))  # Output: 140234567890123 (example - actual number varies)
print(id(y))  # Output: 140234567890456 (different from x)
print(id(z))  # Output: 140234567890123 (same as x)

Les nombres réels que vous verrez seront différents à chaque exécution du programme, mais le schéma reste le même : x et y ont des identités différentes parce que ce sont des objets différents, même s’ils contiennent les mêmes valeurs. En revanche, z a la même identité que x parce que z n’est qu’un autre nom pour le même objet.

Voici un exemple pratique montrant pourquoi l’identité compte :

python
# Deux élèves avec les mêmes notes
student1_grades = [85, 90, 92]
student2_grades = [85, 90, 92]
 
# Ce sont des objets différents (identités différentes)
print(id(student1_grades))  # Output: 140234567890123 (example)
print(id(student2_grades))  # Output: 140234567890456 (different)
 
# Modifier l'un n'affecte pas l'autre
student1_grades.append(88)
print(student1_grades)  # Output: [85, 90, 92, 88]
print(student2_grades)  # Output: [85, 90, 92]

Considérez maintenant un scénario différent :

python
# Les notes d'un élève suivies par deux variables
original_grades = [85, 90, 92]
backup_reference = original_grades
 
# Elles référencent le MÊME objet (même identité)
print(id(original_grades))    # Output: 140234567890123 (example)
print(id(backup_reference))   # Output: 140234567890123 (same!)
 
# Modifier via l'un ou l'autre nom affecte les deux
backup_reference.append(88)
print(original_grades)     # Output: [85, 90, 92, 88]
print(backup_reference)    # Output: [85, 90, 92, 88]

Idée clé : Quand deux variables ont la même identité, elles font référence exactement au même objet en mémoire. Les changements effectués via une variable sont visibles via l’autre parce qu’il n’y a qu’un seul objet en cours de modification.

18.2.2) Type d’objet avec type()

Le type d’un objet détermine quel genre de données il contient et quelles opérations vous pouvez effectuer sur lui. Comme nous l’avons appris au Chapitre 3, vous pouvez vérifier le type d’un objet avec la fonction type() :

python
# Différents types d'objets
number = 42
text = "Hello"
items = [1, 2, 3]
mapping = {"name": "Alice"}
 
print(type(number))   # Output: <class 'int'>
print(type(text))     # Output: <class 'str'>
print(type(items))    # Output: <class 'list'>
print(type(mapping))  # Output: <class 'dict'>

Le type d’un objet ne change jamais après sa création. Vous ne pouvez pas transformer un entier en chaîne — vous pouvez seulement créer un nouvel objet chaîne à partir de la valeur de l’entier :

python
# Le type est fixé à la création
x = 42
print(type(x))  # Output: <class 'int'>
 
# Cela ne change pas le type de x - cela crée un NOUVEL objet chaîne
# et fait que x référence ce nouvel objet à la place
x = str(x)
# L'objet entier original (42) existe toujours en mémoire jusqu'à collecte par le GC
# x pointe maintenant vers un objet complètement différent : la chaîne "42"
 
print(type(x))  # Output: <class 'str'>
print(x)        # Output: 42 (maintenant une chaîne, pas un entier)

Comprendre les types est crucial parce que différents types prennent en charge différentes opérations :

python
# Les listes prennent en charge append
grades = [85, 90]
grades.append(92)
print(grades)  # Output: [85, 90, 92]
 
# Les chaînes n'ont pas append - elles sont immuables
text = "Hello"
# text.append(" World")  # AttributeError: 'str' object has no attribute 'append'
 
# Mais les chaînes prennent en charge la concaténation
text = text + " World"
print(text)  # Output: Hello World

18.2.3) Valeur d’objet

La valeur d’un objet est la donnée réelle qu’il contient. Contrairement à l’identité et au type, la valeur peut changer pour les objets mutables (comme les listes et les dictionnaires) mais ne peut pas changer pour les objets immuables (comme les entiers et les chaînes).

python
# Pour les objets mutables, la valeur peut changer
shopping_cart = ["milk", "bread"]
print(shopping_cart)  # Output: ['milk', 'bread']
 
shopping_cart.append("eggs")
print(shopping_cart)  # Output: ['milk', 'bread', 'eggs']
# Même objet (même identité), valeur différente
 
# Pour les objets immuables, la valeur ne peut pas changer
count = 5
print(count)  # Output: 5
 
count = count + 1
print(count)  # Output: 6
# Cela a créé un NOUVEL objet avec une nouvelle identité

Voici un exemple complet montrant les trois caractéristiques :

python
# Création d'un objet liste
data = [10, 20, 30]
 
print("Identity:", id(data))      # Output: Identity: 140234567890123 (example)
print("Type:", type(data))        # Output: Type: <class 'list'>
print("Value:", data)             # Output: Value: [10, 20, 30]
 
# Modification de la valeur (l'identité et le type restent identiques)
data.append(40)
 
print("Identity:", id(data))      # Output: Identity: 140234567890123 (unchanged)
print("Type:", type(data))        # Output: Type: <class 'list'> (unchanged)
print("Value:", data)             # Output: Value: [10, 20, 30, 40] (changed)

Objet

Identité : ID unique

Type : class 'list'

Valeur : 10, 20, 30, 40

Ne change jamais

Ne change jamais

Peut changer pour les types mutables

Comprendre ces trois caractéristiques vous aide à prédire comment les objets vont se comporter dans vos programmes. L’identité vous dit si deux variables font référence au même objet, le type vous dit quelles opérations sont autorisées, et la valeur vous dit quelles données l’objet contient actuellement.

18.3) Types mutables et immuables

L’une des distinctions les plus importantes en Python est celle entre les types mutables et immuables. Cette distinction affecte la façon dont les objets se comportent quand vous essayez de les modifier, et la comprendre évite de nombreuses erreurs de programmation courantes.

18.3.1) Types immuables : des valeurs qui ne peuvent pas changer

Un objet immuable est un objet dont la valeur ne peut pas être changée après sa création. Lorsque vous effectuez une opération qui semble modifier un objet immuable, Python crée en réalité un nouvel objet avec la valeur modifiée.

Les types immuables de Python incluent :

  • Entiers (int)
  • Nombres à virgule flottante (float)
  • Chaînes de caractères (str)
  • Tuples (tuple)
  • Booléens (bool)
  • None (NoneType)

Voyons l’immutabilité en action avec les entiers :

python
# Création d'un entier
x = 100
print("Original x:", x)           # Output: Original x: 100
print("Identity of x:", id(x))    # Output: Identity of x: 140234567890123 (example)
 
# Cela donne l'impression de modifier x, mais on crée en fait un nouvel objet
x = x + 1
print("Modified x:", x)           # Output: Modified x: 101
print("Identity of x:", id(x))    # Output: Identity of x: 140234567890456 (different!)

L’identité a changé parce que x = x + 1 a créé un objet entier entièrement nouveau avec la valeur 101. L’objet d’origine de valeur 100 existe toujours (jusqu’à ce que le ramasse-miettes de Python le supprime), mais x fait maintenant référence à un objet différent.

Les chaînes illustrent l’immutabilité encore plus clairement :

python
# Création d'une chaîne
message = "Hello"
print("Original:", message)        # Output: Original: Hello
print("Identity:", id(message))    # Output: Identity: 140234567890789 (example)
 
# Les méthodes des chaînes ne modifient pas l'original - elles renvoient de nouvelles chaînes
uppercase = message.upper()
print("Original:", message)        # Output: Original: Hello (unchanged)
print("Uppercase:", uppercase)     # Output: Uppercase: HELLO
print("Identity of original:", id(message))    # Output: Identity of original: 140234567890789 (same)
print("Identity of uppercase:", id(uppercase)) # Output: Identity of uppercase: 140234567891012 (different)

Même les opérations qui semblent modifier une chaîne créent en réalité de nouveaux objets chaîne :

python
# Construire une chaîne avec la concaténation
text = "Python"
print("Before:", text, "- ID:", id(text))  # Output: Before: Python - ID: 140234567891234 (example)
 
text = text + " Programming"
print("After:", text, "- ID:", id(text))   # Output: After: Python Programming - ID: 140234567891567 (different)

Pourquoi l’immutabilité est importante : Les objets immuables peuvent être partagés sans risque entre différentes parties de votre programme, car aucune partie ne peut les modifier accidentellement. Cela rend votre code plus prévisible et plus facile à comprendre.

18.3.2) Types mutables : des valeurs qui peuvent changer

Un objet mutable est un objet dont la valeur peut être modifiée après la création sans créer un nouvel objet. L’identité de l’objet reste la même, mais son contenu peut être modifié.

Les types mutables de Python incluent :

  • Listes (list)
  • Dictionnaires (dict)
  • Ensembles (set)

Voyons la mutabilité avec les listes :

python
# Création d'une liste
numbers = [1, 2, 3]
print("Original:", numbers)        # Output: Original: [1, 2, 3]
print("Identity:", id(numbers))    # Output: Identity: 140234567892345 (example)
 
# Modification de la liste - même objet, valeur différente
numbers.append(4)
print("Modified:", numbers)        # Output: Modified: [1, 2, 3, 4]
print("Identity:", id(numbers))    # Output: Identity: 140234567892345 (same!)

L’identité n’a pas changé parce que nous avons modifié l’objet liste existant plutôt que d’en créer un nouveau. C’est fondamentalement différent du fonctionnement des types immuables.

Les dictionnaires et les ensembles sont également mutables :

python
# Exemple de dictionnaire
student = {"name": "Alice", "grade": 85}
print("Before:", student, "- ID:", id(student))  # Output: Before: {'name': 'Alice', 'grade': 85} - ID: 140234567893012 (example)
 
student["grade"] = 90  # Modification du dictionnaire
print("After:", student, "- ID:", id(student))   # Output: After: {'name': 'Alice', 'grade': 90} - ID: 140234567893012 (same)
 
# Exemple d'ensemble
unique_numbers = {1, 2, 3}
print("Before:", unique_numbers, "- ID:", id(unique_numbers))  # Output: Before: {1, 2, 3} - ID: 140234567893345 (example)
 
unique_numbers.add(4)  # Modification de l'ensemble
print("After:", unique_numbers, "- ID:", id(unique_numbers))   # Output: After: {1, 2, 3, 4} - ID: 140234567893345 (same)

18.3.3) Pourquoi la mutabilité compte en pratique

La différence entre les types mutables et immuables devient critique quand plusieurs variables font référence au même objet :

python
# Exemple immuable - partage sûr
x = "Hello"
y = x  # y référence le même objet chaîne que x
 
# "Modifier" x crée un nouvel objet
x = x + " World"
 
print(x)  # Output: Hello World
print(y)  # Output: Hello (unchanged - y still refers to the original)
python
# Exemple mutable - modifications partagées
list1 = [1, 2, 3]
list2 = list1  # list2 référence le MÊME objet liste
 
# Modifier via list1 affecte list2
list1.append(4)
 
print(list1)  # Output: [1, 2, 3, 4]
print(list2)  # Output: [1, 2, 3, 4] (also changed!)

Types immuables

int, float, str, tuple, bool, None

La valeur ne peut pas changer

Les opérations créent de nouveaux objets

Sûr à partager

Types mutables

list, dict, set

La valeur peut changer

Les opérations modifient l'objet existant

Le partage exige de la prudence

Comprendre la mutabilité est essentiel pour :

  1. Prédire le comportement : savoir si une opération crée un nouvel objet ou modifie un objet existant
  2. Éviter les bugs : prévenir des modifications involontaires lorsque des objets sont partagés
  3. Écrire du code efficace : choisir le bon type pour votre cas d’usage
  4. Comprendre le comportement des fonctions : savoir quand les paramètres de fonction peuvent être modifiés

Dans les sections suivantes, nous allons explorer comment l’affectation fonctionne avec ces différents types et comment créer des copies indépendantes lorsque c’est nécessaire.

18.4) Comment l’affectation fonctionne avec les objets

L’affectation en Python ne copie pas les objets — elle crée des références vers des objets. Comprendre cette distinction est crucial pour écrire des programmes corrects, en particulier lorsque vous travaillez avec des types mutables.

18.4.1) L’affectation crée des références, pas des copies

Quand vous écrivez x = y, Python ne crée pas une copie de l’objet auquel y fait référence. À la place, il fait en sorte que x référence le même objet que y. Les deux variables deviennent des noms pour le même objet en mémoire.

Voyons cela avec des objets immuables d’abord :

python
# Affectation avec des entiers (immuables)
a = 100
b = a  # b référence maintenant le même objet entier que a
 
print("a:", a)           # Output: a: 100
print("b:", b)           # Output: b: 100
print("Same object?", id(a) == id(b))  # Output: Same object? True
 
# "Modifier" a crée un nouvel objet
a = a + 1
 
print("a:", a)           # Output: a: 101
print("b:", b)           # Output: b: 100 (unchanged)
print("Same object?", id(a) == id(b))  # Output: Same object? False

Avec les objets immuables, ce comportement est généralement sûr, parce que vous ne pouvez pas modifier l’objet d’origine. Lorsque vous effectuez une opération qui change la valeur, Python crée un nouvel objet.

Cependant, avec des objets mutables, le comportement est très différent :

python
# Affectation avec des listes (mutables)
list1 = [1, 2, 3]
list2 = list1  # list2 référence le MÊME objet liste que list1
 
print("list1:", list1)   # Output: list1: [1, 2, 3]
print("list2:", list2)   # Output: list2: [1, 2, 3]
print("Same object?", id(list1) == id(list2))  # Output: Same object? True
 
# Modifier via list1 affecte list2
list1.append(4)
 
print("list1:", list1)   # Output: list1: [1, 2, 3, 4]
print("list2:", list2)   # Output: list2: [1, 2, 3, 4] (also changed!)
print("Same object?", id(list1) == id(list2))  # Output: Same object? True

list1 et list2 sont tous deux des noms pour le même objet liste. Quand vous modifiez la liste via l’un ou l’autre nom, vous voyez le changement via les deux noms parce qu’il n’y a qu’une seule liste.

Affectation avec des types immuables

Les deux variables référencent le même objet au départ

Les opérations créent de nouveaux objets

Les variables deviennent indépendantes

Affectation avec des types mutables

Les deux variables référencent le même objet

Les opérations modifient un objet partagé

Les changements sont visibles via les deux variables

Voici un exemple pratique qui montre pourquoi cela compte :

python
# Gestion des notes d'un élève
alice_grades = [85, 90, 92]
backup_grades = alice_grades  # Tentative de créer une sauvegarde
 
print("Original:", alice_grades)  # Output: Original: [85, 90, 92]
print("Backup:", backup_grades)   # Output: Backup: [85, 90, 92]
 
# Ajout d'une nouvelle note
alice_grades.append(88)
 
# La "sauvegarde" a aussi été modifiée !
print("Original:", alice_grades)  # Output: Original: [85, 90, 92, 88]
print("Backup:", backup_grades)   # Output: Backup: [85, 90, 92, 88]

Ce n’est pas du tout une sauvegarde — les deux variables font référence à la même liste. Pour créer une véritable sauvegarde, vous devez créer une copie (ce que nous verrons en Section 18.8).

18.4.2) Affectation lors des appels de fonction

Lorsque vous passez un argument à une fonction, Python utilise la même sémantique de référence. Le paramètre devient un autre nom pour le même objet :

python
# Fonction avec paramètre immuable
def increment(number):
    number = number + 1  # Crée un nouvel objet
    return number
 
value = 5
result = increment(value)
 
print("Original value:", value)    # Output: Original value: 5 (unchanged)
print("Returned result:", result)  # Output: Returned result: 6

Le paramètre number fait initialement référence au même objet entier que value. Quand nous faisons number = number + 1, nous créons un nouvel objet entier et faisons en sorte que number le référence. L’objet d’origine (et value) reste inchangé.

Avec des objets mutables, le comportement est différent :

python
# Fonction avec paramètre mutable
def add_item(items, new_item):
    items.append(new_item)  # Modifie la liste d'origine
 
shopping_list = ["milk", "bread"]
add_item(shopping_list, "eggs")
 
print("Original list:", shopping_list)  # Output: Original list: ['milk', 'bread', 'eggs']

Le paramètre items fait référence au même objet liste que shopping_list. Quand nous modifions la liste via items, nous modifions la liste d’origine.

Voici une erreur courante et comment l’éviter :

python
# ERREUR : modification involontaire de l'original
def process_grades(grades):
    grades.append(100)  # Modifie l'original !
    return grades
 
student_grades = [85, 90, 92]
processed = process_grades(student_grades)
 
print("Original:", student_grades)  # Output: Original: [85, 90, 92, 100] (modified!)
print("Processed:", processed)      # Output: Processed: [85, 90, 92, 100]
 
# CORRECT : créer une copie si vous ne voulez pas modifier l'original
def process_grades_safely(grades):
    # Créer une nouvelle liste avec les mêmes éléments
    result = grades + [100]  # La concaténation crée une nouvelle liste
    return result
 
student_grades = [85, 90, 92]
processed = process_grades_safely(student_grades)
 
print("Original:", student_grades)  # Output: Original: [85, 90, 92] (unchanged)
print("Processed:", processed)      # Output: Processed: [85, 90, 92, 100]

Remarque importante sur les arguments par défaut mutables : Un autre piège courant connexe consiste à utiliser des objets mutables comme valeurs par défaut des paramètres (comme def func(items=[]):). Les paramètres par défaut sont créés une seule fois lorsque la fonction est définie, et non à chaque appel, ce qui peut conduire à un comportement inattendu où la liste par défaut accumule des valeurs sur plusieurs appels de fonction. Nous explorerons cela en détail au Chapitre 20, mais sachez que c’est une source fréquente de bugs lorsque vous travaillez avec des paramètres mutables.

18.5) Sémantique de référence et aliasing d’objets

La sémantique de référence signifie qu’en Python, les variables sont des noms qui font référence à des objets, et non des conteneurs qui contiennent des valeurs. Quand plusieurs variables font référence au même objet, on appelle cela l’aliasing. Comprendre l’aliasing est essentiel pour prédire le comportement de vos programmes.

18.5.1) Qu’est-ce que l’aliasing ?

L’aliasing se produit lorsque deux variables ou plus font référence au même objet en mémoire. Les variables sont des « alias » les unes des autres — des noms différents pour la même chose.

Voyons l’aliasing avec un exemple simple :

python
# Création d'une liste et d'un alias
original = [1, 2, 3]
alias = original  # alias référence la même liste que original
 
print("Original:", original)  # Output: Original: [1, 2, 3]
print("Alias:", alias)        # Output: Alias: [1, 2, 3]
print("Same object?", id(original) == id(alias))  # Output: Same object? True
 
# Modification via l'alias
alias.append(4)
 
# Le changement est visible via les deux noms
print("Original:", original)  # Output: Original: [1, 2, 3, 4]
print("Alias:", alias)        # Output: Alias: [1, 2, 3, 4]

Il n’y a qu’un seul objet liste en mémoire, mais il a deux noms : original et alias. Toute modification effectuée via l’un ou l’autre nom affecte le même objet sous-jacent.

Voici un exemple plus réaliste avec des dossiers d’élèves :

python
# Base d'élèves avec aliasing
students = {
    "alice": {"name": "Alice", "grade": 85},
    "bob": {"name": "Bob", "grade": 90}
}
 
# Création d'un alias vers le dossier d'Alice
alice_record = students["alice"]
 
print("Alice's grade:", alice_record["grade"])  # Output: Alice's grade: 85
 
# Modification via l'alias
alice_record["grade"] = 95
 
# Le changement est visible dans le dictionnaire original
print("Updated grade:", students["alice"]["grade"])  # Output: Updated grade: 95

La variable alice_record est un alias pour le dictionnaire stocké à students["alice"]. Lorsque nous modifions alice_record, nous modifions le même dictionnaire qui est stocké dans le dictionnaire students.

18.5.2) Détecter l’aliasing avec l’opérateur is

Vous pouvez vérifier si deux variables sont des alias (font référence au même objet) avec l’opérateur is :

python
# Vérification de l'aliasing
list1 = [1, 2, 3]
list2 = list1      # Alias
list3 = [1, 2, 3]  # Objet différent avec la même valeur
 
print("list1 is list2:", list1 is list2)  # Output: list1 is list2: True (aliases)
print("list1 is list3:", list1 is list3)  # Output: list1 is list3: False (different objects)
print("list1 == list3:", list1 == list3)  # Output: list1 == list3: True (same value)

L’opérateur is vérifie l’identité (si deux variables font référence au même objet), tandis que == vérifie la valeur (si deux objets ont le même contenu). Nous explorerons cette distinction en détail dans la Section 18.6.

18.5.3) Aliasing dans les collections

L’aliasing devient plus complexe lorsque des objets sont stockés dans des collections :

python
# Création d'une liste de listes
row = [0, 0, 0]
grid = [row, row, row]  # Les trois éléments sont des alias de la même liste !
 
print("Grid:")
for r in grid:
    print(r)
# Output:
# [0, 0, 0]
# [0, 0, 0]
# [0, 0, 0]
 
# Modifier un élément affecte toutes les lignes
grid[0][0] = 1
 
print("\nAfter modification:")
for r in grid:
    print(r)
# Output:
# [1, 0, 0]
# [1, 0, 0]
# [1, 0, 0]

C’est une erreur courante lorsqu’on essaie de créer une grille 2D. Les trois lignes sont des alias de la même liste, donc modifier une ligne modifie toutes les autres.

La bonne façon de créer des lignes indépendantes :

python
# Création de lignes indépendantes
grid = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]  # Chaque ligne est une liste séparée
 
print("Grid:")
for r in grid:
    print(r)
# Output:
# [0, 0, 0]
# [0, 0, 0]
# [0, 0, 0]
 
# Maintenant, modifier un élément n'affecte que cette ligne
grid[0][0] = 1
 
print("\nAfter modification:")
for r in grid:
    print(r)
# Output:
# [1, 0, 0]
# [0, 0, 0]
# [0, 0, 0]

18.6) Égalité, identité et appartenance (==, is et in) selon les types

Python fournit trois opérateurs fondamentaux pour comparer et vérifier des relations entre objets : == pour l’égalité, is pour l’identité, et in pour l’appartenance. Comprendre quand utiliser chaque opérateur est crucial pour écrire des programmes corrects.

18.6.1) Égalité avec == (comparer des valeurs)

L’opérateur == vérifie si deux objets ont la même valeur. Peu importe qu’il s’agisse du même objet en mémoire — seules leurs valeurs (contenus) comptent.

python
# Comparer des valeurs avec ==
list1 = [1, 2, 3]
list2 = [1, 2, 3]
list3 = list1
 
print(list1 == list2)  # Output: True (same values)
print(list1 == list3)  # Output: True (same values)

Même si list1 et list2 sont des objets différents en mémoire, ils ont la même valeur, donc == renvoie True.

Voici comment == fonctionne avec différents types :

python
# Égalité entre différents types
print(42 == 42)              # Output: True (same integer value)
print(42 == 42.0)            # Output: True (integer equals float with same value)
print("hello" == "hello")    # Output: True (same string value)
print([1, 2] == [1, 2])      # Output: True (same list contents)
print({"a": 1} == {"a": 1})  # Output: True (same dictionary contents)
 
# Valeurs différentes
print(42 == 43)              # Output: False
print("hello" == "Hello")    # Output: False (case-sensitive)
print([1, 2] == [2, 1])      # Output: False (order matters)

Pour les collections, == effectue une comparaison profonde — il vérifie si tous les éléments sont égaux :

python
# Comparaison profonde avec des structures imbriquées
list1 = [[1, 2], [3, 4]]
list2 = [[1, 2], [3, 4]]
 
print(list1 == list2)  # Output: True (all nested elements are equal)
 
# Même si les listes internes sont des objets différents
print(id(list1[0]) == id(list2[0]))  # Output: False (different objects)
print(list1[0] == list2[0])          # Output: True (same values)

18.6.2) Identité avec is (comparer l’identité des objets)

L’opérateur is vérifie si deux variables font référence au même objet en mémoire. Il compare les identités, pas les valeurs.

python
# Comparer des identités avec is
list1 = [1, 2, 3]
list2 = [1, 2, 3]
list3 = list1
 
print(list1 is list2)  # Output: False (different objects)
print(list1 is list3)  # Output: True (same object)
 
# Confirmation avec id()
print(id(list1) == id(list2))  # Output: False
print(id(list1) == id(list3))  # Output: True

Quand utiliser is : L’usage le plus courant de is est de vérifier None :

python
# Vérifier None (la bonne manière)
def find_student(name, students):
    """Return student record or None if not found."""
    for student in students:
        if student["name"] == name:
            return student
    return None
 
students = [
    {"name": "Alice", "grade": 85},
    {"name": "Bob", "grade": 90}
]
 
result = find_student("Charlie", students)
 
# Utiliser 'is' pour tester None
if result is None:
    print("Student not found")  # Output: Student not found
else:
    print(f"Found: {result}")

18.6.3) Appartenance avec in (vérifier la présence)

L’opérateur in vérifie si une valeur est contenue dans une collection. Il fonctionne avec les chaînes, les listes, les tuples, les ensembles et les dictionnaires :

python
# Appartenance dans différents types
print(2 in [1, 2, 3])           # Output: True
print("hello" in "hello world")  # Output: True
print("x" in {"x": 10, "y": 20}) # Output: True (checks keys)
print(5 in {1, 2, 3, 4, 5})     # Output: True

Pour les dictionnaires, in vérifie si une clé existe :

python
# Vérifier l'appartenance dans un dictionnaire
student = {"name": "Alice", "grade": 85, "age": 20}
 
print("name" in student)    # Output: True (key exists)
print("Alice" in student)   # Output: False (value, not key)
print("grade" in student)   # Output: True (key exists)
 
# Vérifier des valeurs nécessite d'accéder à .values()
print("Alice" in student.values())  # Output: True

L’opérateur not in vérifie l’absence :

python
# Vérifier l'absence
shopping_list = ["milk", "bread", "eggs"]
 
if "butter" not in shopping_list:
    print("Don't forget to buy butter!")  # Output: Don't forget to buy butter!

Résumé de quand utiliser chaque opérateur :

  • Utilisez == quand vous voulez vérifier si deux objets ont la même valeur
  • Utilisez is quand vous voulez vérifier si deux variables font référence au même objet (le plus souvent avec None, ou lors du débogage de l’aliasing)
  • Utilisez in quand vous voulez vérifier si une valeur est contenue dans une collection

Comprendre ces distinctions vous aide à écrire des comparaisons plus précises et correctes dans vos programmes.

18.7) Comparer des objets qui contiennent d’autres objets

Lorsque des objets contiennent d’autres objets (comme des listes dans des listes, ou des dictionnaires contenant des listes), les comparaisons deviennent plus nuancées. Comprendre comment Python compare des structures imbriquées est essentiel pour travailler avec des données complexes.

18.7.1) Comment == fonctionne avec des structures imbriquées

L’opérateur == effectue une comparaison récursive pour les structures imbriquées. Il compare non seulement le conteneur externe, mais aussi tous les objets imbriqués :

python
# Comparer des listes imbriquées
list1 = [[1, 2], [3, 4]]
list2 = [[1, 2], [3, 4]]
 
print(list1 == list2)  # Output: True
 
# Même si les listes internes sont des objets différents
print(id(list1[0]) == id(list2[0]))  # Output: False
print(list1[0] == list2[0])          # Output: True

Python compare récursivement chaque élément. Pour que list1 == list2 soit True, chaque élément correspondant doit être égal, y compris les éléments imbriqués.

Voici un exemple plus complexe :

python
# Structure imbriquée à plusieurs niveaux
data1 = {
    "students": [
        {"name": "Alice", "grades": [85, 90, 92]},
        {"name": "Bob", "grades": [88, 91, 87]}
    ],
    "class": "Python 101"
}
 
data2 = {
    "students": [
        {"name": "Alice", "grades": [85, 90, 92]},
        {"name": "Bob", "grades": [88, 91, 87]}
    ],
    "class": "Python 101"
}
 
print(data1 == data2)  # Output: True

Python compare :

  1. Les clés et valeurs du dictionnaire au niveau supérieur ("students" et "class")
  2. La liste des élèves
  3. Chaque dictionnaire d’élève (avec les clés "name" et "grades")
  4. La liste des notes pour chaque élève
  5. Chaque note individuelle

Tous les niveaux doivent correspondre pour que la comparaison renvoie True.

18.7.2) L’ordre compte pour les séquences

Pour les séquences (listes et tuples), l’ordre des éléments compte :

python
# L'ordre compte dans les listes
list1 = [[1, 2], [3, 4]]
list2 = [[3, 4], [1, 2]]
 
print(list1 == list2)  # Output: False (different order)
 
# Mais l'ordre ne compte pas pour les ensembles
set1 = {frozenset([1, 2]), frozenset([3, 4])}
set2 = {frozenset([3, 4]), frozenset([1, 2])}
 
print(set1 == set2)  # Output: True (sets are unordered)

18.7.3) Comparer des collections de types différents

Différents types de collections (list, tuple, set) ne sont jamais égaux entre eux, même s’ils contiennent les mêmes éléments :

python
# Comparer des types différents
print([1, 2, 3] == (1, 2, 3))  # Output: False (list vs tuple)
print([1, 2, 3] == {1, 2, 3})  # Output: False (list vs set)
 
# Même avec les mêmes éléments
list_version = [1, 2, 3]
tuple_version = (1, 2, 3)
set_version = {1, 2, 3}
 
print(list_version == tuple_version)  # Output: False
print(list_version == set_version)    # Output: False
print(tuple_version == set_version)   # Output: False

18.8) Copies superficielles de listes, dictionnaires et ensembles

Lorsque vous travaillez avec des objets mutables, vous devez souvent créer des copies indépendantes pour éviter des modifications non intentionnelles. Par exemple, lorsqu’on sauvegarde des données avant de les traiter, qu’on crée des scénarios de test sans affecter les données de production, ou qu’on passe des données à des fonctions qui ne devraient pas modifier l’original. Comprendre comment fonctionnent les mécanismes de copie de Python vous aide à créer des copies réellement indépendantes lorsque c’est nécessaire.

Cependant, toutes les méthodes de copie ne créent pas des copies complètement indépendantes. Comprendre la différence entre les copies superficielles et les copies profondes est crucial pour éviter des bugs subtils.

18.8.1) Qu’est-ce qu’une copie superficielle ?

Une copie superficielle crée un nouvel objet, mais ne crée pas de copies des objets qu’il contient. À la place, le nouvel objet contient des références vers les mêmes objets imbriqués que l’original.

Voyons cela avec une liste simple :

python
# Création d'une copie superficielle d'une liste simple
original = [1, 2, 3]
copy = original.copy()  # Crée une copie superficielle
 
print("Original:", original)  # Output: Original: [1, 2, 3]
print("Copy:", copy)          # Output: Copy: [1, 2, 3]
 
# Ce sont des objets différents
print("Same object?", original is copy)  # Output: Same object? False
 
# Modifier la copie n'affecte pas l'original
copy.append(4)
 
print("Original:", original)  # Output: Original: [1, 2, 3]
print("Copy:", copy)          # Output: Copy: [1, 2, 3, 4]

Pour des listes simples contenant des objets immuables (comme des entiers), une copie superficielle fonctionne parfaitement. La copie est indépendante de l’original.

Mais que se passe-t-il avec des structures imbriquées ? Voyons où les copies superficielles montrent leurs limites :

python
# Copie superficielle avec des listes imbriquées
original = [[1, 2], [3, 4]]
copy = original.copy()
 
print("Original:", original)  # Output: Original: [[1, 2], [3, 4]]
print("Copy:", copy)          # Output: Copy: [[1, 2], [3, 4]]
 
# Les listes externes sont des objets différents
print("Same outer list?", original is copy)  # Output: Same outer list? False
 
# Mais les listes imbriquées sont les MÊMES objets
print("Same nested list?", original[0] is copy[0])  # Output: Same nested list? True
 
# Modifier une liste imbriquée affecte les deux
copy[0].append(99)
 
print("Original:", original)  # Output: Original: [[1, 2, 99], [3, 4]]
print("Copy:", copy)          # Output: Copy: [[1, 2, 99], [3, 4]]

Liste originale

Liste imbriquée 1 : 1, 2, 99

Liste imbriquée 2 : 3, 4

Copie superficielle

18.8.2) Créer des copies superficielles de listes

Il existe plusieurs façons de créer une copie superficielle d’une liste :

python
# Méthode 1 : utiliser la méthode copy()
original = [[1, 2], [3, 4]]
copy1 = original.copy()
 
# Méthode 2 : utiliser le slicing de liste
copy2 = original[:]
 
# Méthode 3 : utiliser le constructeur list()
copy3 = list(original)
 
# Les trois créent des copies superficielles
print(copy1)  # Output: [[1, 2], [3, 4]]
print(copy2)  # Output: [[1, 2], [3, 4]]
print(copy3)  # Output: [[1, 2], [3, 4]]
 
# La liste externe est différente
print(original is copy1)  # Output: False
print(original is copy2)  # Output: False
print(original is copy3)  # Output: False
 
# Mais les listes internes sont PARTAGÉES
print(original[0] is copy1[0])  # Output: True
print(original[0] is copy2[0])  # Output: True
print(original[0] is copy3[0])  # Output: True

18.8.3) Créer des copies superficielles de dictionnaires

Les dictionnaires prennent également en charge la copie superficielle :

python
# Méthode 1 : utiliser la méthode copy()
original = {"name": "Alice", "grade": 85}
copy1 = original.copy()
 
# Méthode 2 : utiliser le constructeur dict()
copy2 = dict(original)
 
# Les deux créent des copies superficielles
print(copy1)  # Output: {'name': 'Alice', 'grade': 85}
print(copy2)  # Output: {'name': 'Alice', 'grade': 85}
 
# Ce sont des objets différents
print(original is copy1)  # Output: False
print(original is copy2)  # Output: False
 
# Modifier la copie n'affecte pas l'original
copy1["grade"] = 90
 
print("Original:", original)  # Output: Original: {'name': 'Alice', 'grade': 85}
print("Copy:", copy1)         # Output: Copy: {'name': 'Alice', 'grade': 90}

Cependant, avec des structures imbriquées, la même limite des copies superficielles s’applique :

python
# Copie superficielle avec un dictionnaire imbriqué
original = {
    "name": "Alice",
    "grades": [85, 90, 92]
}
 
copy = original.copy()
 
print("Original:", original)  # Output: Original: {'name': 'Alice', 'grades': [85, 90, 92]}
print("Copy:", copy)          # Output: Copy: {'name': 'Alice', 'grades': [85, 90, 92]}
 
# Les dictionnaires sont des objets différents
print("Same dict?", original is copy)  # Output: Same dict? False
 
# Mais la liste grades est le MÊME objet
print("Same grades list?", original["grades"] is copy["grades"])  # Output: Same grades list? True
 
# Modifier la liste grades affecte les deux
copy["grades"].append(88)
 
print("Original:", original)  # Output: Original: {'name': 'Alice', 'grades': [85, 90, 92, 88]}
print("Copy:", copy)          # Output: Copy: {'name': 'Alice', 'grades': [85, 90, 92, 88]}

18.8.4) Créer des copies superficielles d’ensembles

Les ensembles prennent aussi en charge la copie superficielle. Comme pour les listes et les dictionnaires, une copie superficielle crée un nouvel ensemble « externe », mais les objets qu’il contient ne sont pas copiés.

python
# Méthode 1 : utiliser la méthode copy()
original = {1, 2, 3}
copy1 = original.copy()
 
# Méthode 2 : utiliser le constructeur set()
copy2 = set(original)
 
# Les deux créent des copies superficielles
print(copy1)  # Output: {1, 2, 3}
print(copy2)  # Output: {1, 2, 3}
 
# Ce sont des objets différents
print(original is copy1)  # Output: False
print(original is copy2)  # Output: False
 
# Modifier la copie n'affecte pas l'original
copy1.add(4)
 
print("Original:", original)  # Output: Original: {1, 2, 3}
print("Copy:", copy1)         # Output: Copy: {1, 2, 3, 4}

Comme pour les autres structures, la limitation apparaît quand l’ensemble contient des objets mutables (ou, plus couramment, des objets immuables qui contiennent d’autres objets). Par exemple, on peut stocker des frozenset dans un set, et frozenset peut lui-même contenir des objets (immuables) comme des tuples.

python
# Copie superficielle avec un ensemble qui contient des objets composés
inner = frozenset([(1, 2), (3, 4)])
original = {inner}
copy = original.copy()
 
print("Original:", original)  # Output: Original: {frozenset({(1, 2), (3, 4)})}
print("Copy:", copy)          # Output: Copy: {frozenset({(1, 2), (3, 4)})}
 
# Les ensembles sont des objets différents
print("Same set?", original is copy)  # Output: Same set? False
 
# Mais l'élément frozenset est le MÊME objet dans les deux ensembles
original_element = next(iter(original))
copy_element = next(iter(copy))
print("Same element?", original_element is copy_element)  # Output: Same element? True
© 2025. Primesoft Co., Ltd.
support@primesoft.ai