Практика 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") raisefinally: 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; неинформативные сообщения.
Вопросы для самопроверки
- В каком порядке выполняются блоки
elseиfinally? Сработает лиfinally, если вtryестьreturn? - Почему конкретные
exceptставят выше общих? Что станет с блокомexcept FileNotFoundError, если выше него стоитexcept OSError? - Чем
except Exceptionотличается отexcept BaseExceptionи почему второй опасен? - В чём разница между пустым
raiseиraise НовоеИсключение from исходное? Какую информацию сохраняет каждый? - Как один блок
exceptможет поймать иIndexError, иKeyError? - Почему общий базовый класс собственной иерархии исключений делает клиентский код более гибким?
- Что выведет фрагмент с
returnвtryиprintвfinally— и почему?