Slicing, Mutable face à Immutable, et is face à ==

Objectifs de la leçon
  • Découper des listes et des chaînes avec le Slicing, y compris avec un pas négatif pour inverser
  • Classer les types en Mutable ou Immutable et expliquer la différence
  • Comprendre ce qui se passe quand deux variables pointent vers le même objet, et comment créer une copie
  • Distinguer == (la même valeur) et is (le même objet)

Qu’allons-nous apprendre ?

  • Le Slicing — comment découper une partie d’une liste, d’un tuple ou d’une chaîne.
  • Mutable face à Immutable — ce qui peut être modifié et ce qui ne peut pas, et pourquoi c’est important.
  • Les références — ce qui se passe quand on écrit b = a.
  • is face à == — « la même valeur » face à « exactement le même objet ».

Pourquoi en avons-nous besoin ?

  • Le Slicing est partout : les 10 premiers résultats, les 3 derniers caractères d’un numéro de carte (****1234), « la page 2 » d’une liste.
  • Mutable est à l’origine de l’un des bugs les plus déroutants de Python : « J’ai modifié une liste, et soudain la seconde a changé aussi ! ». Comprendre ce sujet évite des heures de débogage. Et dans le thème POO, les objets des classes se comportent exactement ainsi.

Explication à partir des bases

Le Slicing

Le Slicing (découpage) crée une nouvelle séquence à partir d’une partie d’une séquence existante :

sequence[start:stop:step]
  • start — où commencer (inclus). Par défaut : depuis le début.
  • stop — où s’arrêter (non inclus !, comme dans range). Par défaut : jusqu’à la fin.
  • step — le pas. Par défaut : 1. Un pas négatif = de la fin vers le début.
letters = ["a", "b", "c", "d", "e", "f"]
index:      0    1    2    3    4    5
negative:  -6   -5   -4   -3   -2   -1

Le Slicing ne lance pas d’IndexError même si l’on sort de la plage. Il renvoie simplement ce qu’il y a.

Mutable face à Immutable

Immutable (non modifiable)Mutable (modifiable)
int, float, bool, str, tuplelist, dict, set

Immutable = on ne peut pas modifier l’objet lui-même. Chaque « modification » crée un nouvel objet :

name = "dana"
name.upper()      # returns a NEW string "DANA"; name is still "dana"
name = name.upper()   # now name points to the new string

Mutable = on peut modifier l’objet sur place (append, d[k] = v), et il reste le même objet.

Une variable = une référence

Vous vous souvenez de l’analogie de l’étiquette ? Une variable est une étiquette portant un nom, collée sur un objet. Elle ne « contient » pas l’objet, elle pointe vers lui.

a = [1, 2, 3]
b = a          # NOT a copy! b is a second sticker on the SAME list
a ──┐
    ├──► [1, 2, 3]      one list in memory, two names
b ──┘

Si la liste est Mutable et que vous la modifiez via b, la modification est aussi visible via a. C’est le même objet.

== face à is

OpérateurDemandeExemple
==Les valeurs sont-elles égales ?[1, 2] == [1, 2] → True
isEst-ce exactement le même objet en mémoire ?[1, 2] is [1, 2] → False (deux listes séparées)

id(x) renvoie un identifiant unique d’un objet. a is b équivaut à id(a) == id(b).

Règle : comparez les valeurs avec ==. is ne sert presque que pour comparer avec None : if result is None:.

Analogie : des jumeaux

Deux vrais jumeaux : == → True (ils se ressemblent). is → False (deux personnes différentes). En revanche, « Dana » et « la fille de Ruti » sont la même personne (is → True) : deux noms pour la même personne. Si Dana se coupe les cheveux, « la fille de Ruti » s’est aussi coupé les cheveux.

Syntaxe

# slicing
items[2:5]       # indexes 2, 3, 4
items[:3]        # first 3
items[-3:]       # last 3
items[::2]       # every second item
items[::-1]      # reversed copy
items[:]         # a full (shallow) copy

# copies of mutable objects
copy1 = items[:]
copy2 = items.copy()
copy3 = list(items)
d_copy = my_dict.copy()

# identity vs equality
a == b      # same value?
a is b      # same object?
x is None   # the correct way to check for None
id(a)       # the object's unique identity number

Premier exemple de code

week-2/slicing_demo.py :

letters = ["a", "b", "c", "d", "e", "f"]

print(letters[1:4])
print(letters[:2])
print(letters[-2:])
print(letters[::2])
print(letters[::-1])
print(letters[2:100])

