Уроки курса
1 Введение в Python и философия дзен
30 мин
2 Переменные и динамическая типизация
30 мин
3 Базовые типы данных: числа, строки и булевы значения
30 мин
4 Изменяемые и неизменяемые объекты (Mutable vs Immutable)
30 мин
5 Форматирование строк и f-строки
30 мин
6 Углубленная работа со списками
30 мин
7 Кортежи и их особенности
30 мин
8 Словари под капотом
30 мин
9 Множества и математические операции
30 мин
10 Генераторы списков (List Comprehensions)
30 мин
11 Генераторы словарей и множеств
30 мин
12 Встроенные функции для коллекций
30 мин
13 Условные операторы и логические выражения
30 мин
14 Циклы while и управление потоком
30 мин
15 Итерация с циклом for
30 мин
16 Конструкции for...else и while...else
30 мин
17 Функции enumerate и zip
30 мин
18 Создание собственных функций (def)
30 мин
19 Позиционные и именованные аргументы
30 мин
20 Проблема изменяемых аргументов по умолчанию
30 мин
21 Произвольное число аргументов (*args и **kwargs)
30 мин
22 Область видимости переменных (LEGB)
30 мин
23 Анонимные функции (lambda)
30 мин
24 Функции высшего порядка
30 мин
25 Замыкания (Closures)
30 мин
26 Введение в объектно-ориентированное программирование
30 мин
27 Атрибуты классов и экземпляров
30 мин
28 Магический метод __init__
30 мин
29 Методы экземпляра
30 мин
30 Инкапсуляция и сокрытие данных
30 мин
31 Декоратор @property
30 мин
32 Наследование классов
30 мин
33 Переопределение методов и функция super()
30 мин
34 Полиморфизм в Python
30 мин
35 Магические методы строк (__str__ и __repr__)
30 мин
36 Обработка исключений (try-except)
30 мин
37 Блоки else и finally
30 мин
38 Генерация собственных исключений (raise)
30 мин
39 Открытие и чтение файлов
30 мин
40 Запись данных в файлы
30 мин
41 Контекстные менеджеры (with)
30 мин
42 Работа с форматом JSON
30 мин
43 Модули и импорты
30 мин
44 Полезные модули стандартной библиотеки
30 мин
45 Модуль datetime
30 мин
46 Модуль collections
30 мин
47 Виртуальные окружения (venv)
30 мин
48 Установка сторонних пакетов через pip
30 мин
49 Организация структуры Python-проекта
30 мин
50 Финальный проект: создание приложения
30 мин

Обработка исключений (try-except)

Безопасное выполнение кода и перехват потенциальных ошибок без аварийного завершения программы.

Прогресс урока: 0%

Введение в концепцию исключительных ситуаций

В мире программирования на языке Python, концепция обработки исключений занимает одно из самых центральных мест в архитектуре надежных и отказоустойчивых приложений. Когда интерпретатор Python выполняет ваш код, он может столкнуться с ситуациями, которые не были предусмотрены стандартным потоком выполнения. В отличие от синтаксических ошибок (SyntaxError), которые выявляются еще на этапе парсинга и компиляции исходного кода в байт-код (до начала реального выполнения программы), исключения возникают исключительно во время выполнения (runtime). Это означает, что синтаксически ваш код абсолютно корректен, но логика или внешние факторы (например, отсутствие запрашиваемого файла, обрыв сетевого соединения, неверный тип данных от пользователя, деление на ноль) приводят к невозможности дальнейшего штатного выполнения инструкций.

Исторически в таких языках, как C, для сигнализации об ошибках использовались коды возврата (return codes). Функция возвращала 0 в случае успеха и какое-либо другое число в случае ошибки. Это заставляло разработчиков писать огромное количество шаблонного кода для проверки каждого вызова функции, что приводило к так называемому 'лапша-коду' и ухудшало читаемость бизнес-логики. Более того, если разработчик забывал проверить код возврата, программа могла продолжить работу с некорректным состоянием, что в итоге приводило к катастрофическим последствиям и трудноуловимым багам (silent failures).

