Уроки технологии
Технология Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

Обработка ошибок

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

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

В этом модуле вы научитесь:

Читать и использовать сообщения об ошибках из исключений.
Правильно обрабатывать исключения.
Вызывать исключения с информативными сообщениями об ошибках.
Использовать исключения для управления потоком программы.

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

Сценарий: создание программы для космического корабля

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

Поиск ошибок с помощью обратной трассировки

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

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

Обратная трассировка — это текст, который может указывать на возникновение (и окончание) необработанной ошибки. Понимание компонентов обратной трассировки позволяет эффективнее устранять ошибки или отлаживать программу, в которой возникают проблемы.

При первом возникновении исключений в Python может возникнуть искушение просто подавить ошибку. Если в программе возникает необработанная ошибка, возникает обратная трассировка. Как вы увидите в этом модуле, трассировки полезны. Существуют способы правильной обработки ошибок, чтобы они не появлялись или предоставляли полезную информацию.

Откройте интерактивный сеанс Python и попробуйте открыть несуществующий файл:

>>> open("/path/to/mars.jpg")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
FileNotFoundError: [Errno 2] No such file or directory: '/path/to/mars.jpg'

Эти выходные данные содержат несколько ключевых частей. Во первых, обратная трассировка указывает порядок выходных данных. Затем она сообщает, что файл — stdin (входные данные в интерактивном терминале) в первой строке входных данных. Ошибка — FileNotFoundError (имя исключения). Это означает, что файл не существует или каталог не существует.

Это большой объем информации. Может быть сложно понять, почему строка 1 имеет смысл или что означает Errno 2.

Создайте файл Python с именем open.py и следующим содержимым:

def main():
    open("/path/to/mars.jpg")

if __name__ == '__main__':
    main()

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

$ python3 open.py
Traceback (most recent call last):
  File "/tmp/open.py", line 5, in <module>
    main()
  File "/tmp/open.py", line 2, in main
    open("/path/to/mars.jpg")
FileNotFoundError: [Errno 2] No such file or directory: '/path/to/mars.jpg'

Теперь выходные данные ошибки имеют больше смысла. Пути указывают на один файл с именем open.py. В выходных данных говорится, что ошибка начинается в строке 5, которая включает вызов к main(). Далее в выходных данных следует ошибка в строке 2 в вызове функции open(). И, наконец, FileNotFoundError снова сообщает о том, что файл или каталог не существует.

Обратная трассировка почти всегда содержит следующие сведения:

Все используемые пути к файлам, для каждого вызова каждой функции.
Номера строк, связанные с каждым путем к файлу.
Имена функций, методов или классов, участвующих в создании исключения.
Имя вызванного исключения.

Обработка исключений

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

Что делать, если во время полета к Марсу навигационная система сообщает: “Произошла ошибка”? Представьте, что никакой другой информации или контекста нет, просто мигающий красный огонек с текстом ошибки. Разработчикам полезно представлять себя в роли пользователей. Что может сделать пользователь, если возникнет ошибка?

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

Блоки try и except

Давайте рассмотрим пример навигатора, чтобы создать код, который открывает файлы конфигурации для миссии на Марс. В файлах конфигурации могут возникать самые разные проблемы, поэтому очень важно точно сообщать о проблемах при их появлении. Мы знаем, что если файл или каталог не существует, возникает исключение FileNotFoundError. Если мы хотим обработать это исключение, можно использовать блок try и except:

>>> try:
...     open('config.txt')
... except FileNotFoundError:
...     print("Couldn't find the config.txt file!")
...
Couldn't find the config.txt file!

После ключевого слова try вы добавляете код, который может вызвать исключение. Затем добавьте ключевое слово except вместе с возможным исключением, а за ним — любой код, который должен выполняться при наступлении этого условия. Поскольку config.txt не существует в системе, Python сообщает, что файл конфигурации отсутствует. Блок try и except вместе с информативным сообщением предотвращает обратную трассировку, но при этом все равно информирует пользователя о проблеме.

Отсутствие файла — распространенная ошибка, но далеко не единственная. Недопустимые разрешения файла могут препятствовать чтению файла, даже если он существует. Давайте создадим новый файл Python с именем config.py. Файл содержит код, который находит и считывает файл конфигурации системы навигации:

def main():
    try:
        configuration = open('config.txt')
    except FileNotFoundError:
        print("Couldn't find the config.txt file!")


if __name__ == '__main__':
    main()

Затем удалите файл config.txt и создайте каталог с именем config.txt. Попробуйте вызвать файл config.py, чтобы увидеть новую ошибку, аналогичную следующей:

$ python config.py
Traceback (most recent call last):
  File "/tmp/config.py", line 9, in <module>
    main()
  File "/tmp/config.py", line 3, in main
    configuration = open('config.txt')
