5.3. Модель исключений Python. Try, except, else, finally. Модули

В этом параграфе вы разберётесь, как Python позволяет обрабатывать ошибки во время выполнения программ. Вы узнаете, что такое исключения и как они устроены, научитесь использовать конструкции try, except, else и finally, чтобы перехватывать и обрабатывать ошибки. Также рассмотрите два подхода к управлению ошибками и научитесь создавать собственные классы исключений. В завершение вы познакомитесь с понятием модуля в Python и узнаете, как правильно организовывать и импортировать код в разных частях программы.

Ключевые вопросы параграфа

  • Что такое исключение и как оно влияет на выполнение программы?
  • В чём разница между подходами LBYL и EAFP при работе с ошибками?
  • Как работает конструкция try–except и когда использовать else и finally?
  • Как устроена иерархия исключений в Python и как создать собственный класс исключения?
  • Что такое модуль и как правильно его импортировать и использовать в других программах?

Что такое исключение и как оно влияет на выполнение программы

При выполнении заданий к предыдущим параграфам вы, скорее всего, нередко сталкивались с возникновением ошибок. В этом разделе мы разберёмся, что именно происходит при таких ошибках — и как Python позволяет их обрабатывать.

Напишем программу, которая считает обратные значения для целых чисел из заданного диапазона и выводит их в одну строку с разделителем ;. Один из возможных вариантов решения выглядит так:

1print(";".join(str(1 / x) for x in range(int(input()), int(input()) + 1)))

Программа получилась очень лаконичной за счёт использования генератора внутри join. Однако если в диапазоне окажется число 0 (например, от -1 до 1), возникнет ошибка:

1ZeroDivisionError: division by zero

Ошибка прерывает выполнение программы — она называется исключением. Исключения в Python — это события, которые происходят при нарушении корректного выполнения кода и останавливают его работу.

Попробуем избежать ошибки деления на ноль. Допустим, при наличии 0 в диапазоне программа просто выводит сообщение и не выполняет деление. Для этого добавим проверку:

1interval = range(int(input()), int(input()) + 1)
2if 0 in interval:
3    print("Диапазон чисел содержит 0.")
4else:
5    print(";".join(str(1 / x) for x in interval))

Теперь программа работает безопасно для диапазонов вроде от -2 до 2. Однако она по-прежнему может завершиться ошибкой, если ввести не числа:

1ValueError: invalid literal for int() with base 10: 'a'

Чтобы предотвратить это, нужно проверить корректность ввода заранее. Для этого используем метод .isdigit() и учтём возможность отрицательных чисел с помощью .lstrip("-"):

1start = input()
2end = input()
3if not (start.lstrip("-").isdigit() and end.lstrip("-").isdigit()):
4    print("Необходимо ввести два числа.")
5else:
6    interval = range(int(start), int(end) + 1)
7    if 0 in interval:
8        print("Диапазон чисел содержит 0.")
9    else:
10        print(";".join(str(1 / x) for x in interval))

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

В чём разница между подходами LBYL и EAFP при работе с ошибками

Подход, который был нами применён для предотвращения ошибок, называется Look Before You Leap (LBYL), или «Cначала проверь, потом действуй». В программе, реализующей такой подход, проверяются возможные условия возникновения ошибок до исполнения основного кода.

У подхода LBYL есть свои недостатки. Программу из примера стало сложнее читать из-за вложенного условного оператора. Проверка условия, что строка может быть преобразована в число, выглядит даже сложнее, чем само списочное выражение.

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

Существует другой подход для работы с ошибками: Easier to Ask Forgiveness than Permission (EAFP), или «Проще попросить прощения, чем разрешения». В этом подходе сначала исполняется код, а в случае возникновения ошибок происходит их обработка. Подход EAFP реализован в Python в виде механизма обработки исключений.

LBYL и EAFP в Python

Параметр сравнения

LBYL (Look Before You Leap)