Python использует совершенно иной, более современный и безопасный подход: механизм исключений (Exceptions). Когда происходит нештатная ситуация, Python создает специальный объект — исключение (Exception Object) — и 'выбрасывает' (raises) его. Если этот объект не перехватить и не обработать в коде, он будет распространяться вверх по стеку вызовов (Call Stack), пока не достигнет верхнего уровня интерпретатора. На этом этапе программа аварийно завершится (crash), и в стандартный поток вывода ошибок (stderr) будет выведен так называемый Traceback — подробный отчет о том, в каком файле, на какой строке и в какой функции произошла ошибка, а также цепочка вызовов, которая к ней привела. Понимание того, как читать Traceback и как управлять потоком выполнения с помощью блоков try-except, является фундаментальным навыком, отличающим разработчика уровня Junior от уверенного Middle-специалиста.

python
def divide_numbers(a, b):
    # Синтаксически код верен, но b может быть нулем
    return a / b

# Это вызовет аварийное завершение программы:
# ZeroDivisionError: division by zero
print(divide_numbers(10, 0))

В какой момент времени в Python возникают исключения (например, ZeroDivisionError или FileNotFoundError)?

Как называется специальный класс ошибок в Python, которые выявляются ДО выполнения программы (например, при пропущенном двоеточии)? Напишите название класса.

Что произойдет, если исключение возникло внутри функции, но в этой функции нет блока для его обработки?

Парадигмы программирования: LBYL против EAFP

Для глубокого понимания философии Python необходимо рассмотреть две основные парадигмы проверки условий и обработки ошибок: LBYL (Look Before You Leap — 'Посмотри, прежде чем прыгнуть') и EAFP (Easier to Ask for Forgiveness than Permission — 'Проще попросить прощения, чем разрешения'). Эти два подхода кардинально отличаются в том, как они управляют потоком выполнения и обрабатывают потенциально опасные операции.

Подход LBYL характерен для многих классических языков программирования, таких как C или Java. Суть LBYL заключается в том, что перед выполнением любого потенциально опасного действия вы пишете цепочку проверок if-else, чтобы убедиться в безопасности операции. Например, перед чтением файла вы проверяете, существует ли он, есть ли у вас права на чтение, не заблокирован ли он другим процессом. Перед делением переменных вы проверяете, не равен ли делитель нулю. Перед обращением к ключу в словаре вы проверяете его наличие с помощью оператора 'in'. Основной недостаток LBYL в Python заключается в том, что проверки замедляют выполнение программы (особенно в циклах), загромождают код бизнес-логики 'защитным' кодом и, что самое главное, подвержены так называемому состоянию гонки (Race Condition). Состояние гонки возникает, когда в многопоточной или многопроцессной среде условие, проверенное оператором if, может измениться между моментом проверки и моментом выполнения операции. Например: вы проверили, что файл существует, но за микросекунду до того, как вы начали его читать, другой процесс этот файл удалил. В этом случае ваш подход LBYL все равно приведет к ошибке во время выполнения.

Подход EAFP является так называемым 'Pythonic way' (идиоматичным для Python). Вместо того чтобы тратить время и ресурсы на предварительные проверки, мы просто предполагаем, что операция выполнится успешно, и пишем код прямолинейно. Однако мы 'оборачиваем' этот код в блок перехвата исключений (try-except). Если операция проходит успешно, код выполняется максимально быстро без накладных расходов на проверки. Если же возникает проблема (например, ключа нет в словаре или файл удален), Python генерирует исключение, которое мы 'элигантно' перехватываем в блоке except и обрабатываем. EAFP решает проблему состояния гонки, так как мы реагируем на реальный результат выполнения операции, а не на наше предположение о ее безопасности. Код в стиле EAFP обычно более читаем, так как бизнес-логика (в блоке try) четко отделена от логики обработки ошибок (в блоке except). Более того, создание и вызов исключений в современных версиях Python сильно оптимизированы, и блоки try, внутри которых не возникает исключений, выполняются практически без потери производительности ('Zero-cost exceptions' в Python 3.11+).