IsADirectoryError: [Errno 21] Is a directory: 'config.txt'

Для этой ошибки было бы неэффективно перехватить все возможные исключения для предотвращения обратной трассировки. Чтобы понять, почему перехват всех исключений приведет к проблемам, попробуйте обновить функцию main():

def main():
    try:
        configuration = open('config.txt')
    except Exception:
        print("Couldn't find the config.txt file!")

Теперь снова запустите код в том же месте, где существует файл config.txt с неправильными разрешениями:

$ python config.py
Couldn't find the config.txt file!

Проблема в том, что сообщение об ошибке содержит неверную информацию. Файл существует, но имеет другие разрешения, и Python не может его прочитать. При работе с ошибками программного обеспечения очень неприятно получать ошибки, которые:

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

Попробуем исправить этот фрагмент кода, чтобы устранить все эти проблемы. Отмените перехват FileNotFoundError, а затем добавьте еще один блок except для перехвата PermissionError:

def main():
    try:
        configuration = open('config.txt')
    except FileNotFoundError:
        print("Couldn't find the config.txt file!")
    except IsADirectoryError:
        print("Found config.txt but it is a directory, couldn't read it")

Теперь запустите его снова в том же месте, где находится config.txt с неверными разрешениями:

$ python config.py
Found config.txt but couldn't read it

Теперь удалите файл config.txt, чтобы убедиться, что будет достигнут первый блок except:

$ python config.py
Couldn't find the config.txt file!

Если ошибки имеют схожую природу, и нет необходимости в их обработке по отдельности, можно сгруппировать исключения в одно с помощью круглых скобок в строке except. Например, если система навигации работает под высокой нагрузкой и файловая система обрабатывает слишком много задач, имеет смысл перехватывать BlockingIOError и TimeOutError вместе:

def main():
    try:
        configuration = open('config.txt')
    except FileNotFoundError:
        print("Couldn't find the config.txt file!")
    except IsADirectoryError:
        print("Found config.txt but it is a directory, couldn't read it")
    except (BlockingIOError, TimeoutError):
        print("Filesystem under heavy load, can't complete reading configuration file")

Совет

Хотя исключения можно сгруппировать, делайте это только тогда, когда нет необходимости обрабатывать их по отдельности. Старайтесь не группировать слишком много исключений, чтобы сообщение об ошибке не содержало слишком общую информацию.

Если необходимо получить доступ к ошибке, связанной с исключением, необходимо обновить строку except, включив ключевое слово as. Этот метод удобен, если исключение является слишком универсальным, и сообщение об ошибке может быть полезным:

>>> try:
...     open("mars.jpg")
... except FileNotFoundError as err:
...     print("got a problem trying to read the file:", err)
...
got a problem trying to read the file: [Errno 2] No such file or directory: 'mars.jpg'

В этом случае as err означает, что err становится переменной с объектом исключения в качестве значения. Затем это значение используется для вывода сообщения об ошибке, связанного с исключением. Еще одна причина использования этой методики — прямой доступ к атрибутам ошибки. Например, если вы перехватите более универсальное исключение OSError, которое является родительским исключением, или FilenotFoundError и PermissionError сразу, их можно различать с помощью атрибута .errno:

>>> try:
...     open("config.txt")
... except OSError as err:
...     if err.errno == 2:
...         print("Couldn't find the config.txt file!")
...     elif err.errno == 13:
...         print("Found config.txt but couldn't read it")
...
Couldn't find the config.txt file!

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

Упражнение: обработка исключений

Зная обратные трассировки и то, как возникают и обрабатываются исключения, пришло время попрактиковаться в некоторых из этих концепций с помощью этой записной книжки. Мы будем использовать аналогичный сценарий, когда программа должна считывать значения конфигурации. Иногда могут быть ошибки в настройке значений, которые ваша программа должна уметь обрабатывать.

Загрузка конфигурации

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

Начните с создания функции, которая открывает и читает содержимое файла конфигурации. Добавьте параметр в функцию с именем path для пути к файлу конфигурации.

loaded_config = """# Rocket Ship Configuration File!
fuel_tanks=4
oxygen_tanks=3
initial_propulsion_level=84
$ End of file"""

Разбор информации о конфигурации

Для этого сценария вы хотите загрузить любую информацию о ключе/значении. Ожидаемый формат: key=value. В Python вы можете использовать split для разделения текста на основе символа, например, split(’=’).

Если вы посмотрите на load_config, то заметите, что не все строки соответствуют этому формату. В результате нам нужно убедиться, что код изящно обрабатывает любые ошибки. Если символ, используемый для разделения, не найден, возникает ошибка ValueError.

