# Исключения B: else и finally — полная структура<no value>

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

В предыдущем уроке мы использовали `try` и `except`. В структуре есть ещё две части:

- **`else`** — выполняется, **только если ошибки не было**.
- **`finally`** — выполняется **всегда**, с ошибкой или без.

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

Представьте код, который открывает соединение с базой данных (темы бэкенда и баз данных), выполняет действие и закрывает соединение. Что будет, если действие упадёт посередине? Если закрытие записано после действия, оно **не будет выполнено**, и соединение останется открытым. После сотни таких сбоев сервер упадёт, потому что у него закончились соединения.

`finally` гарантирует, что очистка произойдёт **в любом случае**. `else` сохраняет `try` маленьким и делает понятным, что выполняется только после успеха.

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

### Четыре блока

| Блок | Когда выполняется | Для чего |
|---|---|---|
| `try` | Всегда начинают с него | Код, который **может упасть** |
| `except` | **Только если** выброшена ошибка подходящего типа | Обработка ошибки |
| `else` | **Только если** в `try` **не** было выброшено ни одной ошибки | Продолжение, зависящее от успеха |
| `finally` | **Всегда**, в конце, в любом случае | Очистка: закрытие файлов, соединений, завершающее сообщение |

{{< box "analogy" "Аналогия: готовка" >}}
**try** = вы пытаетесь испечь торт. **except** = если он сгорел, вы заказываете торт в пекарне. **else** = если он получился, вы его украшаете (сгоревший торт не украшают!). **finally** = в любом случае вы выключаете духовку и моете посуду.
{{< /box >}}

### Зачем `else`, а не просто продолжить в `try`?

Код продолжения можно положить внутрь `try`. Но тогда, если **этот** код упадёт с той же ошибкой, `except` по ошибке поймает её и обработает не ту проблему. `else` делает ясным: **внутри `try` — только рискованная строка**, а остальное — снаружи.

### `finally` выполняется, даже когда...

- Была ошибка, и её поймали.
- Была ошибка, и её **не** поймали. `finally` выполняется, а затем программа падает.
- В середине `try` стоит `return`. `finally` выполняется **до того**, как функция действительно вернёт значение.

## Синтаксис

```python
try:
    # risky code
except SomeError:
    # runs only if SomeError happened
else:
    # runs only if NO error happened in try
finally:
    # runs ALWAYS, last
```

**Обязательный порядок:** `try` → `except` (один или несколько) → `else` (необязательно) → `finally` (необязательно). Можно также `try` + `finally` без `except`.

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

`week-2/full_try.py`:

```python
def convert(text):
    print("--- converting:", text)
    try:
        number = int(text)
    except ValueError:
        print("except: not a number")
    else:
        print("else: success, doubled =", number * 2)
    finally:
        print("finally: done with", text)

convert("21")
convert("hi")
```

**Вывод:**

```text
--- converting: 21
else: success, doubled = 42
finally: done with 21
--- converting: hi
except: not a number
finally: done with hi
```

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

| Блок | `convert("21")` | `convert("hi")` |
|---|---|---|
| `try: number = int(text)` | Успешно: `number = 21` | `ValueError` |
| `except ValueError:` | Пропускается | **Выполняется** |
| `else:` | **Выполняется**: печатает 42 | Пропускается |
| `finally:` | **Выполняется** | **Выполняется** |

`else` и `except` **никогда не выполняются вместе**: либо ошибка была, либо нет. А `finally` выполняется **в обоих случаях**.

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

**«Соединение», которое нужно закрыть.** Здесь мы имитируем соединение с внешним ресурсом (например, с базой данных) и видим, что `finally` закрывает его и тогда, когда что-то падает посередине, и тогда, когда есть `return`. `week-2/connection_demo.py`:

```python
def open_connection():
    print("[connection opened]")

def close_connection():
    print("[connection closed]")

def fetch_price(product_name, prices):
    open_connection()
    try:
        price = prices[product_name]
    except KeyError:
        print("No such product:", product_name)
        return None
    else:
        print("Found", product_name)
        return price
    finally:
        close_connection()

catalog = {"bread": 8, "milk": 6}

print("Result:", fetch_price("milk", catalog))
print()
print("Result:", fetch_price("cake", catalog))
```

**Вывод:**

```text
[connection opened]
Found milk
[connection closed]
Result: 6

[connection opened]
No such product: cake
[connection closed]
Result: None
```

**Разбор:**

- `catalog = {"bread": 8, "milk": 6}` — это **dict** (словарь): каждому ключу (`"bread"`) сопоставлено значение (`8`). `prices["milk"]` возвращает 6. Подробно изучим его в теме структур данных и функций. Пока достаточно знать, что обращение к несуществующему ключу выбрасывает `KeyError`.
- Обратите внимание: `return` есть и в `except`, и в `else`. **И всё же** `[connection closed]` печатается до `Result`. То есть `finally` выполняется **после** того, как `return` «решён», но **до того**, как функция действительно возвращает значение.
- Без `finally` пришлось бы писать `close_connection()` перед **каждым** `return`. Легко забыть, и много повторений.

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

**Ошибка 1: класть очистку после `try`/`except`, а не в `finally`.**

```python
open_connection()
try:
    price = prices[name]
except KeyError:
    print("missing")
    return None
close_connection()   # never reached when except returns!
```

В случае `KeyError` оператор `return` выходит из функции, и `close_connection()` **не выполняется**. Соединение остаётся открытым. Именно это решает `finally`.

**Ошибка 2: код продолжения внутри `try` и `except`, который ловит не ту ошибку.**

```python
try:
    age = int(text)
    category = ["child", "adult"][age // 18]   # can also raise IndexError... or ValueError?
except ValueError:
    print("Age must be a number")
```

