# Les fonctions imbriquées, les closures et nonlocal<no value>

## Qu'allons-nous apprendre ?

- **La fonction imbriquée** (Nested Function) — une fonction définie dans une fonction.
- **Enclosing** — le E de LEGB, de près.
- **La closure** — une fonction qui « se souvient » de variables.
- **`nonlocal`** — comment modifier une variable de la fonction englobante.

## Pourquoi en avons-nous besoin ?

- **Une fonction d'aide privée :** parfois une fonction a besoin d'une petite fonction d'aide qui n'appartient **qu'à elle**. Il n'y a aucune raison qu'elle soit globale et se mélange au reste du code.
- **Des fonctions qui se souviennent d'un état :** dans la leçon précédente, nous avons vu que `make_multiplier(2)` renvoie une fonction qui « se souvient » du nombre 2. Aujourd'hui, nous comprendrons comment cela fonctionne — et comment construire un compteur, un accumulateur ou un cache **sans** `global`.

## Explication à partir des bases

### Une fonction dans une fonction

On peut définir un `def` à l'intérieur d'un `def`. La fonction intérieure :

- N'existe **qu'à l'intérieur** de la fonction extérieure — on ne peut pas l'appeler de l'extérieur.
- **Voit** les variables et les paramètres de la fonction extérieure.

### Enclosing — le E de LEGB

Dans la leçon sur la portée, nous avons appris l'ordre de recherche **L → E → G → B**. Quand une fonction intérieure utilise un nom, Python le cherche :

```text
L  Local       → inside the inner function
E  Enclosing   → inside the outer function
G  Global      → top level of the file
B  Built-in    → print, len, ...
```

### La closure — une fonction qui se souvient

Quand une fonction extérieure **renvoie** la fonction intérieure, il se passe quelque chose d'intéressant : la fonction extérieure est déjà terminée, mais la fonction intérieure **se souvient encore** de ses variables. Cette combinaison — une fonction + les variables dont elle se souvient — s'appelle une **closure** (fermeture).

{{< box "analogy" "Analogie : une lettre dans une enveloppe" >}}
La fonction extérieure écrit une lettre (la fonction intérieure) et la met dans une enveloppe avec un petit mot contenant quelques données (les variables). Elle envoie l'enveloppe et disparaît — mais celui qui ouvre l'enveloppe reçoit à la fois la lettre et le petit mot. Chaque enveloppe a son propre petit mot.
{{< /box >}}

### nonlocal

**Lire** une variable de la portée Enclosing — c'est toujours permis. La **modifier** (lui affecter une nouvelle valeur) — ce n'est pas possible sans déclaration, exactement comme avec une globale. La déclaration est `nonlocal name` : « cette variable appartient à la fonction englobante ».

| Déclaration | La variable appartient à... |
|---|---|
| `global x` | Le niveau du fichier |
| `nonlocal x` | La fonction englobante la plus proche |

## Syntaxe

```python
def outer():
    count = 0                 # enclosing variable

    def inner():
        nonlocal count        # assignments go to outer's count
        count += 1
        return count

    return inner              # return the function itself (no parentheses)
```

## Premier exemple de code

**Une fonction d'aide privée.** `week-2/receipt.py` :

```python
def format_receipt(items):
    def line(name, price):
        return name.ljust(10) + str(price)

    lines = []
    for name, price in items:
        lines.append(line(name, price))
    return "\n".join(lines)

print(format_receipt([("Milk", 6), ("Bread", 9)]))
```

**Sortie :**

```text
Milk      6
Bread     9
```

## Analyse du code ligne par ligne

| Ligne | Explication |
|---|---|
| `def line(name, price):` | Une fonction intérieure — n'existe que dans `format_receipt`. |
| `name.ljust(10)` | Complète le nom avec des espaces jusqu'à 10 caractères, pour que les prix soient alignés en colonne. |
| `lines.append(line(name, price))` | La fonction extérieure utilise sa fonction d'aide. |
| `"\n".join(lines)` | Joint toutes les lignes en un seul texte, avec un saut de ligne entre elles. |

## Un autre exemple

**Une closure — une fonction qui se souvient.** `week-2/greeter.py` :

```python
def make_greeter(greeting):
    def greet(name):
        return greeting + ", " + name
    return greet

hello = make_greeter("Hello")
shalom = make_greeter("Shalom")
print(hello("Dana"))
print(shalom("Avi"))
```

**Sortie :**

```text
Hello, Dana
Shalom, Avi
```

`make_greeter` s'est déjà terminée à la ligne `hello = ...`, mais `hello` se souvient encore que `greeting` vaut `"Hello"`. Chaque fonction créée a **sa propre copie** de la variable — `shalom` se souvient de `"Shalom"`.

**Un compteur avec `nonlocal`.** `week-2/counter.py` :

```python
def make_counter():
    count = 0

    def increment():
        nonlocal count
        count += 1
        return count

    return increment

counter = make_counter()
print(counter(), counter(), counter())

other = make_counter()
print(other())
```

**Sortie :**

```text
1 2 3
1
```

Chaque compteur se souvient de **son propre** `count` : `other` part de zéro, indépendamment de `counter`. C'est un état privé — sans variable globale que tout le code peut modifier.

## Erreur courante

**L'erreur : modifier une variable de la fonction englobante sans `nonlocal`.**

```python
def make_counter():
    count = 0

    def increment():
        count += 1
        return count

    return increment

counter = make_counter()
counter()
```

```text
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value
```