В ячейку ниже добавьте код для разбора загруженного_config. Начните с создания пустого словаря, используя { } с именем parsed_config для хранения проанализированной информации. Используйте цикл for для разделения каждой строки с помощью split(’\n’). Затем используйте попытку разобрать строку, как указано выше (используя split(’=’)). Если синтаксический анализ прошел успешно, сохраните пару ключ/значение в файле load_config. Если возникает ValueError, отобразите сообщение Unable to parse: и строку с неправильным форматом. Закончите отображением parsed_config.

parsed_config = {}
for line in loaded_config.split('\n'):
    try:
        key, value = line.split('=')
        parsed_config[key] = value
    except ValueError:
        print(f'Unable to parse {line}')
print(parsed_config)

При запуске результат должен выглядеть следующим образом:

Unable to parse # Rocket Ship Configuration File!
Unable to parse $ End of file
{'fuel_tanks': '4', 'oxygen_tanks': '3', 'initial_propulsion_level': '84'}

Возникновение исключений

Завершено XP: 100

1 минута

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

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

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

Астронавты должны потреблять не более 11 литров воды в день. Давайте создадим функцию, которая, в зависимости от числа астронавтов, может вычислить, сколько воды станется через день или больше: Python

def water_left(astronauts, water_left, days_left):
    daily_usage = astronauts * 11
    total_usage = daily_usage * days_left
    total_water_left = water_left - total_usage
    return f"Total water left after {days_left} days is: {total_water_left} liters"

Допустим, у нас пять астронавтов, 100 литров воды и два дна до конца пути:

>>> water_left(5, 100, 2)
'Total water left after 2 days is: -10 liters'

Возникнет дефицит, которые приведет к ошибке. Система навигации может предупредить астронавтов о том, что у них не хватит воды на два дня. Если вы инженер, который программирует систему навигации, вы можете вызвать исключение в функции water_left(), чтобы предупредить о состоянии ошибки:

def water_left(astronauts, water_left, days_left):
    daily_usage = astronauts * 11
    total_usage = daily_usage * days_left
    total_water_left = water_left - total_usage
    if total_water_left < 0:
        raise RuntimeError(f"There is not enough water for {astronauts} astronauts after {days_left} days!")
    return f"Total water left after {days_left} days is: {total_water_left} liters"

Снова выполняем запуск:

>>> water_left(5, 100, 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in water_left
RuntimeError: There is not enough water for 5 astronauts after 2 days!

В системе навигации код для оповещения о предупреждении теперь может использовать RuntimeError для оповещения:


try:
    water_left(5, 100, 2)
except RuntimeError as err:
    alert_navigation_system(err)

Функцию water_left() также можно обновить, чтобы предотвратить передачу неподдерживаемых типов. Попробуйте передать аргументы, не являющиеся целыми числами, чтобы проверить вывод ошибок:


>>> water_left("3", "200", None)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in water_left
TypeError: can't multiply sequence by non-int of type 'NoneType'

Ошибка из TypeError не очень понятна в контексте того, что требует функция. Обновите функцию, чтобы она использовала TypeError, но с более информативным сообщением:


def water_left(astronauts, water_left, days_left):
    for argument in [astronauts, water_left, days_left]:
        try:
            # If argument is an int, the following operation will work
            argument / 10
        except TypeError:
            # TypError will be raised only if it isn't the right type 
            # Raise the same exception but with a better error message
            raise TypeError(f"All arguments must be of type int, but received: '{argument}'")
    daily_usage = astronauts * 11
    total_usage = daily_usage * days_left
    total_water_left = water_left - total_usage
    if total_water_left < 0:
        raise RuntimeError(f"There is not enough water for {astronauts} astronauts after {days_left} days!")
    return f"Total water left after {days_left} days is: {total_water_left} liters"

Теперь повторите попытку, чтобы получить более качественную ошибку:


>>> water_left("3", "200", None)
Traceback (most recent call last):
  File "<stdin>", line 5, in water_left
TypeError: unsupported operand type(s) for /: 'str' and 'int'

During handling of the preceding exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 9, in water_left
TypeError: All arguments must be of type int, but received: '3'

Упражнение: создание исключений

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

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

Для целей этого упражнения вы будете использовать приведенные ниже значения для true и false.

true_values = ['yes', 'y']
false_values = ['no', 'n']

Создайте функцию для проверки истинности или ложности

Вы будете использовать значения true_values и false_values для создания функции с именем str_to_bool для преобразования строк в логические значения. str_to_bool примет один параметр с именем value.

Создайте функцию str_to_bool. Преобразование значения в строчные буквы. Если значение соответствует записи в true_values, функция должна вернуть True. Если значение соответствует записи в false_values, оно должно вернуть False. Если он не соответствует ни одному из значений, он должен вызвать ValueError с сообщением Invalid entry.

ef str_to_bool(value):
    value = value.lower()
    if value in true_values:
        return True
    elif value in false_values:
        return False
    else:
        raise ValueError('Invalid entry')