EAFP (Easier to Ask Forgiveness than Permission)

Идея

Сначала проверяем, можно ли выполнить действие

Сначала пытаемся выполнить, а потом обрабатываем ошибки

Характер кода

Защитный, с большим числом условий

Оптимистичный, с обработкой ошибок постфактум

Минусы

Много условий, код перегружен проверками

Возможны неожиданные ошибки, если не учесть исключения

Типичный сценарий

Возможные ошибки малочисленны или
не требуют сложной проверки

Ошибки возникают на разных уровнях вложенности кода, но приводят к равноценным вариантам обработки

Как работает конструкция try–except и когда использовать else и finally

Чтобы обрабатывать ошибки в Python, используется конструкция try–except. Она позволяет «поймать» исключение, если оно произошло, и выполнить нужные действия — вместо того чтобы программа аварийно завершилась.

Базовый синтаксис

1try:
2    <код, который может вызвать исключение>
3except <КлассИсключения_1>:
4    <обработка ошибки этого типа>
5except <КлассИсключения_2>:
6    <обработка другого типа ошибки>
7...
8else:
9    <выполняется, если исключений не было>
10finally:
11    <выполняется всегда -- и при ошибках, и без>

Почему важен порядок блоков except

Когда в блоке try возникает ошибка, Python начинает искать подходящий блок except, чтобы её обработать. Он делает это сверху вниз, по порядку.

Если первым стоит except Exception, то он перехватит практически любое исключение — почти все встроенные ошибки наследуются от Exception. В этом случае интерпретатор не дойдёт до остальных блоков, и они просто не выполнятся. Это означает, что более специфические обработчики окажутся бесполезными — их код никогда не сработает.

Пример 1. Правильный порядок

1try:
2    print(1 / int(input()))
3except ZeroDivisionError:
4    print("Ошибка деления на ноль.")
5except ValueError:
6    print("Невозможно преобразовать строку в число.")
7except Exception:
8    print("Неизвестная ошибка.")

Если ввести 0, будет выведено:

Ошибка деления на ноль.

Если ввести a, будет выведено:

Невозможно преобразовать строку в число.

Если произойдёт другая ошибка — сработает except Exception.

Пример 2. Неправильный порядок

1try:
2    print(1 / int(input()))
3except Exception:
4    print("Неизвестная ошибка.")
5except ZeroDivisionError:
6    print("Ошибка деления на ноль.")
7except ValueError:
8    print("Невозможно преобразовать строку в число.")

Здесь всегда сработает первый блок except Exception, и остальные никогда не будут выполнены. Поэтому при вводе и 0, и a программа выведет только:

Неизвестная ошибка.

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

Блок else

else используется, если вы хотите выполнить действия только в случае, если ошибок не было. Это удобно для явного разделения логики и обработки ошибок:

1try:
2    print(1 / int(input()))
3except ZeroDivisionError:
4    print("Ошибка деления на ноль.")
5except ValueError:
6    print("Невозможно преобразовать строку в число.")
7except Exception:
8    print("Неизвестная ошибка.")
9else:
10    print("Операция выполнена успешно.")

Ввод 5:

0.2  
Операция выполнена успешно.

Ввод 0:

Ошибка деления на ноль.

Блок finally

Блок finally всегда выполняется, независимо от того, было ли исключение. Его используют для завершения операций — например, чтобы закрыть файл или освободить ресурсы:

1try:
2    print(1 / int(input()))
3except ZeroDivisionError:
4    print("Ошибка деления на ноль.")
5except ValueError:
6    print("Невозможно преобразовать строку в число.")
7except Exception:
8    print("Неизвестная ошибка.")
9else:
10    print("Операция выполнена успешно.")
11finally:
12    print("Программа завершена.")

Программа завершится с финальной строкой даже в случае ошибки.

Конструкция try–except позволяет не расписывать множество условий заранее. Программа становится проще и чище, особенно по сравнению с подходом LBYL, где много вложенных проверок.