python
person = {'name': 'Alice', 'age': 25}

# Подход LBYL (Look Before You Leap)
if 'job' in person:
    print(person['job'])
else:
    print('Профессия не указана')

# Подход EAFP (Easier to Ask for Forgiveness than Permission) - Pythonic way
try:
    print(person['job'])
except KeyError:
    print('Профессия не указана')

Как расшифровывается аббревиатура EAFP, которая является стандартом написания кода в Python?

Задание

Перепишите логику кода из парадигмы LBYL в парадигму EAFP. Представьте, что у вас есть код: if hasattr(obj, 'do_something'): obj.do_something() else: print('Метод не найден')

  • Создайте конструкцию try-except.
  • В блоке try попытайтесь напрямую вызвать obj.do_something().
  • В блоке except перехватите AttributeError.
  • В обработчике исключения выведите строку 'Метод не найден'.
10 баллов

Какую критическую проблему (возникающую в многопоточных средах) решает подход EAFP, которой подвержен подход LBYL?

Базовый синтаксис: блоки try и except

Механизм управления исключениями в Python строится вокруг использования составной инструкции, начинающейся с ключевого слова try. Блок try определяет область видимости кода, который мы считаем 'опасным' или в котором мы ожидаем возможное возникновение исключений. Синтаксис требует, чтобы за блоком try следовал как минимум один блок except или блок finally. Нельзя написать try сам по себе.

Алгоритм выполнения базовой конструкции try-except выглядит следующим образом: интерпретатор начинает выполнять инструкции внутри блока try строго последовательно, сверху вниз. Если ни одна из инструкций не вызывает исключения, интерпретатор просто перешагивает через все прикрепленные блоки except и продолжает выполнение программы дальше. Однако, если на какой-либо строке внутри try возникает исключительная ситуация, выполнение блока try немедленно прерывается. Все инструкции, находящиеся ниже проблемной строки в том же блоке try, игнорируются и никогда не будут выполнены в рамках этого прохода. Интерпретатор мгновенно перепрыгивает к блоку except. Если тип возникшего исключения совпадает с типом, указанным в операторе except, выполняется код внутри блока обработки. После завершения блока except, программа не возвращается в блок try, а продолжает выполнение со строки, следующей после всей конструкции try-except. Это означает, что аварийного завершения не происходит, и программа остается стабильной.

Критически важно понимать опасность использования так называемого 'голого' except (bare except). Если вы напишете конструкцию except: без указания конкретного класса исключения, Python перехватит абсолютно все ошибки. Сначала может показаться, что это удобно — программа никогда не упадет. Но на практике это считается одним из самых грубых антипаттернов в Python. 'Голый' except перехватит не только ваши бизнес-ошибки (например, деление на ноль или отсутствие ключа), но и критические системные события, такие как `KeyboardInterrupt` (когда пользователь нажимает Ctrl+C для принудительной остановки скрипта) или `MemoryError`. В результате вы получите программу-'зомби', которую невозможно закрыть штатным образом и которая тихо 'проглатывает' серьезные баги, затрудняя отладку. Всегда перехватывайте только те исключения, появления которых вы реально ожидаете и знаете, как именно их обработать.

python
print('Старт программы')
try:
    print('Пытаемся выполнить опасный код')
    result = 10 / 0  # Здесь происходит ZeroDivisionError
    print('Эта строка никогда не выполнится')
except ZeroDivisionError:
    print('Ошибка: деление на ноль! Подставляем значение по умолчанию')
    result = 0

print(f'Программа продолжает работу. Результат: {result}')

Если внутри блока try возникло исключение на второй строке из пяти, что произойдет с оставшимися тремя строками в этом блоке try?

Почему использование 'голого' except (просто except:) считается плохой практикой?

Иерархия встроенных исключений Python

