Обработка исключений в контексте

В этом уроке мы рассмотрим более продвинутые техники обработки исключений, которые позволяют создавать более надежные и устойчивые программы. Мы изучим контекстные менеджеры, вложенные исключения и лучшие практики обработки ошибок.

Продвинутые концепции обработки исключений

Контекстные менеджеры и исключения

Контекстные менеджеры (с использованием with) автоматически обрабатывают исключения и обеспечивают корректное освобождение ресурсов:

Контекстные менеджеры с обработкой исключений
# Создание контекстного менеджера с обработкой исключений
class БезопасныйФайл:
    def __init__(self, имя_файла, режим='r'):
        self.имя_файла = имя_файла
        self.режим = режим
        self.файл = None
    
    def __enter__(self):
        try:
            self.файл = open(self.имя_файла, self.режим, encoding='utf-8')
            print(f"Файл '{self.имя_файла}' открыт")
            return self.файл
        except Exception as e:
            print(f"Ошибка при открытии файла: {e}")
            raise
    
    def __exit__(self, тип_исключения, значение_исключения, трассировка):
        if self.файл:
            self.файл.close()
            print(f"Файл '{self.имя_файла}' закрыт")
        
        # Обработка исключений
        if тип_исключения:
            print(f"Произошло исключение: {тип_исключения.__name__}: {значение_исключения}")
            # Возвращаем False, чтобы исключение распространилось дальше
            return False
        
        return True  # Исключение не будет распространяться

# Использование контекстного менеджера
try:
    with БезопасныйФайл("test.txt", 'w') as файл:
        файл.write("Тестовая запись\n")
        print("Данные записаны в файл")
        # Искусственно вызываем исключение для демонстрации
        raise ValueError("Тестовое исключение")
except ValueError as e:
    print(f"Поймано исключение: {e}")

# Пример контекстного менеджера, подавляющего определенные исключения
class ПодавляющийОшибки:
    def __init__(self, *типы_исключений):
        self.типы_исключений = типы_исключений
    
    def __enter__(self):
        return self
    
    def __exit__(self, тип_исключения, значение_исключения, трассировка):
        if тип_исключения and issubclass(тип_исключения, self.типы_исключений):
            print(f"Подавлено исключение: {тип_исключения.__name__}: {значение_исключения}")
            return True  # Подавляем исключение
        return False  # Не подавляем

# Использование подавляющего контекстного менеджера
with ПодавляющийОшибки(ZeroDivisionError, ValueError):
    print("Выполняем код, который может вызвать ошибку...")
    результат = 10 / 0  # ZeroDivisionError будет подавлено
    print("Эта строка не выполнится")

print("Продолжаем выполнение после подавленного исключения")

Агрегирование исключений

Python 3.11+ вводит новую функциональность для агрегирования исключений с помощью ExceptionGroup и except*:

Агрегирование исключений (Python 3.11+)
# Агрегирование исключений (требуется Python 3.11+)
# Пример синтаксиса, может не работать в более старых версиях Python

# Создание группы исключений
def выполнить_несколько_операций():
    исключения = []
    
    # Имитируем несколько операций, которые могут вызвать ошибки
    try:
        int("не число")  # ValueError
    except ValueError as e:
        исключения.append(e)
    
    try:
        10 / 0  # ZeroDivisionError
    except ZeroDivisionError as e:
        исключения.append(e)
    
    try:
        [1, 2, 3][10]  # IndexError
    except IndexError as e:
        исключения.append(e)
    
    # Если есть исключения, создаем группу
    if исключения:
        raise ExceptionGroup("Несколько ошибок", исключения)

# Обработка группы исключений
try:
    выполнить_несколько_операций()
except* ValueError as eg:
    print(f"Пойманы ValueError: {eg.exceptions}")
except* ZeroDivisionError as eg:
    print(f"Пойманы ZeroDivisionError: {eg.exceptions}")
except* IndexError as eg:
    print(f"Пойманы IndexError: {eg.exceptions}")

Логирование исключений

Правильное логирование исключений помогает в отладке и мониторинге приложений:

Логирование исключений
import logging
import traceback

# Настройка логирования
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler("app.log", encoding='utf-8'),
        logging.StreamHandler()
    ]
)

logger = logging.getLogger(__name__)