Сравним их.

Подход LBYL

1start = input()
2end = input()
3if not (start.lstrip("-").isdigit() and end.lstrip("-").isdigit()):
4    print("Введите два числа.")
5else:
6    interval = range(int(start), int(end) + 1)
7    if 0 in interval:
8        print("Диапазон чисел содержит 0.")
9    else:
10        print(";".join(str(1 / x) for x in interval))

Подход EAFP

Перепишем код, созданный с применением подхода LBYL, для первого примера из этого параграфа с использованием обработки исключений:

1try:
2    print(";".join(str(1 / x) for x in range(int(input()), int(input()) + 1)))
3except ZeroDivisionError:
4    print("Диапазон чисел содержит 0.")
5except ValueError:
6    print("Необходимо ввести два числа.")

Код с исключениями короче, проще читается, и логика задачи не утопает в проверках.

Оператор raise

Иногда нужно не просто обрабатывать исключения, но и вызывать их вручную, если данные некорректны или программа зашла в «опасное» состояние. Для этого используется оператор raise:

1raise <КлассИсключения>(сообщение)

Например:

1x = int(input("Введите число: "))
2if x < 0:
3    raise ValueError("Число должно быть положительным")

Как устроена иерархия исключений в Python и как создать собственный класс исключения

Исключения — это классы, а значит, вы можете не только использовать встроенные, но и создавать собственные. Такие классы могут наследоваться от Exception или любого другого подходящего базового исключения.

Исключения в Python являются объектами классов ошибок. Сами классы исключений организованы в иерархию, где все стандартные исключения наследуются от базового класса BaseException. Благодаря этому механизму можно группировать исключения и обрабатывать их гибко и последовательно.

В документации Python версии 3.10.8 приводится следующее дерево иерархии стандартных исключений:

BaseException
+-- SystemExit
+-- KeyboardInterrupt
+-- GeneratorExit
+-- Exception
+-- StopIteration
+-- StopAsyncIteration
+-- ArithmeticError
| +-- FloatingPointError
| +-- OverflowError
| +-- ZeroDivisionError
+-- AssertionError
+-- AttributeError
+-- BufferError
+-- EOFError
+-- ImportError
| +-- ModuleNotFoundError
+-- LookupError
| +-- IndexError
| +-- KeyError
+-- MemoryError
+-- NameError
| +-- UnboundLocalError
+-- OSError
| +-- BlockingIOError
| +-- ChildProcessError
| +-- ConnectionError
| | +-- BrokenPipeError
| | +-- ConnectionAbortedError
| | +-- ConnectionRefusedError
| | +-- ConnectionResetError
| +-- FileExistsError
| +-- FileNotFoundError
| +-- InterruptedError
| +-- IsADirectoryError
| +-- NotADirectoryError
| +-- PermissionError
| +-- ProcessLookupError
| +-- TimeoutError
+-- ReferenceError
+-- RuntimeError
| +-- NotImplementedError
| +-- RecursionError
+-- SyntaxError
| +-- IndentationError
| +-- TabError
+-- SystemError
+-- TypeError
+-- ValueError
| +-- UnicodeError
| +-- UnicodeDecodeError
| +-- UnicodeEncodeError
| +-- UnicodeTranslateError
+-- Warning
+-- DeprecationWarning
+-- PendingDeprecationWarning
+-- RuntimeWarning
+-- SyntaxWarning
+-- UserWarning
+-- FutureWarning
+-- ImportWarning
+-- UnicodeWarning
+-- BytesWarning
+-- EncodingWarning
+-- ResourceWarning

Создание собственных исключений

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

Создавать исключения следует через наследование от стандартных классов, чаще всего от Exception.

Например, создадим программу, которая складывает список целых чисел, но выбрасывает исключения, если в списке есть хотя бы одно чётное или отрицательное число.