Для правильного перехвата ошибок необходимо понимать, как организована структура исключений в Python. Все классы исключений построены на принципах объектно-ориентированного программирования (ООП) и образуют строгую иерархию наследования. На самой вершине этого дерева находится класс BaseException. От него наследуются четыре основных ветви: SystemExit (вызывается функцией sys.exit()), KeyboardInterrupt (генерация при нажатии Ctrl+C), GeneratorExit и, самое главное, класс Exception.

Класс Exception является базовым для всех встроенных 'обычных' ошибок, с которыми вы сталкиваетесь при написании бизнес-логики: ValueError, TypeError, IndexError, KeyError, ZeroDivisionError и множества других. Важное правило проектирования: когда вы пишете блок except, вы всегда должны перехватывать конкретные ошибки (например, except ValueError). Если же вы хотите написать общий обработчик ('перехватить вообще любую программную ошибку'), вы должны использовать except Exception:, но ни в коем случае не except BaseException: и не голый except. Перехватывая Exception, вы поймаете все математические ошибки, ошибки типов и коллекций, но позволите исключениям SystemExit и KeyboardInterrupt беспрепятственно завершить программу (так как они наследуются не от Exception, а параллельно от BaseException).

Понимание иерархии также влияет на то, в каком порядке вы располагаете блоки except, если их несколько. Поскольку проверка типов исключений идет сверху вниз, вы должны указывать более специфичные (узкие) классы исключений выше, а более общие — ниже. Если вы сначала напишете except Exception:, а затем except ValueError:, блок для ValueError никогда не выполнится, потому что ValueError является подклассором Exception, и первое же условие перехватит его. Это классическая логическая ошибка (unreachable code), которую часто допускают новички.

python
def process_data(data_str):
    try:
        # int() может вызвать ValueError, если строка не число
        value = int(data_str)
        result = 100 / value
    except ValueError:
        print('Ошибка: передана не числовая строка!')
    except ZeroDivisionError:
        print('Ошибка: нельзя делить на ноль!')
    except Exception: # Общий перехватчик ОБЯЗАТЕЛЬНО должен быть последним
        print('Произошла непредвиденная программная ошибка')

process_data('abc') # Сработает ValueError
process_data('0')   # Сработает ZeroDivisionError

От какого базового класса наследуются такие ошибки, как ValueError и TypeError, но НЕ наследуется KeyboardInterrupt?

Если у вас есть несколько блоков except, какой класс исключения должен стоять самым последним: TypeError, ValueError или Exception?

Группировка исключений и извлечение объекта ошибки (as e)