Если строка продолжения упадёт, например с `IndexError` для возраста 40 (40 // 18 = 2, а индекса 2 нет), сообщение будет совершенно не по делу. Положите внутрь `try` только `int(text)`, а продолжение — в `else`.

**Ошибка 3: думать, что `finally` «ловит» ошибку.** Нет. Если подходящего `except` нет, `finally` выполняется, а **после этого** программа падает с Traceback.

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

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

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

Напишите, что будет напечатано для каждого вызова, — до запуска:

```python
def half(text):
    try:
        number = int(text)
    except ValueError:
        print("not a number:", text)
    else:
        print("half is", number / 2)
    finally:
        print("checked", text)

half("8")
half("x")
```

{{< solution >}}
```python
def half(text):
    try:
        number = int(text)
    except ValueError:
        print("not a number:", text)
    else:
        print("half is", number / 2)
    finally:
        print("checked", text)


half("8")
half("x")
```

```text
half is 4.0
checked 8
not a number: x
checked x
```

- Для `"8"` ошибки нет, поэтому выполняется `else`, а за ним `finally`.
- Для `"x"` выбрасывается `ValueError`, поэтому выполняется `except` — а `else` **не** выполняется. `finally` выполняется в обоих случаях.
{{< /solution >}}

### Упражнение 2: finally выполняется даже после return

```python
def check(number):
    try:
        if number > 0:
            return "positive"
        return "not positive"
    finally:
        print("done checking", number)

print(check(5))
print(check(-2))
```

Что будет напечатано и в каком порядке? Объясните почему.

{{< solution >}}
```python
def check(number):
    try:
        if number > 0:
            return "positive"
        return "not positive"
    finally:
        print("done checking", number)


print(check(5))
print(check(-2))
```

```text
done checking 5
positive
done checking -2
not positive
```

`finally` выполняется **до того**, как функция действительно возвращает значение, — даже когда внутри `try` есть `return`. Поэтому «done checking» печатается раньше значения, которое вернула функция.
{{< /solution >}}

### Упражнение 3: Четыре блока

Напишите функцию `parse_quantity(text)`, которая использует все четыре блока:

- `try` — преобразует текст в число;
- `except` — печатает `invalid: <text>`;
- `else` — печатает `quantity: <number>`;
- `finally` — печатает `---`.

```python
parse_quantity("3")
parse_quantity("three")
```

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

```text
quantity: 3
---
invalid: three
---
```

{{< solution >}}
```python
def parse_quantity(text):
    try:
        quantity = int(text)
    except ValueError:
        print("invalid:", text)
    else:
        print("quantity:", quantity)
    finally:
        print("---")


parse_quantity("3")
parse_quantity("three")
```

```text
quantity: 3
---
invalid: three
---
```

Код, который выполняется только при полном успехе, находится в `else`, а не внутри `try`, — так `try` остаётся маленьким и ловит только строку, которая действительно может упасть.
{{< /solution >}}

### Упражнение 4: Закрываем то, что открыли

Функции `open_register()` и `close_register()` открывают и закрывают кассу. Напишите функцию `sell(prices, position)`, которая открывает кассу, возвращает цену на выбранной позиции (позиция — индекс списка, начинается с 0) и **всегда** закрывает кассу — даже когда позиции не существует. Для несуществующей позиции возвращается `None`.

```python
def open_register():
    print("register opened")

def close_register():
    print("register closed")

print(sell([10, 20, 30], 1))
print(sell([10, 20, 30], 7))
```

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

```text
register opened
register closed
20
register opened
register closed
None
```

{{< solution >}}
```python
def open_register():
    print("register opened")


def close_register():
    print("register closed")


def sell(prices, position):
    open_register()
    try:
        return prices[position]
    except IndexError:
        return None
    finally:
        close_register()


print(sell([10, 20, 30], 1))
print(sell([10, 20, 30], 7))
```

```text
register opened
register closed
20
register opened
register closed
None
```

Это именно задача `finally`: **очистка** — закрыть открытый ресурс, как бы ни завершилась функция.
{{< /solution >}}

### Упражнение 5: else или внутри try?

Два разработчика написали один и тот же код двумя способами:

```text
# A                              # B
try:                             try:
    number = int(text)               number = int(text)
    print(100 / number)          except ValueError:
except ValueError:                   print("not a number")
    print("not a number")        else:
                                     print(100 / number)
```

В чём разница и какая версия лучше? Что произойдёт в каждой из них, когда `text` равен `"0"`?

{{< solution >}}
- В **A** деление тоже находится внутри `try`. В **B** внутри `try` только преобразование, а деление перенесено в `else`.
- Когда `text` равен `"0"`, обе версии выбрасывают `ZeroDivisionError`, который не ловит ни один `except`, и программа падает. Но в **B** совершенно ясно, что `except ValueError` относится **только** к преобразованию.
- **B лучше**: `try` как можно меньше. Так `except` не ловит по ошибке ошибки из других строк, и легко увидеть, что делает каждая часть.
{{< /solution >}}

<!-- exercises:end -->

## Итоги

- **`try`** — рискованный код. **`except`** — только если произошла подходящая ошибка. **`else`** — только если ошибки **не** было. **`finally`** — **всегда**.
- `except` и `else` никогда не выполняются вместе.
- `finally` выполняется даже после `return`, а также когда ошибка не поймана (и тогда программа падает после него).
- **`else`** сохраняет `try` маленьким и сфокусированным. **`finally`** — место для очистки: закрытия файлов и соединений.

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

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

- [Python Tutorial — Defining Clean-up Actions](https://docs.python.org/3/tutorial/errors.html#defining-clean-up-actions)
- [The try statement](https://docs.python.org/3/reference/compound_stmts.html#the-try-statement)

<!-- docs:end -->
