25. Аккуратная обработка исключений
В главе 24 мы узнали, как читать и понимать исключения, когда они возникают. Теперь мы узнаем, как обрабатывать исключения аккуратно, позволяя нашим программам восстанавливаться после ошибок вместо аварийного завершения. Это необходимо для написания надёжных, удобных для пользователя программ, которые могут справляться с неожиданными ситуациями.
Когда в Python возникает исключение, нормальный ход выполнения программы сразу же прекращается. Но что, если бы мы могли перехватить это исключение до того, как оно «уронит» нашу программу? Что, если бы мы могли отреагировать на ошибку — например, попросив пользователя попробовать снова, или используя значение по умолчанию, или записав проблему в лог и продолжив работу? Именно это и позволяет нам делать обработка исключений.
25.1) Использование блоков try и except
25.1.1) Базовая структура try и except
Блок try-except — это способ Python сказать: «попробуй сделать это, и если возникнет исключение, сделай вместо этого вот это». Базовая структура выглядит так:
try:
# Код, который может вызвать исключение
risky_operation()
except:
# Код, который выполняется, если происходит ЛЮБОЕ исключение
print("Something went wrong!")Блок try содержит код, который может вызвать исключение. Если в любом месте блока try возникает исключение, Python немедленно прекращает выполнение блока try и переходит к блоку except. Если исключение не возникает, блок except полностью пропускается.
Рассмотрим конкретный пример. Помните из главы 24, что попытка преобразовать недопустимую строку в целое число вызывает ValueError:
# Без обработки исключений — программа падает
user_input = "hello"
number = int(user_input) # ValueError: invalid literal for int() with base 10: 'hello'
print("This line never executes")Теперь обработаем это исключение аккуратно:
# С обработкой исключений — программа продолжает работу
user_input = "hello"
try:
number = int(user_input)
print(f"You entered: {number}")
except:
print("That's not a valid number!")
number = 0 # Используем значение по умолчанию
print(f"Using number: {number}")Output:
That's not a valid number!
Using number: 0Программа не упала! Когда int(user_input) вызвал ValueError, Python перешёл к блоку except, напечатал наше сообщение об ошибке, установил значение по умолчанию, а затем продолжил выполнение оставшейся части программы.
Вот что происходит шаг за шагом:
Понимание «прыжка» — что на самом деле происходит
Когда мы говорим, что Python «прыгает» к блоку except, мы имеем в виду, что он прерывает нормальное последовательное выполнение. Это фундаментальное изменение того, как течёт выполнение вашей программы, — не просто простая ветка, как в операторе if. Давайте рассмотрим это подробно на конкретном примере:
# Наблюдаем поток выполнения при исключениях
print("1. Starting program")
try:
print("2. Entered try block")
number = int("hello") # Исключение происходит ЗДЕСЬ
print("3. After conversion") # Эта строка НИКОГДА не выполняется
result = number * 2 # Эта строка НИКОГДА не выполняется
print("4. After calculation") # Эта строка НИКОГДА не выполняется
except ValueError:
print("5. In except block - handling the error")
print("6. After try-except block")Output:
1. Starting program
2. Entered try block
5. In except block - handling the error
6. After try-except blockОбратите внимание: строки 3 и 4 никогда не выполняются! В момент, когда int("hello") вызывает ValueError, Python:
- Останавливает выполнение блока try немедленно — прямо на строке, где произошло исключение
- Ищет подходящую ветку except, которая может обработать этот тип исключения
- Переходит в этот блок except, пропуская весь оставшийся код в блоке try
- Продолжает выполнение после конструкции try-except, как только блок except завершится
Это принципиально отличается от обычного потока выполнения. При обычном выполнении Python запускает каждую строку последовательно. При исключении Python бросает текущий путь и выбирает полностью другой маршрут. Без обработки исключений программа упала бы на строке 2 и завершилась. С обработкой исключений программа восстанавливается и продолжает работу.
Почему это важно:
Понимание этого поведения «прыжка» критически важно, потому что:
- Любой код после исключения в блоке try пропускается — нельзя считать, что более поздние строки в try выполнились
- Переменные могут быть не инициализированы, если исключение происходит до их присваивания
- Нужно планировать, в каком состоянии находится ваша программа, когда выполняется блок except
25.1.2) Безопасная обработка пользовательского ввода
Одно из самых распространённых применений обработки исключений — проверка пользовательского ввода. Пользователи могут ввести что угодно, и нам нужно корректно обрабатывать некорректный ввод. Вот практический пример программы, которая спрашивает возраст пользователя:
# Безопасный ввод возраста с обработкой исключений
print("Please enter your age:")
user_input = input()
try:
age = int(user_input)
print(f"You are {age} years old.")
# Вычисляем год рождения (предполагая, что текущий год — 2024)
birth_year = 2024 - age
print(f"You were born around {birth_year}.")
except:
print("Invalid input! Age must be a number.")
print("Using default age of 0.")
age = 0Если пользователь вводит "25", вывод будет:
Please enter your age:
25
You are 25 years old.
You were born around 1999.Если пользователь вводит "twenty-five", вывод будет:
Please enter your age:
twenty-five
Invalid input! Age must be a number.
Using default age of 0.Обратите внимание, как программа аккуратно обрабатывает ошибку вместо того, чтобы падать с traceback. Это гораздо лучше для пользовательского опыта.
25.1.3) Обработка нескольких операций в блоке try
Вы можете поместить несколько операций в один блок try. Если любая из них вызывает исключение, Python немедленно переходит к блоку except. Начнём с простого примера:
# Две операции в блоке try
print("Enter a number:")
user_input = input()
try:
number = int(user_input) # Первая операция — может вызвать ValueError
result = 100 / number # Вторая операция — может вызвать ZeroDivisionError
print(f"100 / {number} = {result}")
except:
print("Something went wrong!")Если пользователь введёт "hello", исключение произойдёт на первой операции (преобразование). Если пользователь введёт "0", исключение произойдёт на второй операции (деление). В любом случае наш единственный блок except перехватит это.
Теперь расширим это до трёх операций:
# Несколько операций в блоке try
print("Enter two numbers to divide:")
numerator_input = input("Numerator: ")
denominator_input = input("Denominator: ")
try:
numerator = int(numerator_input) # Может вызвать ValueError
denominator = int(denominator_input) # Может вызвать ValueError
result = numerator / denominator # Может вызвать ZeroDivisionError
print(f"{numerator} / {denominator} = {result}")
except:
print("Something went wrong with the calculation!")
print("Make sure you enter valid numbers and don't divide by zero.")Если пользователь вводит "10" и "2":
Enter two numbers to divide:
Numerator: 10
Denominator: 2
10 / 2 = 5.0Если пользователь вводит "10" и "zero":
Enter two numbers to divide:
Numerator: 10
Denominator: zero
Something went wrong with the calculation!
Make sure you enter valid numbers and don't divide by zero.Если пользователь вводит "10" и "0":
Enter two numbers to divide:
Numerator: 10
Denominator: 0
Something went wrong with the calculation!
Make sure you enter valid numbers and don't divide by zero.В этом примере три разных вещи могут пойти не так: может не получиться преобразовать числитель, может не получиться преобразовать знаменатель или может не получиться выполнить деление (если знаменатель равен нулю). Наш единственный блок except обрабатывает все эти случаи. Однако у этого подхода есть ограничение: мы не можем понять, какая именно ошибка произошла. Мы разберём это в следующем разделе.
25.1.4) Проблема «голых» конструкций except
Использование except: без указания типа исключения называется bare except clause. Хотя он перехватывает все исключения, часто это слишком широко и может скрывать неожиданные проблемы. Рассмотрим такой пример:
# Bare except ловит ВСЁ — даже то, чего мы не ожидаем
numbers = [10, 20, 30]
try:
index = 5 # Мы ожидаем IndexError, если index выходит за пределы диапазона
value = numbers[index]
print(f"Value at index {index}: {value}")
except:
print("Could not access the list element.")Это выглядит разумно — мы пытаемся получить элемент списка, которого может не существовать. Но что, если в нашем коде есть опечатка?
# Что если в нашем коде есть опечатка?
numbers = [10, 20, 30]
try:
index = 2
value = numbrs[index] # Опечатка: 'numbrs' вместо 'numbers'
print(f"Value at index {index}: {value}")
except:
print("Could not access the list element.")Output:
Could not access the list element.«Голый» except перехватывает NameError из-за опечатки и печатает "Could not access the list element" — давая нам неправильную информацию о том, что пошло не так! Мы думаем, что индекс выходит за пределы диапазона, но на самом деле у нас опечатка в имени переменной.
Кроме того, bare except перехватывает KeyboardInterrupt (когда пользователь нажимает Ctrl+C) и SystemExit (когда программа завершается с помощью sys.exit() или аналогичного вызова), что обычно перехватывать не следует. По этим причинам лучше перехватывать конкретные исключения, что мы изучим далее.
25.2) Перехват конкретных исключений
25.2.1) Указание типов исключений
Вместо перехвата всех исключений с помощью bare except, мы можем указать, какие типы исключений хотим обрабатывать. Это делает наш код более точным и помогает нам корректно реагировать на разные ошибки:
# Перехват конкретного типа исключения
user_input = "hello"
try:
number = int(user_input)
print(f"You entered: {number}")
except ValueError:
print("That's not a valid number!")
number = 0
print(f"Using number: {number}")Output:
That's not a valid number!
Using number: 0Теперь наша ветка except перехватывает только исключения ValueError. Если возникнет исключение другого типа (например, NameError из-за опечатки), оно не будет перехвачено, и мы увидим полный traceback — что на самом деле полезно для отладки!
Синтаксис такой: except ExceptionType: где ExceptionType — это имя класса исключения, которое вы хотите перехватывать (например, ValueError, TypeError, ZeroDivisionError и т. д.).
Распространённая ошибка: перехват неправильного типа исключения
Что произойдёт, если вы укажете тип исключения, который не соответствует тому, что реально происходит? Давайте посмотрим:
# Перехват неправильного типа исключения
user_input = "hello"
try:
number = int(user_input) # Это вызывает ValueError
print(f"You entered: {number}")
except TypeError: # Но мы перехватываем TypeError!
print("That's not a valid number!")
number = 0
print(f"Using number: {number}")Output:
Traceback (most recent call last):
File "example.py", line 4, in <module>
number = int(user_input)
ValueError: invalid literal for int() with base 10: 'hello'Программа упала! Почему? Потому что int("hello") вызывает ValueError, а наш except перехватывает только TypeError. Поскольку нет подходящей ветки except, исключение не перехватывается, и программа завершается.
Это на самом деле полезно во время разработки: если вы перехватываете неправильный тип исключения, вы увидите полный traceback и поймёте свою ошибку. Это одна из причин, почему перехватывать конкретные исключения лучше, чем использовать bare except.
Как избежать этой ошибки:
- Прочитайте traceback, чтобы увидеть, какой тип исключения на самом деле произошёл
- Используйте этот конкретный тип исключения в вашей ветке except
- Если вы не уверены, запустите код и дайте ему упасть — traceback вам подскажет!
25.2.2) Разная обработка разных исключений
Вы можете иметь несколько веток except, чтобы по-разному обрабатывать разные типы исключений. Это крайне полезно, когда разные ошибки требуют разных реакций:
# Разная обработка для разных исключений
print("Enter two numbers to divide:")
numerator_input = input("Numerator: ")
denominator_input = input("Denominator: ")
try:
numerator = int(numerator_input)
denominator = int(denominator_input)
result = numerator / denominator
print(f"{numerator} / {denominator} = {result}")
except ValueError:
print("Error: Both inputs must be valid integers.")
print("You entered something that isn't a number.")
except ZeroDivisionError:
print("Error: Cannot divide by zero.")
print("The denominator must be a non-zero number.")Если пользователь вводит "10" и "abc":
Enter two numbers to divide:
Numerator: 10
Denominator: abc
Error: Both inputs must be valid integers.
You entered something that isn't a number.Если пользователь вводит "10" и "0":
Enter two numbers to divide:
Numerator: 10
Denominator: 0
Error: Cannot divide by zero.
The denominator must be a non-zero number.Python проверяет каждую ветку except по порядку. Когда возникает исключение, Python находит первую ветку except, которая соответствует типу исключения, и выполняет этот блок. Остальные ветки except пропускаются.
25.2.3) Перехват нескольких типов исключений в одной ветке
Иногда вы хотите обрабатывать несколько разных типов исключений одинаково. Вместо написания нескольких одинаковых блоков except вы можете перехватить несколько типов исключений в одной ветке, указав их в скобках как кортеж:
# Совместный перехват нескольких типов исключений
print("Enter a number:")
user_input = input()
try:
number = int(user_input)
result = 100 / number
print(f"100 divided by {number} is {result}")
except (ValueError, ZeroDivisionError):
print("Invalid input or division by zero.")
print("Please enter a non-zero number.")Если пользователь вводит "hello":
Enter a number:
hello
Invalid input or division by zero.
Please enter a non-zero number.Если пользователь вводит "0":
Enter a number:
0
Invalid input or division by zero.
Please enter a non-zero number.И ValueError (из-за некорректного преобразования), и ZeroDivisionError (из-за деления на ноль) обрабатываются одной и той же веткой except. Это полезно, когда разные ошибки должны приводить к одной и той же реакции.
25.2.4) Доступ к информации об исключении
Иногда вам нужно знать больше деталей об исключении, которое произошло. Вы можете сохранить объект исключения с помощью ключевого слова as. Но сначала давайте разберёмся, что такое объект исключения.
Что такое объект исключения?
Когда Python возбуждает исключение, он не просто сигнализирует, что что-то пошло не так, — он создаёт объект, который содержит информацию об ошибке. Этот объект исключения похож на подробный отчёт об ошибке, который включает:
- Сообщение об ошибке: описание того, что пошло не так
- Тип исключения: какой именно вид ошибки произошёл (ValueError, TypeError и т. д.)
- Дополнительные атрибуты: конкретная информация в зависимости от типа исключения
Думайте об объекте исключения как о контейнере, который хранит всю информацию об ошибке. Так же как объект списка содержит элементы и имеет методы вроде append(), объект исключения содержит информацию об ошибке и имеет атрибуты, к которым вы можете обращаться.
Когда вы пишете except ValueError as error:, вы говорите Python: «Если произойдёт ValueError, создай переменную с именем error и помести в неё объект исключения, чтобы я мог его изучить».
Давайте посмотрим, что находится внутри объекта исключения:
# Изучаем содержимое объекта исключения
try:
number = int("hello")
except ValueError as error:
print("Exception caught! Let's examine it:")
print(f"Type: {type(error)}")
print(f"String representation: {error}")
print(f"Args tuple: {error.args}")Output:
Exception caught! Let's examine it:
Type: <class 'ValueError'>
String representation: invalid literal for int() with base 10: 'hello'
Args tuple: ("invalid literal for int() with base 10: 'hello'",)У объекта исключения есть:
- Тип (класс ValueError) — это говорит вам, какой вид ошибки произошёл
- Строковое представление (сообщение об ошибке) — это то, что вы видите в traceback
- Атрибут args (кортеж, содержащий сообщение и любые другие аргументы) — это даёт структурированный доступ к деталям ошибки
Почему это важно:
У разных типов исключений разные атрибуты, которые дают специфическую информацию. Понимание структуры объектов исключений помогает извлекать полезную информацию для отладки или обратной связи пользователю:
# У разных исключений разные атрибуты
numbers = [10, 20, 30]
try:
value = numbers[10]
except IndexError as error:
print(f"IndexError message: {error}")
print(f"Exception args: {error.args}")
# Теперь попробуем со словарём
grades = {"Alice": 95}
try:
grade = grades["Bob"]
except KeyError as error:
print(f"KeyError message: {error}")
print(f"Missing key: {error.args[0]}")Output:
IndexError message: list index out of range
Exception args: ('list index out of range',)
KeyError message: 'Bob'
Missing key: BobОбратите внимание, что KeyError включает в своё сообщение фактический ключ, которого не хватало. Разные типы исключений предоставляют разную полезную информацию, к которой можно получить доступ через объект исключения.
25.3) Использование else и finally с блоками try
25.3.1) Ветка else: код, который выполняется только при успехе
Ветка else в конструкции try-except выполняется только если в блоке try не произошло исключения. Это полезно для кода, который должен выполняться только когда рискованная операция успешно завершилась:
# Используем else для кода, выполняющегося только при успехе
print("Enter a number:")
user_input = input()
try:
number = int(user_input)
except ValueError:
print("That's not a valid number!")
else:
# Это выполняется только если int(user_input) успешно отработал
print(f"Successfully converted: {number}")
squared = number ** 2
print(f"The square of {number} is {squared}")Если пользователь вводит "5":
Enter a number:
5
Successfully converted: 5
The square of 5 is 25Если пользователь вводит "hello":
Enter a number:
hello
That's not a valid number!Зачем использовать else, вместо того чтобы просто поместить этот код в конец блока try? Есть две важные причины:
- Ясность: ветка
elseявно показывает, что этот код выполняется только при успехе - Область перехвата исключений: исключения, возникшие в ветке
else, не перехватываются предшествующими веткамиexcept
Вот пример, показывающий, почему важен второй пункт:
# Демонстрируем, почему else полезен для области перехвата исключений
try:
number_1 = int(input("Enter a number_1: "))
except ValueError:
print("Invalid input!")
else:
# Если ошибка произойдёт здесь, она не будет перехвачена except выше
# Это помогает различать ошибки ввода и ошибки обработки
number_2 = int(input("Enter a number_2: ")) # Could raise ValueErrorЕсли бы мы поместили number_2 = int(input(...)) в блок try вместе с number_1, любой ValueError от любого из двух вводов был бы перехвачен одной и той же веткой except ValueError. Это сделало бы невозможным понять, какой ввод вызвал проблему.
Поместив number_2 = int(input(...)) в блок else, мы разделяем обработку ошибок. Ветка except перехватывает ошибки только от number_1, а ошибки от number_2 приведут к неперехваченному исключению с полным traceback — что ясно показывает, что не удался второй ввод, а не первый.
25.3.2) Ветка finally: код, который выполняется всегда
Ветка finally содержит код, который выполняется в любом случае — было исключение или нет, было оно перехвачено или нет. Это необходимо для операций очистки, которые должны происходить всегда:
# Используем finally для очистки
print("Enter a number:")
user_input = input()
try:
number = int(user_input)
result = 100 / number
print(f"Result: {result}")
except ValueError:
print("Invalid number!")
except ZeroDivisionError:
print("Cannot divide by zero!")
finally:
print("Calculation attempt completed.")Если пользователь вводит "5":
Enter a number:
5
Result: 20.0
Calculation attempt completed.Если пользователь вводит "hello":
Enter a number:
hello
Invalid number!
Calculation attempt completed.Если пользователь вводит "0":
Enter a number:
0
Cannot divide by zero!
Calculation attempt completed.Блок finally выполняется во всех трёх случаях! Это ключевое поведение finally: он выполняется всегда, независимо от того, что произошло в блоке try.
25.3.3) Комбинирование try, except, else и finally
Вы можете использовать все четыре ветки вместе, чтобы создать комплексную обработку исключений:
# Полная структура обработки исключений
print("Enter a number to calculate its reciprocal:")
user_input = input()
try:
# Рискованные операции
number = int(user_input)
reciprocal = 1 / number
except ValueError:
# Обрабатываем ошибки преобразования
print("Error: Input must be a valid integer.")
except ZeroDivisionError:
# Обрабатываем деление на ноль
print("Error: Cannot calculate reciprocal of zero.")
else:
# Код только для случая успеха
print(f"The reciprocal of {number} is {reciprocal}")
print(f"Verification: {number} × {reciprocal} = {number * reciprocal}")
finally:
# Код очистки, который выполняется всегда
print("Reciprocal calculation completed.")Если пользователь вводит "4":
Enter a number to calculate its reciprocal:
4
The reciprocal of 4 is 0.25
Verification: 4 × 0.25 = 1.0
Reciprocal calculation completed.Если пользователь вводит "hello":
Enter a number to calculate its reciprocal:
hello
Error: Input must be a valid integer.
Reciprocal calculation completed.Если пользователь вводит "0":
Enter a number to calculate its reciprocal:
0
Error: Cannot calculate reciprocal of zero.
Reciprocal calculation completed.Поток выполнения такой:
- Блок
tryвсегда выполняется первым - Если происходит исключение, выполняется соответствующий блок
except - Если исключения нет, выполняется блок
else(если он есть) - Блок
finallyвсегда выполняется последним, независимо от того, что произошло
25.4) Намеренное возбуждение исключений с помощью raise
25.4.1) Зачем возбуждать исключения?
До сих пор мы перехватывали исключения, которые Python возбуждает автоматически. Но иногда вам нужно возбуждать исключение намеренно в собственном коде. Это полезно, когда:
- Вы обнаружили некорректную ситуацию, с которой ваш код не может справиться
- Вы хотите обеспечить соблюдение правил или ограничений
- Вы хотите сообщить об ошибке вызывающему коду
Возбуждение исключения — это способ Python сказать: «Я не могу продолжать — что-то не так, и тот, кто меня вызвал, должен с этим разобраться».
Синтаксис простой: raise ExceptionType("error message")
Вот базовый пример:
# Намеренно возбуждаем исключение
age = -5
if age < 0:
raise ValueError("Age cannot be negative!")
print(f"Age: {age}") # Эта строка никогда не выполняетсяOutput:
Traceback (most recent call last):
File "example.py", line 5, in <module>
raise ValueError("Age cannot be negative!")
ValueError: Age cannot be negative!Когда Python встречает raise, он немедленно создаёт исключение и начинает искать блок except, который сможет его обработать. Если такого блока нет, программа завершается с traceback.
25.4.2) Возбуждение исключений в функциях
Возбуждение исключений особенно полезно в функциях для проверки входных данных и обеспечения ограничений:
# Функция, которая проверяет ввод, возбуждая исключения
def calculate_discount(price, discount_percent):
"""Calculate discounted price.
Args:
price: Original price (must be positive)
discount_percent: Discount percentage (must be 0-100)
Returns:
Discounted price
Raises:
ValueError: If inputs are invalid
"""
if price < 0:
raise ValueError("Price cannot be negative!")
if discount_percent < 0 or discount_percent > 100:
raise ValueError("Discount must be between 0 and 100!")
discount_amount = price * (discount_percent / 100)
return price - discount_amount
# Используем функцию
try:
final_price = calculate_discount(100, 20)
print(f"Final price: ${final_price}")
except ValueError as error:
print(f"Error: {error}")Output:
Final price: $80.0Теперь попробуем с некорректными входными данными:
# Некорректная цена
try:
final_price = calculate_discount(-50, 20)
print(f"Final price: ${final_price}")
except ValueError as error:
print(f"Error: {error}")Output:
Error: Price cannot be negative!# Некорректная скидка
try:
final_price = calculate_discount(100, 150)
print(f"Final price: ${final_price}")
except ValueError as error:
print(f"Error: {error}")Output:
Error: Discount must be between 0 and 100!Возбуждая исключения, функция ясно сообщает, что пошло не так. Затем вызывающий код может решить, как обработать ошибку — например, попросить пользователя ввести данные заново, использовать значения по умолчанию или записать ошибку в лог.
25.4.3) Выбор правильного типа исключения
В Python есть много встроенных типов исключений, и выбор правильного делает ваш код более понятным. Вот наиболее часто используемые исключения для валидации:
- ValueError: используйте, когда значение имеет правильный тип, но неподходящее значение (например, отрицательный возраст, неверный процент)
- TypeError: используйте, когда значение имеет совершенно неправильный тип (например, строка вместо числа)
- KeyError: используйте, когда ключа в словаре нет
- IndexError: используйте, когда индекс последовательности выходит за пределы диапазона
Вот пример, показывающий разные типы исключений:
# Используем подходящие типы исключений
def get_student_grade(grades, student_name):
"""Get a student's grade from the grades dictionary.
Args:
grades: Dictionary mapping student names to grades
student_name: Name of the student
Returns:
The student's grade
Raises:
TypeError: If grades is not a dictionary
KeyError: If student_name is not in grades
ValueError: If the grade is invalid
"""
if not isinstance(grades, dict):
raise TypeError("Grades must be a dictionary!")
if student_name not in grades:
raise KeyError(f"Student '{student_name}' not found!")
grade = grades[student_name]
if not (0 <= grade <= 100):
raise ValueError(f"Invalid grade: {grade} (must be 0-100)")
return grade
# Тестируем с корректными данными
grades = {"Alice": 95, "Bob": 87, "Carol": 92}
try:
grade = get_student_grade(grades, "Alice")
print(f"Alice's grade: {grade}")
except (TypeError, KeyError, ValueError) as error:
print(f"Error: {error}")Output:
Alice's grade: 95# Тестируем с отсутствующим студентом
try:
grade = get_student_grade(grades, "David")
print(f"David's grade: {grade}")
except (TypeError, KeyError, ValueError) as error:
print(f"Error: {error}")Output:
Error: Student 'David' not found!# Тестируем с неправильным типом
try:
grade = get_student_grade("not a dict", "Alice")
print(f"Alice's grade: {grade}")
except (TypeError, KeyError, ValueError) as error:
print(f"Error: {error}")Output:
Error: Grades must be a dictionary!Использование подходящего типа исключения помогает другим программистам (и вам в будущем) понимать, какой тип ошибки произошёл.
25.4.4) Повторное возбуждение исключений
Иногда вы хотите перехватить исключение, сделать что-то (например, логирование), а затем позволить исключению продолжить распространяться дальше. Это можно сделать, используя raise без аргументов внутри блока except:
# Повторно возбуждаем исключение после логирования
def divide_numbers(a, b):
"""Divide two numbers with error logging."""
try:
result = a / b
return result
except ZeroDivisionError:
print("ERROR LOG: Division by zero attempted")
print(f" Numerator: {a}, Denominator: {b}")
raise # Повторно возбуждаем то же исключение
# Используем функцию
try:
result = divide_numbers(10, 0)
print(f"Result: {result}")
except ZeroDivisionError:
print("Cannot divide by zero!")Output:
ERROR LOG: Division by zero attempted
Numerator: 10, Denominator: 0
Cannot divide by zero!Оператор raise без аргументов повторно возбуждает исключение, которое только что было перехвачено. Это полезно, когда вы хотите:
- Записать или зафиксировать ошибку
- Выполнить некоторую очистку
- Дать ошибке подняться к вызывающему коду
25.4.5) Возбуждение исключений на основе других исключений
Иногда вы хотите возбудить новое исключение при обработке другого, сохранив контекст исходной ошибки. Python 3 предоставляет для этого синтаксис raise ... from ...:
# Возбуждаем новое исключение на основе существующего
def load_config(config_dict, key):
"""Load configuration value from dictionary."""
try:
config_value = config_dict[key]
# Пытаемся распарсить как целое число
parsed_value = int(config_value)
return parsed_value
except KeyError as error:
raise RuntimeError(f"Configuration key missing: {key}") from error
except ValueError as error:
raise RuntimeError(f"Invalid configuration format for {key}") from error
# Используем функцию
config = {"timeout": "30", "retries": "5"}
try:
value = load_config(config, "timeout")
print(f"Config value: {value}")
except RuntimeError as error:
print(f"Configuration error: {error}")
print(f"Original cause: {error.__cause__}")Output:
Config value: 30Если ключ не существует:
try:
value = load_config(config, "missing_key")
print(f"Config value: {value}")
except RuntimeError as error:
print(f"Configuration error: {error}")
print(f"Original cause: {error.__cause__}")Output:
Configuration error: Configuration key missing: missing_key
Original cause: 'missing_key'Ключевое слово from связывает новое исключение с исходным. Это создаёт цепочку исключений, которая помогает при отладке — вы можете увидеть и то, что произошло на высоком уровне (ошибка конфигурации), и то, какая была первопричина (ключ не найден).
Обработка исключений — один из самых важных инструментов для написания надёжных программ. Используя блоки try-except, вы можете предвидеть проблемы, аккуратно их обрабатывать и обеспечивать лучший опыт для ваших пользователей. Помните:
- Используйте
try-exceptдля аккуратной обработки ожидаемых ошибок - Перехватывайте конкретные типы исключений, а не используйте bare
except - Используйте
elseдля кода, который должен выполняться только при успехе - Используйте
finallyдля кода очистки, который должен выполняться всегда - Возбуждайте исключения в собственном коде, чтобы сигнализировать о проблемах
- Выбирайте подходящие типы исключений, чтобы ошибки были понятными
- Давайте полезные сообщения об ошибках, объясняющие, что пошло не так
В следующей главе мы изучим техники защитного программирования, которые объединяют обработку исключений с проверкой ввода и другими стратегиями, чтобы сделать наши программы ещё более надёжными.