**Pourquoi ?** À cause de l'affectation `count += 1`, Python décide que `count` est une variable **locale** de `increment` — puis essaie de lire une variable locale qui n'a pas encore de valeur. C'est exactement la même erreur que celle que nous avons vue avec les globales dans la leçon sur la portée.

**La correction :** `nonlocal count` au début de `increment`.

<!-- 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: Une fonction d'aide intérieure

Écrivez une fonction `format_receipt(items)` qui reçoit une liste de paires (nom, prix) et renvoie le texte d'un ticket. À l'intérieur, définissez une fonction intérieure `line(name, price)` qui complète le nom jusqu'à 10 caractères (`ljust`).

```python
print(format_receipt([("Milk", 6), ("Bread", 9)]))
```

**Résultat attendu :**

```text
Milk      6
Bread     9
```

{{< solution >}}
```python
def format_receipt(items):
    def line(name, price):
        return name.ljust(10) + str(price)

    lines = []
    for name, price in items:
        lines.append(line(name, price))
    return "\n".join(lines)


print(format_receipt([("Milk", 6), ("Bread", 9)]))
```

```text
Milk      6
Bread     9
```

`line` n'existe que dans `format_receipt` — c'est une fonction d'aide privée, qui ne se mélange pas au reste du code.
{{< /solution >}}

### Exercice 2: Closure

Écrivez `make_greeter(greeting)` qui renvoie une fonction qui reçoit un nom et renvoie une salutation.

```python
hello = make_greeter("Hello")
shalom = make_greeter("Shalom")
print(hello("Dana"))
print(shalom("Avi"))
```

**Résultat attendu :**

```text
Hello, Dana
Shalom, Avi
```

{{< solution >}}
```python
def make_greeter(greeting):
    def greet(name):
        return greeting + ", " + name
    return greet


hello = make_greeter("Hello")
shalom = make_greeter("Shalom")
print(hello("Dana"))
print(shalom("Avi"))
```

```text
Hello, Dana
Shalom, Avi
```

`make_greeter` est déjà terminée, mais chaque fonction qu'elle a renvoyée **se souvient** de son propre `greeting`. C'est une closure.
{{< /solution >}}

### Exercice 3: Prédire la sortie

```python
def outer():
    message = "outer"

    def show():
        print("show:", message)

    def change():
        nonlocal message
        message = "changed"

    show()
    change()
    show()

outer()
```

{{< solution >}}
```python
def outer():
    message = "outer"

    def show():
        print("show:", message)

    def change():
        nonlocal message
        message = "changed"

    show()
    change()
    show()


outer()
```

```text
show: outer
show: changed
```

`change` utilise `nonlocal`, donc l'affectation modifie le `message` de `outer` — elle ne crée pas de nouvelle variable locale.
{{< /solution >}}

### Exercice 4: Un compteur avec un pas

Écrivez `make_counter(step)` — chaque appel au compteur l'augmente de `step` et renvoie la nouvelle valeur. Créez deux compteurs et vérifiez qu'ils ne s'influencent pas l'un l'autre.

```python
by_two = make_counter(2)
by_ten = make_counter(10)
print(by_two(), by_two())
print(by_ten())
print(by_two())
```

**Résultat attendu :**

```text
2 4
10
6
```

{{< solution >}}
```python
def make_counter(step):
    count = 0

    def increment():
        nonlocal count
        count += step
        return count

    return increment


by_two = make_counter(2)
by_ten = make_counter(10)
print(by_two(), by_two())
print(by_ten())
print(by_two())
```

```text
2 4
10
6
```

- `step` n'est que **lu** dans `increment`, il n'a donc besoin d'aucune déclaration.
- `count` **change**, il a donc besoin de `nonlocal count`.
{{< /solution >}}

### Exercice 5: Sans nonlocal

```python
def make_counter():
    count = 0
    def increment():
        count += 1
        return count
    return increment

counter = make_counter()
counter()
```

1. Quelle erreur obtiendrez-vous, et pourquoi ?
2. Quelle est la différence entre `global` et `nonlocal` ?

{{< solution >}}
1. **`UnboundLocalError`**. À cause de l'affectation `count += 1`, Python décide que `count` est une variable **locale** de `increment` — et elle n'a pas de valeur quand on essaie de l'augmenter. La correction : `nonlocal count` au début de `increment`.
2. **`global`** — la variable appartient au niveau du fichier. **`nonlocal`** — la variable appartient à la fonction **englobante** la plus proche. Une closure avec `nonlocal` donne à chaque compteur un état privé, sans variable globale que tout le code peut modifier.
{{< /solution >}}

<!-- exercises:end -->

## Résumé

- **Fonction imbriquée** — une fonction dans une fonction. Elle n'existe qu'à l'intérieur, et voit les variables de la fonction englobante (**Enclosing**).
- **Closure** — une fonction intérieure renvoyée, qui se souvient des variables de la fonction qui l'a créée. Chacune a sa propre copie.
- **Lire** une variable de la portée Enclosing — c'est permis. La **modifier** — seulement avec `nonlocal`, sinon `UnboundLocalError`.
- `global` — une variable au niveau du fichier. `nonlocal` — une variable dans la fonction englobante.
- Une closure donne un **état privé** sans variable globale. Et pourtant — quand c'est possible, un paramètre en entrée et un `return` en sortie, c'est mieux.

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

## Documentation officielle

- [Scopes and Namespaces Example](https://docs.python.org/3/tutorial/classes.html#scopes-and-namespaces-example)
- [The nonlocal statement](https://docs.python.org/3/reference/simple_stmts.html#the-nonlocal-statement)

<!-- docs:end -->
