В Python исключения служат способом реагирования на ошибки, которые могут возникнуть во время выполнения программы. В Python существует множество встроенных исключений, таких как IndexError, когда вы пытаетесь обратиться к несуществующему индексу в списке, или TypeError, когда вы пытаетесь выполнить операцию над типом, который её не поддерживает. Однако иногда бывает полезно определить собственные исключения. Это позволяет выдавать понятные сообщения об ошибках и реагировать на специфические ситуации, которые встроенные исключения не могут обработать. Пользовательские исключения создаются путем определения нового класса, который наследуется от встроенного класса Exception или одного из его подклассов.
Рассмотрим программу, которая работает с теоретической системой библиотеки. В этой системе пользователю не разрешается брать более 5 книг одновременно. Вот как мы можем создать для этого пользовательское исключение:
class TooManyBooksError(Exception): # Определяем новый класс исключения
pass
def checkout_books(user, num_books):
if num_books > 5:
# Выбрасываем исключение, если пользователь пытается взять более 5 книг
raise TooManyBooksError('You cannot check out more than 5 books at a time.') # Вызываем наше пользовательское исключение
# Иначе добавляем книги в аккаунт пользователя
user.books += num_books
В приведенном выше примере, если пользователь попытается взять более 5 книг одновременно, будет вызвано наше пользовательское исключение TooManyBooksError с определенным сообщением об ошибке. Если исключение не будет перехвачено, выполнение программы остановится, и сообщение об ошибке будет выведено в консоль.
Мы можем перехватить это исключение так же, как и встроенные, используя блок try / except:
try:
checkout_books(user, 7) # Пытаемся взять 7 книг
except TooManyBooksError as e: # Перехватываем наше пользовательское исключение
print(e) # Выводим сообщение об ошибке
Если строка checkout_books(user, 7) вызовет TooManyBooksError, программа перехватит его и выведет наше сообщение об ошибке 'You cannot check out more than 5 books at a time.'. Таким образом, мы можем аккуратно обработать ошибку, и программа сможет продолжить работу.
Так же, как и с любым другим классом, мы можем добавить методы в наш класс исключения. Например, давайте добавим метод, который вычисляет, сколько книг пользователю нужно вернуть, чтобы получить желаемое количество:
class TooManyBooksError(Exception):
def __init__(self, attempted, limit=5, checked_out=0):
self.attempted = attempted
self.limit = limit
self.checked_out = checked_out
self.message = f'You tried to check out {self.attempted} books, but the limit is {self.limit} books.'
super().__init__(self.message)
def books_to_return(self):
return self.checked_out + self.attempted - self.limit
Мы можем использовать этот метод после перехвата исключения:
try:
checkout_books(user, 7)
except TooManyBooksError as e:
print(e) # You tried to check out 7 books, but the limit is 5 books.
print(f'You need to return {e.books_to_return()} books.') # You need to return 2 books.
Таким образом, пользовательские исключения могут иметь свои собственные атрибуты и методы, что делает их мощным инструментом для управления ошибками в соответствии с конкретными потребностями вашей программы.
Лучшие практики при определении пользовательских исключений
При создании пользовательских исключений часто возникает соблазн определить пустой класс, наследуемый от Exception, и каждый раз при вызове исключения передавать полное сообщение об ошибке в виде строки. Хотя этот подход работает, он может привести к повторению кода и усложнить поддержку единообразия сообщений об ошибках.
Вместо этого лучше хранить любые динамические данные внутри класса исключения. Таким образом, вы можете формировать сообщение об ошибке внутри класса, делая ваш код чище и более организованным:
👎 Плохая практика – каждый раз передавать полное сообщение:
class TooManyBooksError(Exception):
pass
# Вы вынуждены каждый раз передавать сообщение
raise TooManyBooksError('You tried to check out 7 books, but the limit is 5!')
# В долгосрочной перспективе это плохая идея
👍 Лучший подход – передавать динамические данные в класс исключения:
class TooManyBooksError(Exception):
def __init__(self, attempted, limit=5):
self.attempted = attempted
self.limit = limit
# Создаем сообщение об ошибке из данных
self.message = f'You tried to check out {self.attempted} books, but the limit is {self.limit}.'
super().__init__(self.message)
# При вызове исключения просто передавайте соответствующие данные
raise TooManyBooksError(7)
💡
Давая классу исключения обрабатывать создание сообщения об ошибке, вы избегаете повторений. Если вам нужно изменить формулировку или формат сообщения, вам придется сделать это только в одном месте — в методе __init__ вашего исключения.
Задание: База данных возрастов
В городе, где все знают всех, местное правительство пытается внедрить систему проверки возраста в городских заведениях, таких как бары, клубы и театры. Им нужна программа, которая может, получив словарь с возрастами людей, принять имя и вернуть возраст этого человека.
Ввод обрабатывается автоматически. Вам нужно реализовать функцию find_age(name), которая по имени возвращает возраст человека.
Однако не все люди могут присутствовать в городской базе данных. Если программа не может найти имя в базе данных, она должна вызвать пользовательское исключение под названием NameNotFoundError и вывести соответствующее сообщение об ошибке: {name} not found in the database. Убедитесь, что создаете это сообщение внутри класса NameNotFoundError.
Input
Output
5
Alice - 24
Bob - 32
Charlie - 21
Daisy - 27
Edgar - 30
Edgar
Edgar - 30
5
Alice - 24
Bob - 32
Charlie - 21
Daisy - 27
Edgar - 30
George
NameNotFoundError: George not found in the database.
Примечание: Имена и возрасты чувствительны к регистру и могут содержать пробелы. Введенное имя должно точно совпадать с именем в базе данных.