Ключевые вопросы параграфа
- Что такое исключение и как оно влияет на выполнение программы?
- В чём разница между подходами 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
выполнится. Если же файл импортируют как модуль — этот код пропускается.
Почему это важно
Если вы пишете утилиту, в которой вызывается ввод/вывод или другой основной код сразу при запуске, то при импорте этого файла всё будет выполняться. Это может привести к неожиданным эффектам в другой программе.
Как правильно импортировать модули
Есть два способа импортировать модуль:
- Импортировать целиком (всё пространство имён).
- Импортировать отдельные объекты.
Рассмотрим каждый на примерах.
Импортируем целиком
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__":
.