Для этого мы создадим:

  • NumbersError — базовый класс для ошибок, связанных с числами;
  • EvenError — пользовательское исключение, которое мы будем выбрасывать, если в списке есть хотя бы одно чётное число;
  • NegativeError — пользовательское исключение, которое выбрасывается, если есть хотя бы одно отрицательное число.

Код:

1class NumbersError(Exception):
2    pass
3
4class EvenError(NumbersError):
5    pass
6
7class NegativeError(NumbersError):
8    pass
9
10def no_even(numbers):
11    if all(x % 2 != 0 for x in numbers):
12        return True
13    raise EvenError("В списке не должно быть чётных чисел")
14
15def no_negative(numbers):
16    if all(x >= 0 for x in numbers):
17        return True
18    raise NegativeError("В списке не должно быть отрицательных чисел")
19
20def main():
21    print("Введите числа в одну строку через пробел:")
22    try:
23        numbers = [int(x) for x in input().split()]
24        if no_negative(numbers) and no_even(numbers):
25            print(f"Сумма чисел равна: {sum(numbers)}.")
26    except NumbersError as e:  # Обращение к исключению как к объекту
27        print(f"Произошла ошибка: {e}.")
28    except Exception as e:
29        print(f"Произошла непредвиденная ошибка: {e}.")
30  
31if __name__ == "__main__":
32    main()

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

Почему это удобно

  • Исключения стали наглядными и осмысленными: из текста ошибки понятно, что именно пошло не так.
  • Мы можем разделять обработку разных типов ошибок — и, если нужно, по-разному на них реагировать.
  • Такой подход помогает писать надёжный и предсказуемый код, особенно в сложных проектах.

Что такое модуль и как правильно его импортировать и использовать в других программах

Каждая программа на Python — это потенциальный модуль, который можно импортировать в другие файлы. Это мощный инструмент повторного использования кода.

Как Python понимает, запущен ли файл напрямую

Чтобы определить, является файл самостоятельной программой или его импортировали как модуль, используют конструкцию:

1if __name__ == "__main__":
2    main()

Если файл запущен напрямую, __name__ будет равен "__main__" и блок кода внутри if выполнится. Если же файл импортируют как модуль — этот код пропускается.

Почему это важно

Если вы пишете утилиту, в которой вызывается ввод/вывод или другой основной код сразу при запуске, то при импорте этого файла всё будет выполняться. Это может привести к неожиданным эффектам в другой программе.

Как правильно импортировать модули

Есть два способа импортировать модуль:

  1. Импортировать целиком (всё пространство имён).
  2. Импортировать отдельные объекты.

Рассмотрим каждый на примерах.

Импортируем целиком

1import example_module

Теперь обращаться к функциям нужно через имя модуля:

1example_module.some_function()

Импортируем отдельные объекты

1from example_module import some_function, ExampleClass

Теперь вы можете использовать some_function() напрямую — но помните, что она теперь находится в пространстве имён текущей программы и имена не должны конфликтовать с другими переменными и функциями.

Пример: что может пойти не так

У нас есть файл module_hello.py с таким содержанием:

1def hello(name):
2    return f"Привет, {name}!"
3
4print(hello(input("Введите своё имя: ")))

И есть вторая программа program.py, в которой мы хотим использовать функцию hello():

1from module_hello import hello
2
3print(hello(input("Добрый день. Введите имя: ")))

Если запустить program.py, вы получите два запроса имени подряд.

Потому что при импорте module_hello Python выполнил весь код в файле, включая print(hello(...)). То есть программа начала работать ещё до того, как дошла до строки print(...) в program.py.

Как это исправить

Мы можем изменить module_hello.py, чтобы код выполнялся только при прямом запуске, а не при импорте:

1def hello(name):
2    return f"Привет, {name}!"
3
4if __name__ == "__main__":
5    print(hello(input("Введите своё имя: ")))

Теперь при импорте ничего не будет выполнено.
Для большей читаемости основную часть программы часто оформляют как отдельную функцию main():

