Как обрабатывать ошибки выполнения в Python
Основные подходы к обработке ошибок выполнения
Ошибки выполнения (runtime errors) возникают во время работы программы. Они делятся на синтаксические (SyntaxError), исключения встроенных типов (TypeError, ValueError, IndexError и др.) и пользовательские исключения. Без правильной обработки программа аварийно завершается. Далее представлены эффективные решения и их варианты.
Универсальная обработка с try-except и логированием
Основной метод перехвата ошибок - конструкция try-except. Для получения полной информации об исключении используется модуль traceback и logging. Это позволяет не только остановить ошибку, но и сохранить диагностику для анализа.
import logging
import traceback
logging.basicConfig(level=logging.ERROR, filename='errors.log', format='%(asctime)s - %(levelname)s - %(message)s')
def divide(a, b):
try:
result = a / b
return result
except ZeroDivisionError:
logging.error('Деление на ноль: %s / %s', a, b)
raise # повторно возбуждаем, если не хотим скрывать
except Exception as e:
logging.error('Неожиданная ошибка: %s\n%s', e, traceback.format_exc())
# можно вернуть значение по умолчанию или пробросить другим способом
return None
print(divide(10, 0)) # ошибка, но программа продолжит работуPython script error (ошибка выполнения скрипта python)
Пояснение: logging.basicConfig настраивает запись ошибок в файл. Ветка except ZeroDivisionError ловит конкретную ошибку, а общий except Exception - любые другие. traceback.format_exc() возвращает полный стек вызовов. После логирования исключение можно пробросить (raise) или вернуть значение по умолчанию.
Типичные проблемы:
- Перехват всех исключений без разбора (
except:) скрывает критические ошибки (например, KeyboardInterrupt). Рекомендуется указывать конкретные классы или использоватьexcept Exception. - При повторном возбуждении исходный стек может потеряться. Используйте
raiseбез аргументов внутри блока except - сохраняется оригинальный стек. - Незапись стека в лог - ключевая информация теряется. Всегда добавляйте
traceback.format_exc()или используйтеlogging.exception().
Вариант 1: Использование assert для проверки предусловий
Как проверить корректность данных на этапе разработки?
Инструкция assert помогает выявить ошибки раньше. Если условие ложно, возбуждается AssertionError.
def sqrt_positive(x):
assert x >= 0, f'Ожидается неотрицательное число, получено {x}'
return x ** 0.5
print(sqrt_positive(9)) # 3.0
# print(sqrt_positive(-1)) # AssertionError
Пояснение: assert используется для инвариантов и проверки параметров в отладочной версии. В production этот метод обычно отключают, поэтому он не подходит для обработки ошибок выполнения в конечном продукте.
Проблемы и решения:
- При запуске с ключом
-O(оптимизация) все assert игнорируются. Решение: не полагаться на assert для критической валидации, использовать явные проверки с raise. - Сообщение в assert не отображается в production. Решение: для пользовательских сообщений использовать raise AssertionError(msg) вручную.
Вариант 2: Пошаговая отладка с pdb
Как остановить выполнение в произвольном месте для анализа состояния?
Встроенный модуль pdb позволяет устанавливать точки останова в коде. После вызова pdb.set_trace() выполнение приостанавливается, и можно исследовать переменные.
import pdb
def process_list(lst):
total = 0
for i, item in enumerate(lst):
pdb.set_trace() # остановка на каждой итерации
total += item
return total
# process_list([1, 2, 'a']) # вызов приведёт к отладчику
Пояснение: pdb удобен для локальной разработки. Команды: n (next), c (continue), p variable (print). Для production не используется.
Проблемы и решения:
- pdb взаимодействует с консолью - не подходит для автоматизированных сред или production. Решение: использовать удалённую отладку (pdb + socket) или логирование вместо остановки.
- Множество точек останова замедляют выполнение. Решение: добавлять их только при необходимости и удалять после отладки.
Вариант 3: Логирование с разными уровнями
Как записывать детали ошибок в файл с разделением по важности?
Использование logging.exception() автоматически записывает стек исключения. Уровни (DEBUG, INFO, WARNING, ERROR, CRITICAL) помогают фильтровать сообщения.
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
try:
1 / 0
except ZeroDivisionError:
logging.exception('Деление на ноль') # автоматически добавляет traceback
Вывод в лог: 2025-03-31 12:00:00,000 [ERROR] Деление на ноль
Traceback (most recent call last): ...
Проблемы и решения:
- Если не настроен handler, сообщения выводятся в stderr. Решение: настроить файловый handler с ротацией.
- Формат по умолчанию не включает имя модуля. Решение: добавить
%(name)sили%(funcName)s.
Вариант 4: Работа с traceback вручную
Как получить полную информацию об ошибке для вывода или анализа?
Модуль traceback предоставляет функции print_exc() и format_exc() для отображения стека. Это полезно, когда нужно сохранить трассировку в строку.
import traceback
try:
open('missing.txt')
except FileNotFoundError:
tb_str = traceback.format_exc()
print('Трассировка сохранена в переменной:')
print(tb_str)
Пояснение: format_exc() возвращает строку со стеком, которую можно записать в лог, отправить по сети или показать пользователю.
Проблемы и решения:
- Слишком детальный вывод может быть избыточным для пользователя. Решение: фильтровать только нужные кадры с помощью
traceback.extract_tb(). - В асинхронном коде стек может быть неполным. Решение: использовать
traceback.print_exc(chain=True)для отображения цепочек исключений.
Вариант 5: Сторонние сервисы мониторинга (Sentry, Rollbar)
Как централизованно собирать ошибки в production с контекстом?
Инструменты вроде Sentry автоматически перехватывают необработанные исключения, группируют их и показывают окружение, стек, пользовательские данные.
import sentry_sdk
sentry_sdk.init('https://examplePublicKey@o0.ingest.sentry.io/0')
try:
1 / 0
except ZeroDivisionError:
sentry_sdk.capture_exception() # отправляет в Sentry
Пояснение: Sentry SDK перехватывает все исключения, если включена интеграция logging. Можно добавить дополнительный контекст (пользователь, запрос).
Проблемы и решения:
- Зависимость от внешнего сервиса и интернета. Решение: настроить fallback - локальное логирование при недоступности Sentry.
- Чувствительные данные могут попасть в Sentry. Решение: настроить фильтры для очистки (scrubbing).
Расширенные примеры обработки ошибок
1. Создание собственного класса исключения
Пользовательские исключения позволяют точнее классифицировать ошибки предметной области. Класс наследует от Exception и может содержать дополнительные поля.
class NegativeValueError(ValueError):
def __init__(self, value, message='Значение не может быть отрицательным'):
self.value = value
self.message = message
super().__init__(self.message)
def set_age(age):
if age < 0:
raise NegativeValueError(age)
print(f'Возраст: {age}')
# Попытка
set_age(-5)
Traceback (most recent call last): File "", line 1, in File " ", line 5, in set_age raise NegativeValueError(age) NegativeValueError: Значение не может быть отрицательным
Пояснение: кастомное исключение хранит переданное значение (value). В блоке except NegativeValueError as e можно получить e.value для логирования.
2. Игнорирование определённых ошибок с contextlib.suppress
Если нужно проигнорировать исключение и продолжить выполнение без обработчика, используется contextlib.suppress.
from contextlib import suppress
with suppress(FileNotFoundError, PermissionError):
with open('/etc/shadow') as f:
content = f.read()
# Если файла нет или нет прав, ошибка молча игнорируется
print('Программа продолжает работу')
Программа продолжает работу
Пояснение: with suppress(...) подавляет указанные исключения. Полезно для опциональных операций (удаление временных файлов, чтение конфигов).
3. Связывание исключений с помощью raise ... from
При перехвате одного исключения и возбуждении другого полезно сохранить первоначальный контекст для отладки.
def read_config(path):
try:
with open(path) as f:
return f.read()
except FileNotFoundError as e:
raise RuntimeError(f'Не удалось прочитать конфиг {path}') from e
# Вызов
# read_config('missing.conf')
Traceback (most recent call last): File "", line 3, in read_config with open(path) as f: FileNotFoundError: [Errno 2] No such file or directory: 'missing.conf' The above exception was the direct cause of the following exception: Traceback (most recent call last): File " ", line 1, in read_config('missing.conf') File " ", line 5, in read_config raise RuntimeError(f'Не удалось прочитать конфиг {path}') from e RuntimeError: Не удалось прочитать конфиг missing.conf
Пояснение: цепочка исключений показывает оба стека. Используется в функциях высокого уровня, чтобы абстрагировать детали, но не терять исходную ошибку.
4. Объединение исключений в кортеж
Если разные типы ошибок требуют одинаковой реакции, их можно перечислить в кортеже после except.
def calculate(a, b, operation):
operations = {
'add': lambda x, y: x + y,
'divide': lambda x, y: x / y
}
try:
func = operations[operation]
return func(a, b)
except (KeyError, ZeroDivisionError, TypeError) as e:
print(f'Ошибка: {type(e).__name__} - {e}')
return None
print(calculate(10, 0, 'divide')) # ZeroDivisionError
print(calculate(10, 0, 'mod')) # KeyError
Ошибка: ZeroDivisionError - division by zero None Ошибка: KeyError - 'mod' None
Пояснение: кортеж исключений упрощает код при одинаковой логике обработки. Важно: порядок не имеет значения, но нужно избегать включения родительских классов (например, Exception), чтобы не перехватывать лишнего.
5. Настройка ротации логов для production
Модуль logging.handlers.RotatingFileHandler автоматически разбивает лог-файл при достижении определённого размера, сохраняя историю.
import logging
from logging.handlers import RotatingFileHandler
logger = logging.getLogger('my_app')
logger.setLevel(logging.DEBUG)
handler = RotatingFileHandler('app.log', maxBytes=1024*100, backupCount=5)
handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
logger.addHandler(handler)
try:
x = 1 / 0
except ZeroDivisionError:
logger.error('Деление на ноль', exc_info=True)
Пояснение: maxBytes=1024*100 указывает максимальный размер файла (100 КБ). После заполнения создаётся новый файл, старые переименовываются (app.log.1 и т.д.). Параметр backupCount=5 хранит до 5 старых файлов.
6. Декоратор для автоматического логирования ошибок функции
Декоратор позволяет централизованно обрабатывать исключения в множестве функций без дублирования кода.
import functools
import logging
logging.basicConfig(level=logging.ERROR)
def log_exceptions(logger=None):
if logger is None:
logger = logging.getLogger(__name__)
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
logger.exception(f'Исключение в {func.__name__}: {e}')
raise # повторно возбуждаем, чтобы вышестоящий код мог обработать
return wrapper
return decorator
@log_exceptions()
def risky_function(x, y):
return x / y
# Вызов
# risky_function(1, 0) # вызовет ZeroDivisionError и запишет лог
ERROR:__main__:Исключение в risky_function: division by zero Traceback (most recent call last): File "", line 10, in wrapper return func(*args, **kwargs) File " ", line 21, in risky_function return x / y ZeroDivisionError: division by zero
Пояснение: декоратор log_exceptions принимает опциональный логгер. Он оборачивает функцию, перехватывает все исключения, логирует их и возбуждает повторно. Это удобно для библиотечных функций.