Как обрабатывать ошибки выполнения в 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 принимает опциональный логгер. Он оборачивает функцию, перехватывает все исключения, логирует их и возбуждает повторно. Это удобно для библиотечных функций.

Ошибка выполнения скрипта Python - comments

En
Python script error (python)