# Исключения A: зачем нужна обработка ошибок, try и except<no value>

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

- Что такое **исключение** (Exception).
- Почему настоящая программа должна **обрабатывать** ошибки, а не падать.
- Синтаксис `try` / `except`.
- **Разную** обработку для разных видов ошибок.

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

Представьте маленький сервер-калькулятор: один клиент отправляет `"hello"` вместо числа, **и весь сервер падает**. Все клиенты, которые придут после него, получают «Connection refused».

В реальном мире:

- Пользователь вводит «abc» в поле возраста. Приложение не должно закрываться.
- Файл, который программа пытается прочитать, удалён. Она должна сообщить об этом, а не упасть.
- Интернет пропал посреди запроса. Стоит попробовать снова или показать сообщение.

Ошибки **будут происходить**: плохой ввод, отсутствующие файлы, нестабильная сеть. Вопрос лишь в том, что программа **делает**, когда они происходят.

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

### Что такое исключение?

**Исключение** (Exception) — объект, который Python **«выбрасывает»** (Raise), когда возникает Runtime Error. Все ошибки, которые мы видели вчера (`ValueError`, `ZeroDivisionError`, `TypeError`...), — это исключения.

Когда выбрасывается исключение:

1. Python немедленно **останавливает** текущую строку.
2. Он ищет того, кто **«поймает»** (Catch) ошибку.
3. Если никто не ловит, **программа падает** и печатается Traceback.

`try` / `except` — способ **поймать**.

{{< box "analogy" "Аналогия: страховочная сетка в цирке" >}}
Канатоходец (ваш код) может упасть (исключение). Без сетки падение — конец представления (падение программы). **`try`** = «представление идёт над сеткой». **`except`** = сетка, которая ловит падение, а потом вы решаете, что делать: попробовать снова, сообщить зрителям или перейти к следующему номеру.
{{< /box >}}

### Как это работает

- **`try:`** — «попробуй выполнить этот блок».
- **`except SomeError:`** — «если внутри `try` выброшено `SomeError`, **перейди сюда** и выполни этот блок вместо падения».
- Если ошибки не было, блок `except` **пропускается**.
- После `try`/`except` программа **продолжает как обычно**.

## Синтаксис

```python
try:
    # code that might fail
except ErrorType:
    # what to do if ErrorType happened

# several error types, each handled differently
try:
    ...
except ValueError:
    ...
except ZeroDivisionError:
    ...

# get the error object itself (its message)
except ValueError as error:
    print(error)

# same handling for several types
except (ValueError, TypeError):
    ...
```

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

`week-2/safe_age.py`:

```python
text = input("Enter your age: ")

try:
    age = int(text)
    print("Next year you will be", age + 1)
except ValueError:
    print("That is not a whole number:", text)

print("Program continues normally.")
```

**Запуск 1 (корректный ввод):**

```text
Enter your age: 30
Next year you will be 31
Program continues normally.
```

**Запуск 2 (некорректный ввод):**

```text
Enter your age: abc
That is not a whole number: abc
Program continues normally.
```

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

| Строка | Запуск 1 (`30`) | Запуск 2 (`abc`) |
|---|---|---|
| `text = input(...)` | `text = "30"` | `text = "abc"` |
| `try:` | Входит в блок | Входит в блок |
| `age = int(text)` | Успешно: `age = 30` | **Выбрасывается `ValueError`**, и Python сразу переходит к `except` |
| `print("Next year...")` | Печатается | **Пропускается**. Мы до неё не доходим |
| `except ValueError:` | Пропускается (ошибки нет) | **Поймано!** |
| `print("That is not...")` | Не выполняется | Печатается |
| `print("Program continues...")` | Печатается | Печатается, и программа **не упала** |

**Важный момент:** как только выброшена ошибка, **остальные строки в `try` не выполняются**. Поэтому внутрь `try` кладут только то, что зависит от успеха.

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

**Надёжный калькулятор деления** — с разной обработкой для каждого вида ошибки и циклом, который спрашивает снова. `week-2/safe_divide.py`:

