Перейти к содержимому

Практика 7. Практическая работа 7. Обработка исключений

Цель

  • Освоить полную конструкцию try / except / else / finally и понять роль каждого блока.
  • Научиться перехватывать исключения с учётом иерархии: от частного к общему, по базовому типу.
  • Отработать проброс исключений: пустой raise для повторного возбуждения и связывание первопричины через raise ... from ....
  • Научиться обрабатывать целое семейство ошибок через общий базовый класс собственной иерархии.
  • Развить навык «чтения» поведения программы: предсказывать, что и почему выведется при возникновении исключения.

Краткая теория

Четыре блока. try защищает потенциально опасный код; except ловит ошибку определённого типа; else выполняется только при отсутствии исключений; finally выполняется всегда (в том числе после return) и служит для освобождения ресурсов.

Порядок except важен. Python проверяет блоки сверху вниз и выполняет первый подходящий. Поскольку перехват по базовому типу ловит и все подтипы, конкретные исключения ставят выше общих. Если поставить except OSError выше except FileNotFoundError, то второй блок станет недостижимым.

Иерархия. Все обычные ошибки наследуются от Exception, а KeyboardInterrupt, SystemExit, GeneratorExit — напрямую от BaseException, минуя Exception. Поэтому ловят Exception, а не BaseException. Промежуточные классы (LookupError, ArithmeticError, OSError) позволяют поймать семейство ошибок одним блоком.

Проброс. Пустой raise внутри except повторно возбуждает текущее исключение, сохраняя исходную трассировку («частично обработать и отпустить»). raise НовоеИсключение from исходное заменяет техническую ошибку доменной, не теряя первопричину: в трассировке появится строка «The above exception was the direct cause of the following exception».

Подробное напоминание — в lecture_10.md.


Задания

Задание 1. Калькулятор ввода: все четыре блока

Реализуйте функцию safe_divide(raw_a, raw_b), которая принимает две строки, преобразует их в числа и делит первое на второе. Используйте все четыре блока.

Требования:

  • except ValueError — если строку нельзя преобразовать в число.
  • except ZeroDivisionError — при делении на ноль.
  • else — печать результата (выполняется только при успехе).
  • finally — печать «Операция завершена» в любом случае.
def safe_divide(raw_a: str, raw_b: str) -> float | None:
try:
...
except ValueError:
...
except ZeroDivisionError:
...
else:
...
finally:
...

Проверьте на входах: ("10", "2"), ("10", "0"), ("abc", "2"). Письменно поясните, какие блоки сработали в каждом случае.


Задание 2. Правильный порядок перехвата по иерархии

Дана функция read_config(path), читающая текстовый файл. Реализуйте обработку так, чтобы:

  • FileNotFoundError обрабатывался отдельным сообщением;
  • PermissionError — отдельным сообщением;
  • любая прочая ошибка ввода-вывода ловилась общим except OSError.

Требования:

  • Расставьте блоки except в правильном порядке (от частного к общему).
  • В комментарии объясните, почему except OSError должен стоять последним.
  • Добавьте второй вариант функции с намеренно неверным порядком и письменно опишите, какой блок станет недостижимым и почему.
def read_config(path: str) -> str:
try:
with open(path, encoding="utf-8") as f:
return f.read()
except ...:
...

Задание 3. Проброс: raise и raise ... from ...

Смоделируйте два слоя обработки настроек.

Часть A. Функция process(raw) преобразует строку в число (int(raw) * 2). При ValueError она должна залогировать сообщение и пробросить ту же ошибку дальше пустым raise. Покажите, что вызывающий код получает исходный ValueError.

Часть B. Функция load_setting(path) читает файл и при FileNotFoundError возбуждает собственное доменное исключение ConfigError через raise ConfigError(...) from e.

Требования:

  • Объясните разницу между пустым raise и raise ... from ....
  • В части B перехватите ConfigError в вызывающем коде и через атрибут __cause__ покажите исходную FileNotFoundError.
class ConfigError(Exception):
"""Ошибка загрузки конфигурации."""
def load_setting(path: str) -> str:
try:
...
except FileNotFoundError as e:
raise ConfigError(...) from e

Задание 4. Семейство ошибок через базовый тип

Постройте небольшую иерархию исключений предметной области «Банк»:

  • BankError — базовое исключение домена.
  • InsufficientFundsError(balance, amount) — недостаточно средств.
  • AccountFrozenError(account_id) — счёт заморожен.

Реализуйте withdraw(account, amount), возбуждающую подходящее исключение. В вызывающем коде покажите гибкую обработку:

  • InsufficientFundsError — особая реакция (предложить пополнить счёт);
  • BankError — общий блок для любой другой ошибки домена.

Требования:

  • Каждое исключение хранит контекст в полях и имеет информативный __str__/сообщение.
  • Объясните, почему общий except BankError даёт клиенту вашего кода удобную точку перехвата и почему он должен стоять после except InsufficientFundsError.

Задание 5. Трассировка: «что выведет и почему»

Не запуская код, письменно определите вывод каждого фрагмента и обоснуйте его. Затем проверьте себя запуском.

Фрагмент A:

def f():
try:
return "try"
finally:
print("finally")
print(f())

Фрагмент B:

try:
print("start")
x = [1, 2][5]
print("after")
except KeyError:
print("KeyError")
except LookupError:
print("LookupError")
except Exception:
print("Exception")

Фрагмент C:

try:
raise ValueError("bad")
except ValueError:
print("caught")
raise
finally:
print("cleanup")

Для каждого фрагмента укажите: какие строки напечатаются, в каком порядке, и завершится ли программа с ошибкой. В обосновании опирайтесь на: порядок except, перехват по базовому типу, поведение finally при return и при повторном raise.


Критерии оценки

КритерийВес
Задание 1: корректное использование всех четырёх блоков, разбор трёх входов20%
Задание 2: правильный порядок except, объяснение недостижимого блока20%
Задание 3: верный проброс (raise и raise ... from ...), показ __cause__20%
Задание 4: корректная иерархия и гибкая обработка через базовый тип20%
Задание 5: верный прогноз вывода и обоснование для всех трёх фрагментов15%
Качество кода: информативные сообщения, нет «глухих» except, читаемость5%

Снижение: перехват BaseException или «голый» except без обоснования; «проглатывание» ошибок через except ...: pass; неинформативные сообщения.


Вопросы для самопроверки

  1. В каком порядке выполняются блоки else и finally? Сработает ли finally, если в try есть return?
  2. Почему конкретные except ставят выше общих? Что станет с блоком except FileNotFoundError, если выше него стоит except OSError?
  3. Чем except Exception отличается от except BaseException и почему второй опасен?
  4. В чём разница между пустым raise и raise НовоеИсключение from исходное? Какую информацию сохраняет каждый?
  5. Как один блок except может поймать и IndexError, и KeyError?
  6. Почему общий базовый класс собственной иерархии исключений делает клиентский код более гибким?
  7. Что выведет фрагмент с return в try и print в finally — и почему?