def деление_с_логированием(a, b):
    try:
        logger.info(f"Попытка деления {a} на {b}")
        результат = a / b
        logger.info(f"Результат деления: {результат}")
        return результат
    except ZeroDivisionError as e:
        logger.error(f"Ошибка деления на ноль: {e}")
        logger.error(f"Трассировка стека:\n{traceback.format_exc()}")
        raise
    except TypeError as e:
        logger.error(f"Ошибка типа данных: {e}")
        logger.error(f"Трассировка стека:\n{traceback.format_exc()}")
        raise
    except Exception as e:
        logger.critical(f"Неожиданная ошибка: {e}")
        logger.critical(f"Трассировка стека:\n{traceback.format_exc()}")
        raise

# Использование функции с логированием
try:
    print(деление_с_логированием(10, 2))
    print(деление_с_логированием(10, 0))
except ZeroDivisionError:
    print("Обработка ошибки деления на ноль в основном коде")

# Пример логирования с пользовательскими полями
def обработать_пользователя(id_пользователя):
    try:
        logger.info("Обработка пользователя", extra={'user_id': id_пользователя})
        # Имитируем обработку
        if id_пользователя < 0:
            raise ValueError("Недопустимый ID пользователя")
        return f"Пользователь {id_пользователя} обработан"
    except ValueError as e:
        logger.error(f"Ошибка обработки пользователя: {e}", extra={'user_id': id_пользователя})
        raise

# Настройка форматтера с пользовательскими полями
class UserFormatter(logging.Formatter):
    def format(self, record):
        if not hasattr(record, 'user_id'):
            record.user_id = 'N/A'
        return super().format(record)

# Применение пользовательского форматтера
for handler in logger.handlers:
    handler.setFormatter(UserFormatter(
        '%(asctime)s - %(name)s - %(levelname)s - User: %(user_id)s - %(message)s'
    ))

# Тестирование с логированием пользователей
try:
    print(обработать_пользователя(123))
    print(обработать_пользователя(-1))
except ValueError:
    print("Обработка ошибки в основном коде")

Обработка исключений в многопоточных приложениях

В многопоточных приложениях исключения в одном потоке не влияют на другие потоки, но требуют специальной обработки:

Исключения в многопоточных приложениях
import threading
import time
import queue

# Очередь для передачи результатов и исключений между потоками
результаты = queue.Queue()

def безопасная_функция(id_задачи, может_вызвать_ошибку=False):
    try:
        print(f"Поток {id_задачи}: начало выполнения")
        time.sleep(1)
        
        if может_вызвать_ошибку:
            raise RuntimeError(f"Ошибка в потоке {id_задачи}")
        
        результат = f"Результат задачи {id_задачи}"
        print(f"Поток {id_задачи}: успешно завершен")
        return результат
    except Exception as e:
        print(f"Поток {id_задачи}: поймано исключение: {e}")
        raise  # Перевыброс для обработки в основном потоке

def рабочий_поток(id_задачи, может_вызвать_ошибку=False):
    try:
        результат = безопасная_функция(id_задачи, может_вызвать_ошибку)
        результаты.put((id_задачи, "успех", результат))
    except Exception as e:
        результаты.put((id_задачи, "ошибка", str(e)))

# Создание и запуск потоков
потоки = []
for i in range(5):
    может_вызвать_ошибку = (i == 2)  # Третий поток вызовет ошибку
    поток = threading.Thread(target=рабочий_поток, args=(i, может_вызвать_ошибку))
    потоки.append(поток)
    поток.start()

# Ожидание завершения всех потоков
for поток in потоки:
    поток.join()

# Обработка результатов
while not результаты.empty():
    id_задачи, статус, данные = результаты.get()
    if статус == "успех":
        print(f"Задача {id_задачи} выполнена: {данные}")
    else:
        print(f"Задача {id_задачи} завершена с ошибкой: {данные}")

# Пример с ThreadPoolExecutor (более современный подход)
from concurrent.futures import ThreadPoolExecutor, as_completed

def задача_с_обработкой(id_задачи):
    try:
        print(f"Выполнение задачи {id_задачи}")
        time.sleep(0.5)
        
        if id_задачи == 3:
            raise ValueError(f"Ошибка в задаче {id_задачи}")
        
        return f"Результат задачи {id_задачи}"
    except Exception as e:
        return Exception(f"Задача {id_задачи} завершена с ошибкой: {e}")

