# Вложенные функции, замыкания и nonlocal<no value>

## Что мы изучим?

- **Вложенную функцию** (Nested Function) — функцию, определённую внутри функции.
- **Enclosing** — букву E в LEGB, вблизи.
- **Замыкание** (Closure) — функцию, которая «помнит» переменные.
- **`nonlocal`** — как изменить переменную объемлющей функции.

## Зачем это нужно?

- **Приватная вспомогательная функция:** иногда функции нужна маленькая вспомогательная функция, которая принадлежит **только** ей. Нет причин делать её глобальной и смешивать с остальным кодом.
- **Функции, которые помнят состояние:** в предыдущем уроке мы видели, что `make_multiplier(2)` возвращает функцию, которая «помнит» число 2. Сегодня поймём, как это работает, — и как построить счётчик, накопитель или кэш **без** `global`.

## Объяснение с основ

### Функция внутри функции

Можно определить `def` внутри `def`. Внутренняя функция:

- Существует **только внутри** внешней функции — снаружи её нельзя вызвать.
- **Видит** переменные и параметры внешней функции.

### Enclosing — буква E в LEGB

В уроке об области видимости мы изучили порядок поиска **L → E → G → B**. Когда внутренняя функция использует имя, Python ищет его:

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

### Замыкание — функция, которая помнит

Когда внешняя функция **возвращает** внутреннюю, происходит интересное: внешняя функция уже завершилась, а внутренняя **всё ещё помнит** её переменные. Это сочетание — функция + переменные, которые она помнит, — называется **замыканием** (Closure).

{{< box "analogy" "Аналогия: письмо в конверте" >}}
Внешняя функция пишет письмо (внутреннюю функцию) и кладёт его в конверт вместе с запиской с данными (переменными). Она отправляет конверт и исчезает, — но тот, кто откроет конверт, получит и письмо, и записку. В каждом конверте своя записка.
{{< /box >}}

### nonlocal

**Читать** переменную из объемлющей области можно всегда. **Изменить** её (присвоить новое значение) без объявления нельзя — точно как с глобальной. Объявление — `nonlocal name`: «эта переменная принадлежит объемлющей функции».

| Объявление | Переменная принадлежит... |
|---|---|
| `global x` | Уровню файла |
| `nonlocal x` | Ближайшей объемлющей функции |

## Синтаксис

```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)
```

## Первый пример кода

**Приватная вспомогательная функция.** `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)]))
```

**Вывод:**

```text
Milk      6
Bread     9
```

## Разбор кода построчно

| Строка | Объяснение |
|---|---|
| `def line(name, price):` | Внутренняя функция — существует только внутри `format_receipt`. |
| `name.ljust(10)` | Дополняет имя пробелами до 10 символов, чтобы цены стояли в столбец. |
| `lines.append(line(name, price))` | Внешняя функция использует свою вспомогательную функцию. |
| `"\n".join(lines)` | Соединяет все строки в один текст с переводом строки между ними. |

## Ещё один пример

**Замыкание — функция, которая помнит.** `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"))
```

**Вывод:**

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

`make_greeter` уже завершилась на строке `hello = ...`, но `hello` всё ещё помнит, что `greeting` — это `"Hello"`. У каждой созданной функции **своя копия** переменной — `shalom` помнит `"Shalom"`.

**Счётчик с `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())
```

**Вывод:**

```text
1 2 3
1
```

Каждый счётчик помнит **свой** `count`: `other` начинает с нуля, независимо от `counter`. Это приватное состояние — без глобальной переменной, которую может менять весь код.

## Типичная ошибка

**Ошибка: изменить переменную объемлющей функции без `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
```

**Почему?** Из-за присваивания `count += 1` Python решает, что `count` — **локальная** переменная `increment`, — и затем пытается прочитать локальную, у которой ещё нет значения. Это ровно та же ошибка, что мы видели с глобальными переменными в уроке об области видимости.

**Исправление:** `nonlocal count` в начале `increment`.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## Практика

Упражнения опираются только на то, что пройдено в этом уроке и в предыдущих. Сначала попробуйте сами, и только потом открывайте решение.

### Упражнение 1: Внутренняя вспомогательная функция

Напишите функцию `format_receipt(items)`, которая получает список пар (название, цена) и возвращает текст чека. Внутри неё определите внутреннюю функцию `line(name, price)`, которая дополняет название до 10 символов (`ljust`).

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

**Ожидаемый вывод:**

```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` существует только внутри `format_receipt` — это приватная вспомогательная функция, и она не смешивается с остальным кодом.
{{< /solution >}}

### Упражнение 2: Замыкание

Напишите `make_greeter(greeting)`, которая возвращает функцию, получающую имя и возвращающую приветствие.

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

**Ожидаемый вывод:**

```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` уже завершилась, но каждая возвращённая ею функция **помнит** свой `greeting`. Это и есть замыкание (Closure).
{{< /solution >}}

### Упражнение 3: Предскажите вывод

```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` использует `nonlocal`, поэтому присваивание меняет `message` функции `outer`, а не создаёт новую локальную переменную.
{{< /solution >}}

### Упражнение 4: Счётчик с шагом

Напишите `make_counter(step)` — каждый вызов счётчика увеличивает его на `step` и возвращает новое значение. Создайте два счётчика и проверьте, что они не влияют друг на друга.

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

{{< 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` внутри `increment` только **читается**, поэтому объявление не нужно.
- `count` **меняется**, поэтому нужен `nonlocal count`.
{{< /solution >}}

### Упражнение 5: Без nonlocal

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

counter = make_counter()
counter()
```

1. Какую ошибку вы получите и почему?
2. В чём разница между `global` и `nonlocal`?

{{< solution >}}
1. **`UnboundLocalError`**. Из-за присваивания `count += 1` Python решает, что `count` — **локальная** переменная `increment`, — и у неё нет значения, когда вы пытаетесь её увеличить. Исправление: `nonlocal count` в начале `increment`.
2. **`global`** — переменная принадлежит уровню файла. **`nonlocal`** — переменная принадлежит ближайшей **объемлющей** функции. Замыкание с `nonlocal` даёт каждому счётчику приватное состояние без глобальной переменной, которую может менять весь код.
{{< /solution >}}

<!-- exercises:end -->

## Итоги

- **Вложенная функция** — функция внутри функции. Существует только внутри и видит переменные объемлющей (**Enclosing**).
- **Замыкание** (Closure) — внутренняя функция, которую вернули и которая помнит переменные создавшей её функции. У каждой — своя копия.
- **Читать** переменную из объемлющей области — можно. **Изменять** — только с `nonlocal`, иначе `UnboundLocalError`.
- `global` — переменная на уровне файла. `nonlocal` — переменная в объемлющей функции.
- Замыкание даёт **приватное состояние** без глобальной переменной. И всё же, когда возможно, параметр на входе и `return` на выходе — лучше.

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

## Официальная документация

- [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 -->
