Исключения Python: как избежать сбоев
Основы обработки исключений
Главный механизм
Для перехвата и обработки ошибок в Python применяется конструкция try–except. Она позволяет выполнить потенциально опасный код и, в случае возникновения исключения, перейти к заранее подготовленному блоку обработки.
try:
user_input = input("Введите число: ")
number = int(user_input)
print(f"Вы ввели {number}")
except ValueError:
print("Ошибка: введено не число")ошибки в python (ошибки в python)
В этом примере блок try содержит преобразование строки в число. Если пользователь введёт нечисловое значение, возникает ValueError, и выполнение переходит к except. Без обработки программа завершилась бы аварийно.
Типичная проблема
Новички часто оборачивают в try слишком много кода, что затрудняет поиск источника ошибки. Рекомендуется помещать в блок только те строки, которые могут вызвать конкретное исключение.
Как обработать несколько типов исключений?
Если блок может породить разные исключения, можно перечислить их в кортеже или использовать отдельные except.
try:
value = int(input())
result = 10 / value
except ValueError:
print("Нужно число")
except ZeroDivisionError:
print("Деление на ноль")Проблема
Порядок except важен: если первое исключение является родительским для второго, второй блок никогда не выполнится. Например, except Exception: перехватит все исключения, включая ValueError, поэтому более конкретные обработчики должны идти раньше.
Как выполнить код независимо от исключения?
Блок finally выполняется всегда, даже если в try произошёл выход через return или break. Это удобно для закрытия файлов или освобождения ресурсов.
file = None
try:
file = open("data.txt")
content = file.read()
except FileNotFoundError:
print("Файл не найден")
finally:
if file:
file.close()Ошибка
В finally не рекомендуется размещать операции, которые могут снова вызвать исключение, иначе оно перекроет предыдущее. Если это необходимо, внутри finally используют вложенный try-except.
Как добавить действие при успешном выполнении?
Блок else выполняется, если в try не возникло исключения. Это помогает отделить основной сценарий от аварийного.
try:
number = int(input())
except ValueError:
print("Ошибка преобразования")
else:
print(f"Введено число {number}")Замечание
Код в else не выполняется, если произошёл выход через return, break или continue внутри блока try. Это редкий случай, но его стоит учитывать.
Как создать собственное исключение?
Иногда нужно описать ошибку, специфичную для приложения. Для этого создают класс, наследующий от Exception.
class NegativeValueError(Exception):
"""Исключение для отрицательных чисел."""
pass
def check_positive(value):
if value < 0:
raise NegativeValueError("Значение не может быть отрицательным")
return value
try:
check_positive(-5)
except NegativeValueError as e:
print(f"Ошибка: {e}")Проблема
Собственные исключения должны быть документированы, иначе другим разработчикам будет непонятно, когда и почему они возникают. Также не стоит излишне плодить классы, если достаточно стандартных встроенных исключений.
Расширенные примеры работы с исключениями
Цепочка исключений (chained exceptions)
В Python 3 можно сохранять контекст исходной ошибки с помощью raise ... from. Это помогает отследить первопричину.
def read_file(filename):
try:
with open(filename) as f:
return f.read()
except FileNotFoundError:
raise ValueError("Невозможно прочитать конфигурацию") from None # отключаем цепочку
try:
read_file("missing.cfg")
except ValueError as e:
print(e)
print("Скрытый контекст не показан, так как использовано from None")Невозможно прочитать конфигурацию Скрытый контекст не показан, так как использовано from None
Повторный вызов исключения (re-raise)
Иногда нужно частично обработать ошибку, а затем передать её дальше. Для этого применяют raise без аргументов внутри блока except.
def safe_divide(a, b):
try:
return a / b
except ZeroDivisionError:
print("Попытка деления на ноль")
raise # повторно возбуждает то же исключение
try:
safe_divide(10, 0)
except ZeroDivisionError:
print("Обработано на верхнем уровне")Попытка деления на ноль Обработано на верхнем уровне
Логирование исключений с traceback
Вместо простого вывода ошибки в консоль лучше использовать модуль logging, который записывает полную трассу стека.
import logging
logging.basicConfig(level=logging.ERROR)
try:
1 / 0
except ZeroDivisionError:
logging.exception("Произошло деление на ноль")ERROR:root:Произошло деление на ноль Traceback (most recent call last): File "<stdin>", line 2, in <module> ZeroDivisionError: division by zero
Использование assert для отладки
Оператор assert проверяет условие и возбуждает AssertionError при его ложности. Он удобен для самопроверок в коде, но отключается при запуске с оптимизацией (-O).
def factorial(n):
assert n >= 0, "Факториал определён только для неотрицательных чисел"
if n == 0:
return 1
return n * factorial(n - 1)
print(factorial(5))
print(factorial(-1)) # вызовет AssertionError120 Traceback (most recent call last): ... AssertionError: Факториал определён только для неотрицательных чисел
Фильтрация предупреждений (warnings)
Некоторые ошибки не являются критическими, но заслуживают внимания. warnings позволяет управлять их отображением.
import warnings
warnings.warn("Эта функция устарела", DeprecationWarning)
# Для игнорирования предупреждений:
warnings.filterwarnings("ignore", category=DeprecationWarning)
warnings.warn("Это сообщение не будет показано", DeprecationWarning)(при первом вызове вывод предупреждения, при втором – ничего)