```python
def ask_number(prompt):
    while True:
        text = input(prompt)
        try:
            return float(text)
        except ValueError:
            print("Please type a number, for example 12 or 3.5")

def divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        print("Cannot divide by zero - returning 0")
        return 0

x = ask_number("First number: ")
y = ask_number("Second number: ")
print("Result:", divide(x, y))
```

**Пример запуска:**

```text
First number: ten
Please type a number, for example 12 or 3.5
First number: 10
Second number: 0
Cannot divide by zero - returning 0
Result: 0
```

**Разбор:**

- `return float(text)` внутри `try` — если преобразование удалось, функция немедленно **возвращает** значение и заодно выходит из цикла. Если не удалось, `except` печатает сообщение, и цикл спрашивает снова. Так вы «вынуждаете» пользователя ввести корректное значение.
- Каждая функция ловит ошибку, **которая относится к ней**: `ask_number` ловит `ValueError` (ввод), `divide` ловит `ZeroDivisionError` (вычисление).
- **Открытый вопрос:** должна ли `divide` возвращать 0 при делении на ноль? Математически это неверный ответ! Во многих случаях лучше **не** ловить ошибку здесь и дать решить вызывающему коду. Поговорим об этом в уроке про `raise`.

### `as error` — получение деталей ошибки

```python
try:
    number = int("12.5")
except ValueError as error:
    print("Conversion failed:", error)
```

```text
Conversion failed: invalid literal for int() with base 10: '12.5'
```

`error` — объект ошибки. Если его напечатать, увидите **сообщение**, которое появилось бы в последней строке Traceback.

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

**Ошибка 1: пустой `except`, который ловит всё.**

```python
try:
    age = int(input("Age: "))
    print("Double:", age * 2)
    reslt = age / 0
except:
    print("Something went wrong")
```

Здесь три разные проблемы (ввод, деление на ноль и даже пропущенная буква в имени переменной), и все они спрятаны под «Something went wrong». **Отладить это невозможно.** И хуже всего: пустой `except:` ловит и `KeyboardInterrupt`, поэтому даже `Ctrl + C` не остановит программу, застрявшую в таком цикле!

**Правило:** ловите **только конкретные ошибки, которые вы умеете обрабатывать.** Все остальные идут дальше вверх и приводят к падению — и это **хорошо**, потому что так обнаруживаются баги.

**Ошибка 2: огромный `try`.**

```python
try:
    # 40 lines of code...
except ValueError:
    print("Invalid value")
```

Какая из 40 строк упала? Неизвестно. **Кладите в `try` только строки, которые могут упасть с ловимой ошибкой**, — как можно меньше.

**Ошибка 3: молча «проглотить» ошибку.**

```python
try:
    price = float(text)
except ValueError:
    pass
```

`pass` означает «ничего не делать». Ошибка исчезает, а `price` может вообще не существовать, что вызовет непонятный `NameError` несколькими строками ниже. Всегда **делайте что-нибудь**: сообщение, объяснённое значение по умолчанию или запись в лог.

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

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

### Упражнение 1: Безопасное преобразование в число

Напишите функцию `to_number(text)`, которая возвращает текст как целое число, а если текст не число — возвращает `None`, и программа при этом не падает.

```python
print(to_number("42"))
print(to_number("hello"))
print(to_number("7"))
```

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

```text
42
None
7
```

{{< solution >}}
```python
def to_number(text):
    try:
        return int(text)
    except ValueError:
        return None


print(to_number("42"))
print(to_number("hello"))
print(to_number("7"))
```

```text
42
None
7
```

`int("hello")` выбрасывает `ValueError`. Без `try` программа упала бы. С `except ValueError` мы решаем, что произойдёт вместо этого.
{{< /solution >}}

### Упражнение 2: Разная обработка для каждого вида ошибки

Напишите функцию `safe_divide(a_text, b_text)`, которая получает два текста, преобразует их в числа и делит. Она возвращает:

- результат деления, если всё корректно;
- `"numbers only"`, если один из текстов не число;
- `"cannot divide by zero"`, если делитель равен 0.

```python
print(safe_divide("10", "4"))
print(safe_divide("10", "0"))
print(safe_divide("ten", "2"))
```

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

```text
2.5
cannot divide by zero
numbers only
```