1def hello(name):
2    return f"Привет, {name}!"
3
4def main():
5    print(hello(input("Введите своё имя: ")))
6
7if __name__ == "__main__":
8    main()

А можно ли импортировать всё? Можно — но не нужно.

Вот так делать не рекомендуется:

1from some_module import *
  • Это загрязняет пространство имён: вы не видите, откуда взялась переменная x или функция run().
  • Если в вашем файле уже есть объект с таким именем — он будет перезаписан.
  • Это затрудняет чтение и отладку кода.

✅ Вы разобрались, как работают классы и объекты в Python?

👉 Оценить этот параграф

Что дальше

Вы научились обрабатывать ошибки в Python с помощью конструкции try–except, использовать блоки else и finally, а также правильно расставлять обработчики исключений в порядке от частных к общим. Разобрались в разнице между подходами LBYL и EAFP, освоили синтаксис вызова исключений с помощью raise и создание собственных классов ошибок. Поняли, как устроены модули в Python и как безопасно импортировать функции и классы из одного файла в другой.

В следующем, заключительном параграфе главы, мы ещё раз соберём всё воедино: вспомним ключевые инструменты работы с числами, таблицами, запросами и модулями — и обсудим, как использовать их вместе для создания настоящих Python-проектов.

А пока вы не ушли дальше — закрепите материал на практике:

  • Отметьте, что урок прочитан, при помощи кнопки ниже.
  • Пройдите мини-квиз, чтобы проверить, насколько хорошо вы усвоили тему.
  • Перейдите к задачам этого параграфа и потренируйтесь.
  • Перед этим загляните в короткий гайд о том, как работает система проверки.

Хотите обсудить, задать вопрос или не понимаете, почему код не работает? Мы всё предусмотрели — вступайте в сообщество Хендбука! Там студенты помогают друг другу разобраться.

Ключевые выводы параграфа

  • Исключения — это ошибки, возникающие во время выполнения программы. Их можно обрабатывать с помощью конструкции try–except, чтобы - программа не завершалась аварийно.
  • Подход EAFP («проще просить прощения, чем разрешения») делает код чище и удобнее, чем подход LBYL («сначала проверь, потом действуй»), особенно при работе с вводом пользователя и нестабильными данными.
  • Блоки else и finally позволяют отделить основной код от обработки ошибок и гарантированно выполнять завершающие действия.
  • Собственные классы исключений создаются через наследование от Exception и позволяют описывать уникальные ситуации в логике вашей программы.
  • Модули в Python — это отдельные файлы с кодом, который можно переиспользовать. Чтобы избежать лишнего выполнения при импорте, используют проверку if __name__ == "__main__":.

Отмечайте параграфы как прочитанные чтобы видеть свой прогресс обучения

Вступайте в сообщество хендбука

Здесь можно найти единомышленников, экспертов и просто интересных собеседников. А ещё — получить помощь или поделиться знаниями.
Вступить
Сообщить об ошибке
Предыдущий параграф5.2. Волшебные методы, переопределение методов. Наследование

В этом параграфе вы познакомитесь с принципами наследования в Python и научитесь строить производные классы на основе уже существующих. Вы разберётесь, как расширять и переопределять методы базовых классов, а также узнаете, что такое множественное наследование и как Python обрабатывает конфликты между родительскими классами. Кроме того, вы узнаете, что такое специальные (или магические) методы Python и как они позволяют объектам взаимодействовать со встроенными функциями и операциями — например, print(), +, == или in.

Следующий параграф5.4. Чему вы научились

Начать практику в облаке

Основ Python хватит для того, чтобы создать реальное приложение. Для размещения своих первых практических проектов можно использовать технологии Yandex Cloud. Бесплатные курсы «Основы работы с Yandex Cloud» и «Профессия “Инженер облачных сервисовˮ» помогут разобраться с тем, как разместить свои проекты в облаках.