В этом уроке мы рассмотрим более продвинутые техники обработки исключений, которые позволяют создавать более надежные и устойчивые программы. Мы изучим контекстные менеджеры, вложенные исключения и лучшие практики обработки ошибок.
Контекстные менеджеры (с использованием 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
# Создание группы исключений
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)