# Slicing, Mutable face à Immutable, et is face à ==<no value>

## 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 :

```text
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.

```text
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`, `tuple` | `list`, `dict`, `set` |

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

```python
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.

```python
a = [1, 2, 3]
b = a          # NOT a copy! b is a second sticker on the SAME list
```

```text
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érateur | Demande | Exemple |
|---|---|---|
| `==` | Les **valeurs** sont-elles égales ? | `[1, 2] == [1, 2]` → `True` |
| `is` | Est-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:`.

{{< box "analogy" "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.
{{< /box >}}

## Syntaxe

```python
# 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` :

```python
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 :**

```text
['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

| Expression | Indices | Explication |
|---|---|---|
| `[1:4]` | 1, 2, 3 | 4 n'est **pas inclus** |
| `[:2]` | 0, 1 | Depuis le début, 2 éléments |
| `[-2:]` | -2, -1 | Les deux derniers |
| `[::2]` | 0, 2, 4 | Un élément sur deux |
| `[::-1]` | 5 → 0 | Un pas négatif : inversé |
| `[2:100]` | de 2 jusqu'à la fin | Sortir de la plage ne lance pas d'erreur |
| `card_number[-4:]` | Les 4 derniers | Le 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` :

```python
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 :**

```text
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 :

```python
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.**

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

La correction : `backup = cart.copy()`.

**Erreur 2 : `is` pour comparer des valeurs.**

```python
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.**

```python
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.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## 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

```python
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 :**

```text
['a', 'b', 'c']
['e', 'f']
['a', 'c', 'e']
['f', 'e', 'd', 'c', 'b', 'a']
```

{{< solution >}}
```python
letters = ["a", "b", "c", "d", "e", "f"]

print(letters[:3])
print(letters[-2:])
print(letters[::2])
print(letters[::-1])
```

```text
['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é.
{{< /solution >}}

### 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.

```python
print(mask_card("4580123498761234"))
```

**Résultat attendu :**

```text
************1234
```

{{< solution >}}
```python
def mask_card(number):
    hidden = len(number) - 4
    return "*" * hidden + number[-4:]


print(mask_card("4580123498761234"))
```

```text
************1234
```

Le Slicing fonctionne aussi sur les chaînes : `number[-4:]` correspond aux quatre derniers chiffres.
{{< /solution >}}

### Exercice 3: La sauvegarde qui s'est vidée

```python
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.

{{< 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 :

```python
cart = ["milk", "bread"]
backup = cart.copy()
cart.clear()
print(backup)
print(backup is cart)
```

```text
['milk', 'bread']
False
```

`is` vérifie si c'est **le même objet** — après `copy()`, ce n'est plus le cas.
{{< /solution >}}

### Exercice 4: Mutable comme paramètre

```python
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.

{{< solution >}}
```python
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)
```

```text
[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.
{{< /solution >}}

### 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).

```python
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 :**

```text
True
False
['c', 'd']
['e']
```

{{< solution >}}
```python
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))
```

```text
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']`.
{{< /solution >}}

<!-- exercises:end -->

## 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`.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## Documentation officielle

- [Common Sequence Operations](https://docs.python.org/3/builtins/stdtypes.html#common-sequence-operations)
- [Data model — Objects, values and types](https://docs.python.org/3/reference/datamodel.html#objects-values-and-types)
- [Identity comparisons](https://docs.python.org/3/reference/expressions.html#is-not)

<!-- docs:end -->