Часто в процессе разработки возникает ситуация, когда для обработки нескольких совершенно разных типов исключений требуется выполнить один и тот же код (например, вывести одинаковое сообщение об ошибке для пользователя или залогировать инцидент и вернуть статус HTTP 400 Bad Request). Писать для каждого типа ошибки отдельный блок except с дублирующимся кодом — это прямое нарушение принципа DRY (Don't Repeat Yourself). В Python предусмотрен элегантный синтаксис для группировки исключений: вы можете указать несколько классов исключений в одном блоке except, обернув их в круглые скобки, то есть передав кортеж (tuple) классов. Например: except (ValueError, TypeError, KeyError):. Обратите внимание, что круглые скобки обязательны; если их опустить, Python воспримет это как старый синтаксис Python 2, что приведет к ошибке.

Кроме того, при перехвате исключения нам часто требуется не просто знать факт его возникновения, но и получить к нему программный доступ: прочитать оригинальное сообщение об ошибке, извлечь пользовательские параметры или проанализировать детали. Исключение в Python — это полноценный объект класса. Мы можем 'поймать' этот объект в переменную, используя ключевое слово as (алиас). Стандартом де-факто в сообществе является использование имени переменной e или err. Запись except ValueError as e: означает: 'если возникла ошибка ValueError, перехвати ее и сохрани сам объект этой ошибки в переменную e'. Внутри блока except мы можем использовать эту переменную. Вызов print(e) вызовет магический метод __str__ исключения, который вернет строковое описание проблемы (например, 'invalid literal for int() with base 10'). Это невероятно полезно при логировании, так как позволяет сохранять точные детали того, что именно пошло не так, не прерывая при этом работу приложения.

Стоит отметить одну важную особенность управления памятью: переменная e, в которую сохраняется исключение, существует только внутри блока except. Как только выполнение выходит за пределы этого блока, интерпретатор Python автоматически удаляет эту переменную (вызывает del e), чтобы разорвать возможные циклические ссылки и предотвратить утечки памяти, так как объект исключения содержит ссылку на весь traceback (фреймы стека), что может занимать много памяти.

python
import logging

def fetch_user_data(user_data):
    try:
        # Может возникнуть KeyError, если нет ключа 'age'
        # Может возникнуть TypeError, если user_data не словарь
        age = int(user_data['age'])
        print(f'Возраст: {age}')
    except (KeyError, TypeError) as e:
        # Группируем две ошибки и ловим объект в переменную 'e'
        logging.error(f'Ошибка обработки данных пользователя: {e}')
        print('Не удалось получить возраст из-за неверных данных')

fetch_user_data({'name': 'John'}) # Вызовет KeyError
fetch_user_data(['age', 25])      # Вызовет TypeError

Как правильно перехватить два разных исключения (IndexError и KeyError) одним блоком except?

Какое ключевое слово используется в Python для присвоения объекта перехваченного исключения переменной (например, except ValueError ... e)?

Задание

Ваша задача - написать конструкцию для перехвата ошибки деления на ноль с сохранением сообщения об ошибке.

  • Начните блок try и попытайтесь выполнить операцию 10 / 0.
  • Напишите блок except для перехвата ZeroDivisionError.
  • Используйте алиас (as) для сохранения объекта исключения в переменную err.
  • Внутри блока except выведите переменную err на печать.
10 баллов

Доступна ли переменная исключения (например, 'e' в конструкции except Exception as e:) после завершения блока except?

Блок else: логика успешного выполнения

Многие разработчики, даже имеющие некоторый опыт, удивляются, узнав, что конструкция try-except может содержать блок else. Подобно тому, как else используется в циклах for и while, в контексте обработки исключений он имеет весьма специфическое и полезное назначение. Блок else располагается сразу после всех блоков except (но перед блоком finally, если он есть). Код внутри блока else выполняется только и исключительно в том случае, если код внутри блока try отработал до самого конца без возникновения каких-либо исключений.

Возникает закономерный вопрос: зачем нужен блок else, если можно просто написать этот успешный код в самом конце блока try? Причина кроется в правильном дизайне и сужении 'зоны риска'. Правило хорошего тона в Python гласит: помещайте в блок try только ту одну-две строчки кода, которые реально могут вызвать ожидаемое исключение. Если вы поместите большой кусок бизнес-логики в try, вы рискуете перехватить исключение (например, KeyError), которое возникло не в той строке, где вы его ожидали, а где-то в другом месте этой большой функции, что приведет к ложной обработке и маскировке багов. Блок else решает эту проблему элегантно: вы кладете опасную операцию в try, обрабатываете возможную ошибку в except, а все последующие действия (которые не должны генерировать ожидаемую ошибку) переносите в else. Если в блоке else произойдет какая-либо ошибка, она уже не будет перехвачена блоками except текущей конструкции, а пробросится дальше, что логически абсолютно правильно.

Рассмотрим реальный пример: вы пишете скрипт для подключения к базе данных. В блок try вы помещаете вызов db.connect(). В except ConnectionError вы обрабатываете отсутствие связи. А вот запрос к базе данных db.query() следует поместить в блок else. Если подключение не удалось, мы попадем в except и скрипт безопасно прервется или сделает ретрай. Если подключение успешно, выполнится else. Если же внутри db.query() возникнет ошибка (например, неверный SQL-синтаксис), мы не хотим, чтобы наш except ConnectionError случайно поймал ее (если они наследуются от одного корня), поэтому логическое разделение на try/except/else является наиболее безопасной архитектурой.

python
def process_file(filename):
    try:
        # Помещаем в try ТОЛЬКО ту операцию, где ожидаем FileNotFoundError
        file = open(filename, 'r')
    except FileNotFoundError:
        print(f'Ошибка: файл {filename} не найден.')
    else:
        # Выполняется ТОЛЬКО если файл успешно открылся
        print(f'Файл открыт успешно. Читаем данные...')
        data = file.read()
        file.close()
        return data

process_file('existing.txt')
process_file('missing.txt')

При каком условии выполняется код, находящийся в блоке else в конструкции try-except?

Почему логику, которая выполняется после успешного блока try, лучше поместить в блок else, а не оставить в самом конце блока try?

Блок finally: гарантированное выполнение и очистка ресурсов

Четвертым и последним компонентом полноценной структуры управления исключениями является блок finally. Его главная и единственная задача — предоставить разработчику механизм для выполнения кода, который должен быть выполнен абсолютно всегда, при любых обстоятельствах, независимо от того, произошло ли исключение в блоке try, было ли оно перехвачено в except, или код успешно отработал и прошел через else. Блок finally располагается в самом низу конструкции. Эта конструкция является критически важной для управления системными ресурсами, требующими явного освобождения (очистки): закрытия открытых файлов, разрыва сетевых соединений (веб-сокеты, соединения с базами данных), освобождения блокировок (mutex/locks) в многопоточном программировании.

Чтобы понять всю мощь блока finally, рассмотрим несколько сценариев. Сценарий 1: все прошло успешно, отработали try и else, после них выполняется finally. Сценарий 2: в try возникла ошибка, она поймана в except, обработана, после этого выполняется finally. Сценарий 3 (самый интересный): внутри блока try возникает исключение, но у нас нет подходящего блока except для его перехвата (или произошел 'голый' raise внутри самого except). В этом случае исключение начинает распространяться вверх по стеку. Кажется, что программа немедленно упадет. Однако, интерпретатор Python приостанавливает распространение исключения (замораживает его), заходит в блок finally, полностью выполняет его код, и только после этого 'размораживает' исходное исключение и программа падает! Это гарантирует, что даже при критическом сбое ваши файлы будут закрыты, а данные не повредятся.

Более того, поведение finally обладает еще одним магическим свойством, связанным с операторами прерывания потока: return, break и continue. Если внутри блока try (или except) находится инструкция return, выходящая из функции, Python не вернет значение сразу. Он запомнит значение для возврата, перейдет в блок finally, выполнит его, и только потом осуществит реальный выход из функции. Однако стоит быть предельно осторожным: если вы поместите инструкцию return прямо внутрь блока finally, она переопределит (перепишет) любой return из блоков try/except, а также молча подавит любые неперехваченные исключения. Использование return внутри finally считается серьезным антипаттерном в Python.

python
def divide_and_close(a, b):
    print('Открываем ресурс (например, соединение)...')
    try:
        return a / b
    except ZeroDivisionError:
        print('Ошибка деления!')
        return 0
    finally:
        # Этот блок выполнится ПЕРЕД тем, как отработают return из try/except
        # и даже если произойдет TypeError (например, при передаче строк)
        print('Закрываем ресурс. Блок finally отработал!')

print('Результат 1:', divide_and_close(10, 2))
print('---')
print('Результат 2:', divide_and_close(10, 0))

Какое утверждение о блоке finally является верным?

Для какой основной задачи чаще всего используется блок finally в программировании? Напишите одно слово (существительное), обозначающее этот процесс (например, 'очистка', 'освобождение', 'закрытие').

Что произойдет, если в функции блок try вызывает return 5, а блок finally в этой же функции вызывает return 10?

Генерация исключений: инструкция raise

До сих пор мы рассматривали ситуации, когда исключения возникают неявно: интерпретатор Python сам генерирует ошибку при выполнении недопустимой операции (деление на ноль, отсутствие файла). Однако в арсенале разработчика есть мощный инструмент для явного контроля потока — ключевое слово raise. Инструкция raise позволяет вам самостоятельно, программно инициировать любую исключительную ситуацию в любом месте вашего кода. Это необходимо для того, чтобы защитить ваши функции и классы от неверных входных данных (так называемый fail-fast подход — упасть как можно раньше, не дожидаясь катастрофы глубже в логике).

Как правильно использовать raise? Самый распространенный паттерн — валидация аргументов. Представьте функцию, которая вычисляет корень квадратный или возраст человека. Технически, Python позволит передать туда отрицательное число. Но с точки зрения вашей бизнес-логики возраст -20 бессмысленен. В начале функции вы пишете проверку: if age < 0: raise ValueError('Возраст не может быть отрицательным'). При вызове raise вы создаете экземпляр класса исключения (чаще всего ValueError или TypeError для проверки аргументов) и передаете в него человекочитаемое сообщение. Это сообщение затем будет отображено в traceback, помогая другим разработчикам, использующим вашу функцию, сразу понять свою ошибку.

Второй, не менее важный сценарий использования raise (без аргументов) — это перехват и повторный проброс (re-raise) исключения внутри блока except. Допустим, в процессе работы с базой данных возникла ошибка. Вы перехватываете ее, чтобы записать факт ошибки в лог-файл (чтобы администратор узнал о проблеме), но вы не знаете, как именно восстановить работу программы в данной функции. Если вы просто запишете лог и выйдете из except, программа решит, что ошибка успешно обработана, и продолжит работу с неверным состоянием. Чтобы этого избежать, после логирования вы пишете просто raise. Эта конструкция берет текущее перехваченное исключение и пробрасывает его дальше вверх по стеку вызовов, сохраняя оригинальный traceback в неизменном виде. Высший уровень архитектуры затем примет решение о том, как завершить запрос пользователя (например, показав страницу 500 Internal Server Error).

python
def set_user_age(age):
    # Явная генерация исключения для защиты бизнес-логики
    if not isinstance(age, int):
        raise TypeError('Возраст должен быть целым числом')
    if age < 0 or age > 150:
        raise ValueError(f'Недопустимый возраст: {age}')
    
    print(f'Возраст успешно установлен: {age}')

# Вызов с неверным типом прервет программу с понятным сообщением
# set_user_age('двадцать')
# set_user_age(-5)

Какое ключевое слово используется для самостоятельного вызова (генерации) исключения в Python?

Что делает ключевое слово raise, если оно используется без аргументов внутри блока except?

Пользовательские исключения (Custom Exceptions)

По мере роста вашего проекта и усложнения бизнес-логики встроенных исключений (таких как ValueError или KeyError) становится недостаточно для адекватного описания специфичных проблем предметной области. Представьте, что вы разрабатываете банковское приложение. Если клиент пытается перевести денег больше, чем есть у него на счету, генерация стандартного ValueError выглядит неинформативно. ValueError может означать ошибку в типе строки, ошибку в парсинге, что угодно. В таких случаях архитектурно правильным решением является создание собственных, пользовательских классов исключений. Это позволяет вызывающему коду очень точно перехватывать конкретные бизнес-ошибки и не путать их с системными сбоями.

В Python создание собственного исключения сводится к простому наследованию от встроенного класса Exception. Технически, для создания базового пользовательского исключения достаточно написать две строчки кода: class InsufficientFundsError(Exception): pass. Вы объявляете новый класс и наследуете его от Exception (важно: именно от Exception, а не от BaseException). Наличие инструкции pass означает, что класс использует всю механику работы с traceback и инициализацией, заложенную в родительском классе. Теперь вы можете вызывать это исключение в коде: raise InsufficientFundsError('Недостаточно средств').

Однако, возможности ООП позволяют пойти дальше и добавить в исключение собственные атрибуты и методы. Вы можете переопределить магический метод __init__, чтобы принимать не только текстовое сообщение, но и, например, текущий баланс клиента и сумму попытки перевода. Эти данные сохраняются как атрибуты экземпляра (self.balance, self.amount). Затем вы можете переопределить метод __str__, чтобы при выводе ошибки на экран она формировала красивое и детальное сообщение автоматически. Когда вызывающий код перехватит такое исключение с помощью алиаса except InsufficientFundsError as e:, он сможет обратиться к e.balance и программно решить, что делать дальше (например, предложить клиенту микрозайм на недостающую сумму, вместо того чтобы просто показывать красное окно ошибки). Это превращает обработку исключений из механизма предотвращения крашей в мощный инструмент маршрутизации бизнес-логики.

python
# Создание собственного исключения
class InsufficientFundsError(Exception):
    def __init__(self, message, balance, required):
        super().__init__(message)
        self.balance = balance
        self.required = required
        
    def __str__(self):
        return f'{self.args[0]} (Баланс: {self.balance}, Требуется: {self.required})'

def withdraw(amount, balance):
    if amount > balance:
        # Генерация пользовательского исключения с параметрами
        raise InsufficientFundsError('Отклонено', balance, amount)
    return balance - amount

try:
    withdraw(5000, 1000)
except InsufficientFundsError as e:
    print(f'Перехвачена ошибка банка: {e}')
    print(f'Вам не хватает {e.required - e.balance} рублей.')

От какого класса должны наследоваться создаваемые вами пользовательские классы исключений?

Задание

Создайте собственное исключение InvalidEmailError для системы регистрации.

  • Объявите класс InvalidEmailError, унаследовав его от встроенного класса Exception.
  • Добавьте в тело класса инструкцию pass для простейшей реализации.
  • В функции регистрации используйте ключевое слово raise для вызова вашего InvalidEmailError, если email не содержит символ '@'.
10 баллов

Нововведения: ExceptionGroup и блок except* (Python 3.11+)

Язык Python постоянно развивается, и в версии 3.11 механизм исключений получил одно из самых масштабных обновлений за последние годы: были введены группы исключений (ExceptionGroup) и новый синтаксис перехвата except*. Чтобы понять, зачем это нужно, рассмотрим проблему асинхронного программирования (asyncio) или конкурентного выполнения задач. Когда вы запускаете несколько независимых задач параллельно (например, скачиваете данные с трех разных сайтов), вполне может случиться так, что сразу несколько из них завершатся с ошибкой одновременно (один сайт вернул 404, второй отвалился по таймауту). Стандартный механизм raise способен 'нести' в себе только один объект исключения. Пришлось бы писать сложные 'обертки', чтобы не потерять информацию об ошибках параллельных потоков.

Для решения этой архитектурной проблемы был создан встроенный класс ExceptionGroup. Это особый тип исключения, который выступает в роли контейнера: он оборачивает внутри себя список из других исключений. Вы можете сгенерировать группу так: raise ExceptionGroup('Ошибки загрузки', [ValueError('Сайт 1'), ConnectionError('Сайт 2')]). При этом Traceback, выводимый в консоль, был существенно переработан, чтобы визуально показывать древовидную структуру вложенных ошибок, что делает отладку параллельного кода невероятно удобной.

Однако, стандартный блок except не умеет эффективно работать с группами: если вы напишете except ValueError:, он не перехватит ValueError, спрятанный глубоко внутри ExceptionGroup. Для этого и был введен новый синтаксис except* (произносится как 'except star'). Конструкция except* ValueError: дает интерпретатору команду: 'распакуй ExceptionGroup, найди внутри все исключения типа ValueError, извлеки их и выполни для них этот блок обработки, а остальные ошибки из группы пропусти дальше'. Самое удивительное в синтаксисе except* то, что один и тот же блок try может привести к срабатыванию сразу нескольких блоков except*. Если в группе есть и ValueError, и TypeError, то сначала отработает блок except* ValueError (получив подгруппу ошибок значений), а затем отработает блок except* TypeError. Это фундаментально меняет классическую логику 'только один except может быть выполнен', открывая дорогу к надежному построению сложных распределенных систем на Python.

Для чего в Python 3.11 был добавлен класс ExceptionGroup?

Какой новый оператор (синтаксис) перехвата был добавлен в Python 3.11 для работы с ExceptionGroup, позволяющий 'распаковывать' группы и выполнять сразу несколько блоков перехвата? (Напишите ключевое слово с символом).