# Использование ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=3) as executor:
    # Запуск задач
    будущие = {executor.submit(задача_с_обработкой, i): i for i in range(5)}
    
    # Обработка результатов по мере завершения
    for будущее in as_completed(будущие):
        id_задачи = будущие[будущее]
        try:
            результат = будущее.result()
            if isinstance(результат, Exception):
                print(f"Ошибка в задаче {id_задачи}: {результат}")
            else:
                print(f"Задача {id_задачи} выполнена: {результат}")
        except Exception as e:
            print(f"Неожиданная ошибка при получении результата задачи {id_задачи}: {e}")

Практические примеры

Рассмотрим практические примеры использования продвинутой обработки исключений:

Система обработки сетевых запросов
import random
import time
from contextlib import contextmanager

# Пользовательские исключения для сетевых операций
class СетеваяОшибка(Exception):
    pass

class ТаймаутОшибка(СетеваяОшибка):
    pass

class АутентификацияОшибка(СетеваяОшибка):
    pass

class СетевойКлиент:
    def __init__(self, базовый_url):
        self.базовый_url = базовый_url
        self.токен = None
    
    def аутентифицировать(self, логин, пароль):
        # Имитация сетевого запроса
        time.sleep(0.1)
        
        if логин == "admin" and пароль == "password":
            self.токен = "токен_аутентификации"
            return True
        else:
            raise АутентификацияОшибка("Неверные учетные данные")
    
    def отправить_запрос(self, endpoint, данные=None, таймаут=5):
        if not self.токен:
            raise АутентификацияОшибка("Требуется аутентификация")
        
        # Имитация сетевого запроса с возможными ошибками
        time.sleep(0.2)
        
        # Имитация случайных ошибок
        случайное_число = random.random()
        
        if случайное_число < 0.1:  # 10% вероятность таймаута
            raise ТаймаутОшибка(f"Таймаут при запросе к {endpoint}")
        elif случайное_число < 0.2:  # 10% вероятность сетевой ошибки
            raise СетеваяОшибка(f"Сетевая ошибка при запросе к {endpoint}")
        
        return {"статус": "успех", "данные": f"Ответ от {endpoint}"}

# Контекстный менеджер для повторных попыток
@contextmanager
def повторные_попытки(максимум_попыток=3, задержка=1):
    попытка = 0
    while попытка < максимум_попыток:
        try:
            yield попытка + 1
            break  # Если успешно, выходим из цикла
        except (ТаймаутОшибка, СетеваяОшибка) as e:
            попытка += 1
            if попытка >= максимум_попыток:
                raise Exception(f"Все попытки исчерпаны. Последняя ошибка: {e}") from e
            
            print(f"Попытка {попытка} не удалась: {e}. Повтор через {задержка} сек...")
            time.sleep(задержка)

# Функция с повторными попытками
def надежный_запрос(клиент, endpoint, данные=None):
    with повторные_попытки(максимум_попыток=3, задержка=1) as попытка:
        print(f"Попытка {попытка} отправки запроса к {endpoint}")
        return клиент.отправить_запрос(endpoint, данные)

# Использование системы
клиент = СетевойКлиент("https://api.example.com")

try:
    # Аутентификация
    клиент.аутентифицировать("admin", "password")
    print("Аутентификация успешна")
    
    # Отправка запроса с повторными попытками
    ответ = надежный_запрос(клиент, "/users")
    print(f"Ответ: {ответ}")
    
except АутентификацияОшибка as e:
    print(f"Ошибка аутентификации: {e}")
except Exception as e:
    print(f"Ошибка запроса: {e}")

Упражнения для самостоятельного решения

Попробуйте решить следующие задачи, применяя полученные знания о продвинутой обработке исключений:

Упражнения

Создайте контекстный менеджер измерить_время(), который измеряет время выполнения блока кода и выводит его. Контекстный менеджер должен корректно обрабатывать любые исключения, которые могут возникнуть в блоке кода, и все равно выводить время выполнения.

import time
from contextlib import contextmanager

@contextmanager
def измерить_время():
    начало = time.time()
    try:
        yield
    finally:
        конец = time.time()
        print(f"Время выполнения: {конец - начало:.4f} секунд")