card_number = "4580123498761234"
print("Card ending in", card_number[-4:])
print("Masked:", "*" * 12 + card_number[-4:])

Sortie :

['b', 'c', 'd']
['a', 'b']
['e', 'f']
['a', 'c', 'e']
['f', 'e', 'd', 'c', 'b', 'a']
['c', 'd', 'e', 'f']
Card ending in 1234
Masked: ************1234

Analyse du code ligne par ligne

ExpressionIndicesExplication
[1:4]1, 2, 34 n’est pas inclus
[:2]0, 1Depuis le début, 2 éléments
[-2:]-2, -1Les deux derniers
[::2]0, 2, 4Un élément sur deux
[::-1]5 → 0Un pas négatif : inversé
[2:100]de 2 jusqu’à la finSortir de la plage ne lance pas d’erreur
card_number[-4:]Les 4 derniersLe Slicing fonctionne aussi sur les chaînes
"*" * 12—Répéter un texte 12 fois

Un autre exemple

Le fameux bug — deux noms pour la même liste. week-2/aliasing.py :

original = ["milk", "bread"]
same = original
copy = original.copy()

same.append("eggs")

print("original:", original)
print("same:    ", same)
print("copy:    ", copy)

print("same == original:", same == original)
print("same is original:", same is original)
print("copy == original:", copy == original)
print("copy is original:", copy is original)

name = "dana"
other = name
other = other.upper()
print("name:", name, "| other:", other)

Sortie :

original: ['milk', 'bread', 'eggs']
same:     ['milk', 'bread', 'eggs']
copy:     ['milk', 'bread']
same == original: True
same is original: True
copy == original: False
copy is original: False
name: dana | other: DANA

Analyse :

  • same = original — aucune nouvelle liste n’a été créée. Il y a maintenant deux noms pour la même liste.
  • same.append("eggs") — nous avons modifié la liste elle-même (elle est Mutable), donc original a « changé » aussi. En réalité, elle n’a pas changé séparément, c’est la même liste.
  • copy = original.copy() — une nouvelle liste avec les mêmes éléments, qui n’est pas affectée par la modification.
  • copy == original → False, car après l’append les valeurs sont déjà différentes. Avant l’append, c’était True, mais is était déjà False.
  • Les chaînes sont Immutable : other.upper() a créé une nouvelle chaîne, et other y a été rattachée. name n’a pas été affectée. Avec Immutable, ce bug ne peut pas se produire.

Mutable comme paramètre de fonction

Exactement le même principe s’applique aux fonctions. Le paramètre est une autre « étiquette » sur le même objet :

def add_bonus(grades):
    grades.append(100)

my_grades = [80, 90]
add_bonus(my_grades)
print(my_grades)   # [80, 90, 100] - the caller's list changed!

Parfois c’est voulu, et parfois c’est un bug. Si une fonction n’est pas censée modifier son entrée, travaillez sur une copie : grades = grades.copy(), ou construisez une nouvelle liste et renvoyez-la.

Erreur courante

Erreur 1 : b = a pour « sauvegarder » une liste avant de la modifier.

backup = cart
cart.clear()
print(backup)   # [] - the "backup" is empty too!

La correction : backup = cart.copy().

Erreur 2 : is pour comparer des valeurs.

a = [1, 2, 3]
b = [1, 2, 3]
if a is b:
    print("same")

Rien n’est affiché. Deux listes égales en valeur sont deux objets séparés. Pour les valeurs, toujours ==.

(Avec de petits nombres et des chaînes courtes, is « fonctionne par hasard », car Python réutilise les petits objets. Avec de grands nombres, ce n’est plus le cas. Ne comptez pas dessus.)

Erreur 3 : oublier que les méthodes de str ne modifient pas la chaîne.

email = "  Dana@Mail.COM "
email.strip().lower()
print(email)   # still "  Dana@Mail.COM "

Une chaîne est Immutable, et le nouveau résultat a été jeté. La correction : email = email.strip().lower().

Erreur 4 : croire que stop est inclus. items[1:3] renvoie 2 éléments (indices 1 et 2), pas 3.

Exercices

Les exercices n’utilisent que ce qui a été vu dans cette leçon et les précédentes. Essayez d’abord seul, et n’ouvrez la solution qu’ensuite.

Exercice 1: Le Slicing

letters = ["a", "b", "c", "d", "e", "f"]

Uniquement avec du Slicing, affichez : les trois premiers, les deux derniers, un élément sur deux, et la liste inversée.

Résultat attendu :