{{< solution >}}
```python
def safe_divide(a_text, b_text):
    try:
        return int(a_text) / int(b_text)
    except ValueError:
        return "numbers only"
    except ZeroDivisionError:
        return "cannot divide by zero"


print(safe_divide("10", "4"))
print(safe_divide("10", "0"))
print(safe_divide("ten", "2"))
```

```text
2.5
cannot divide by zero
numbers only
```

У каждого вида ошибки свой `except` с подходящей обработкой. Python проверяет блоки `except` по порядку и входит в первый подходящий.
{{< /solution >}}

### Упражнение 3: Показываем сообщение об ошибке

Следующий код падает с `ValueError`. Оберните его в `try`, поймайте ошибку через `except ValueError as error` и напечатайте текст `Could not convert:`, а за ним сообщение, которое Python сохранил в `error`:

```python
number = int("12abc")
```

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

```text
Could not convert: invalid literal for int() with base 10: '12abc'
```

{{< solution >}}
```python
try:
    number = int("12abc")
except ValueError as error:
    print("Could not convert:", error)
```

```text
Could not convert: invalid literal for int() with base 10: '12abc'
```

`as error` сохраняет объект ошибки в переменной. Когда вы его печатаете, получаете сообщение, которое создал Python.
{{< /solution >}}

### Упражнение 4: Выбор из списка

У вас есть список имён. Напишите функцию `pick(names, position_text)`, которая получает позицию в виде текста (начиная с 1) и возвращает имя на этой позиции. Она возвращает `"not a number"` для ввода, который не является числом, и `"no such position"` для несуществующей позиции.

```python
names = ["Dana", "Avi", "Noa"]
print(pick(names, "2"))
print(pick(names, "abc"))
print(pick(names, "9"))
```

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

```text
Avi
not a number
no such position
```

{{< solution >}}
```python
def pick(names, position_text):
    try:
        position = int(position_text)
        return names[position - 1]
    except ValueError:
        return "not a number"
    except IndexError:
        return "no such position"


names = ["Dana", "Avi", "Noa"]
print(pick(names, "2"))
print(pick(names, "abc"))
print(pick(names, "9"))
```

```text
Avi
not a number
no such position
```

Две разные ошибки в двух местах: `int(...)` может выбросить `ValueError`, а обращение к списку — `IndexError`.
{{< /solution >}}

### Упражнение 5: except, который ловит всё

```python
def read_age(text):
    try:
        return int(text)
    except:
        return 0
```

1. Почему `except:` без типа ошибки опасен?
2. Приведите пример настоящего бага, который скрыл бы этот код.
3. Исправьте код.

{{< solution >}}
1. Пустой `except:` ловит **любую** ошибку — включая те, о которых мы не подумали, — и скрывает их. Код продолжает работать, будто всё в порядке, с неверным значением.
2. Если кто-то по ошибке передаст `None` вместо текста, будет выброшен `TypeError` — настоящий баг в коде, который вызвал функцию. Вместо того чтобы увидеть его, мы молча получим возраст 0. (Даже `Ctrl + C` ловится пустым `except:`.)
3. Ловить только ту ошибку, которую мы ожидаем:

```python
def read_age(text):
    try:
        return int(text)
    except ValueError:
        return 0


print(read_age("30"))
print(read_age("thirty"))
```

```text
30
0
```
{{< /solution >}}

<!-- exercises:end -->

## Итоги

- **Исключение** (Exception) = ошибка, которую Python выбрасывает во время выполнения. Если её никто не ловит, программа падает.
- `try` = «попробуй». `except SomeError` = «если произошла эта ошибка, сделай это вместо падения».
- Как только выброшена ошибка, **остаток `try` пропускается**.
- `except SomeError as error` даёт доступ к сообщению об ошибке.
- Ловите **только конкретные ошибки**, с **маленьким** `try`. Никаких пустых `except:` и никаких `pass`, которые проглатывают ошибки.

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

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

- [Python Tutorial — Handling Exceptions](https://docs.python.org/3/tutorial/errors.html#handling-exceptions)
- [Built-in Exceptions](https://docs.python.org/3/library/exceptions.html#concrete-exceptions)

<!-- docs:end -->