# Тестирование
with измерить_время():
    time.sleep(1)
    print("Выполнение операции...")

with измерить_время():
    try:
        raise ValueError("Тестовое исключение")
    except ValueError:
        print("Исключение обработано")

Создайте декоратор с_повторными_попытками(максимум_попыток=3, задержка=1), который оборачивает функцию и повторяет её выполнение при определенных исключениях. Декоратор должен принимать список исключений для повторных попыток и задержку между попытками.

import time
import functools

def с_повторными_попытками(максимум_попыток=3, задержка=1, исключения=(Exception,)):
    def декоратор(функция):
        @functools.wraps(функция)
        def обертка(*args, **kwargs):
            for попытка in range(максимум_попыток):
                try:
                    return функция(*args, **kwargs)
                except исключения as e:
                    if попытка == максимум_попыток - 1:
                        raise
                    print(f"Попытка {попытка + 1} не удалась: {e}. Повтор...")
                    time.sleep(задержка)
        return обертка
    return декоратор

# Тестирование
import random

@с_повторными_попытками(максимум_попыток=3, задержка=0.5, исключения=(ValueError,))
def ненадежная_функция():
    if random.random() < 0.7:  # 70% вероятность ошибки
        raise ValueError("Случайная ошибка")
    return "Успех!"

try:
    результат = ненадежная_функция()
    print(f"Результат: {результат}")
except ValueError as e:
    print(f"Все попытки исчерпаны: {e}")

Создайте систему логирования, которая позволяет настраивать уровень детализации логов и сохраняет их в файл. Система должна различать информационные сообщения, предупреждения и ошибки, а также автоматически добавлять информацию о времени и месте возникновения ошибки.

import logging
import traceback
from datetime import datetime
from enum import Enum

class УровеньЛогирования(Enum):
    INFO = "INFO"
    WARNING = "WARNING"
    ERROR = "ERROR"
    CRITICAL = "CRITICAL"

class Логгер:
    def __init__(self, имя_файла="app.log", уровень_детализации=УровеньЛогирования.INFO):
        self.имя_файла = имя_файла
        self.уровень_детализации = уровень_детализации
        self.уровень_порога = self._получить_порог(уровень_детализации)
    
    def _получить_порог(self, уровень):
        уровни = {
            УровеньЛогирования.INFO: 0,
            УровеньЛогирования.WARNING: 1,
            УровеньЛогирования.ERROR: 2,
            УровеньЛогирования.CRITICAL: 3
        }
        return уровни.get(уровень, 0)
    
    def _записать_в_файл(self, уровень, сообщение):
        текущий_порог = self._получить_порог(уровень)
        if текущий_порог < self.уровень_порога:
            return
        
        временная_метка = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        строка_лога = f"[{временная_метка}] {уровень.value}: {сообщение}\n"
        
        with open(self.имя_файла, 'a', encoding='utf-8') as файл:
            файл.write(строка_лога)
        
        # Также выводим в консоль
        print(строка_лога.strip())
    
    def info(self, сообщение):
        self._записать_в_файл(УровеньЛогирования.INFO, сообщение)
    
    def warning(self, сообщение):
        self._записать_в_файл(УровеньЛогирования.WARNING, сообщение)
    
    def error(self, сообщение, исключение=None):
        if исключение:
            сообщение += f"\nИсключение: {исключение}\nТрассировка:\n{traceback.format_exc()}"
        self._записать_в_файл(УровеньЛогирования.ERROR, сообщение)
    
    def critical(self, сообщение, исключение=None):
        if исключение:
            сообщение += f"\nИсключение: {исключение}\nТрассировка:\n{traceback.format_exc()}"
        self._записать_в_файл(УровеньЛогирования.CRITICAL, сообщение)

# Тестирование логгера
логгер = Логгер("тест.log", УровеньЛогирования.INFO)

логгер.info("Приложение запущено")
логгер.warning("Это предупреждение")

try:
    int("не число")
except ValueError as e:
    логгер.error("Ошибка преобразования строки в число", e)

try:
    1 / 0
except ZeroDivisionError as e:
    логгер.critical("Критическая ошибка деления на ноль", e)
Предыдущий урок Следующий урок