['a', 'b', 'c']
['e', 'f']
['a', 'c', 'e']
['f', 'e', 'd', 'c', 'b', 'a']
Afficher la solution
letters = ["a", "b", "c", "d", "e", "f"]

print(letters[:3])
print(letters[-2:])
print(letters[::2])
print(letters[::-1])
['a', 'b', 'c']
['e', 'f']
['a', 'c', 'e']
['f', 'e', 'd', 'c', 'b', 'a']

[start:stop:step] — le stop n’est pas inclus. [::-1] renvoie une copie inversée — l’original n’a pas changé.

Exercice 2: Masquer un numéro de carte

Écrivez une fonction mask_card(number) qui reçoit un numéro de carte sous forme de chaîne, et renvoie des astérisques à la place de tous les chiffres sauf les quatre derniers.

print(mask_card("4580123498761234"))

Résultat attendu :

************1234
Afficher la solution
def mask_card(number):
    hidden = len(number) - 4
    return "*" * hidden + number[-4:]


print(mask_card("4580123498761234"))
************1234

Le Slicing fonctionne aussi sur les chaînes : number[-4:] correspond aux quatre derniers chiffres.

Exercice 3: La sauvegarde qui s’est vidée

cart = ["milk", "bread"]
backup = cart
cart.clear()
print(backup)
  1. Qu’est-ce qui sera affiché, et pourquoi ?
  2. Corrigez pour que la sauvegarde soit conservée.
Afficher la solution
  1. [] sera affiché. backup = cart n’est pas une copie — les deux noms pointent vers la même liste. La vider via cart est aussi visible via backup.
  2. Créez une copie :
cart = ["milk", "bread"]
backup = cart.copy()
cart.clear()
print(backup)
print(backup is cart)
['milk', 'bread']
False

is vérifie si c’est le même objet — après copy(), ce n’est plus le cas.

Exercice 4: Mutable comme paramètre

def add_bonus(grades):
    grades.append(100)

def shout(name):
    name = name.upper()

my_grades = [80, 90]
my_name = "dana"
add_bonus(my_grades)
shout(my_name)
print(my_grades)
print(my_name)

Prédisez ce qui sera affiché, et expliquez la différence entre les deux fonctions.

Afficher la solution
def add_bonus(grades):
    grades.append(100)


def shout(name):
    name = name.upper()


my_grades = [80, 90]
my_name = "dana"
add_bonus(my_grades)
shout(my_name)
print(my_grades)
print(my_name)
[80, 90, 100]
dana
  • Une liste est Mutable : append a modifié la même liste vers laquelle pointe my_grades.
  • Une chaîne est Immutable : name.upper() a créé une nouvelle chaîne, et l’affectation n’a changé que l’endroit vers lequel pointe le name local. my_name n’a pas été affectée.

Exercice 5: Un palindrome et des pages

  1. Écrivez is_palindrome(text) — si le mot se lit de la même façon dans les deux sens (en ignorant les majuscules).
  2. Écrivez paginate(items, page, page_size) qui renvoie les éléments d’une page donnée (la première page est 1).
print(is_palindrome("Level"))
print(is_palindrome("hello"))
print(paginate(["a", "b", "c", "d", "e"], 2, 2))
print(paginate(["a", "b", "c", "d", "e"], 3, 2))

Résultat attendu :

True
False
['c', 'd']
['e']
Afficher la solution
def is_palindrome(text):
    cleaned = text.lower()
    return cleaned == cleaned[::-1]


def paginate(items, page, page_size):
    start = (page - 1) * page_size
    return items[start:start + page_size]


print(is_palindrome("Level"))
print(is_palindrome("hello"))
print(paginate(["a", "b", "c", "d", "e"], 2, 2))
print(paginate(["a", "b", "c", "d", "e"], 3, 2))
True
False
['c', 'd']
['e']

Le Slicing n’échoue pas quand le stop dépasse la longueur de la liste — il renvoie simplement ce qu’il y a. C’est pourquoi la dernière page renvoie ['e'].

Résumé

  • Le Slicing [start:stop:step] renvoie une nouvelle séquence. stop n’est pas inclus, [::-1] inverse, et il n’y a pas d’IndexError.
  • Immutable (int, float, str, tuple, bool) : chaque « modification » crée un nouvel objet. Mutable (list, dict, set) : se modifie sur place.
  • Une variable est une référence. b = a = deux noms pour le même objet, pas une copie. Une copie : .copy() ou [:].
  • Une fonction qui reçoit une liste/un dict peut modifier l’original de celui qui l’a appelée.
  • == = la même valeur. is = le même objet. Pour None, utilisez is None.

Documentation officielle