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

Beginner

This tutorial is from open-source community. Access the source code

Введение

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

Это Guided Lab, который предоставляет пошаговые инструкции, чтобы помочь вам учиться и практиковаться. Внимательно следуйте инструкциям, чтобы выполнить каждый шаг и получить практический опыт. Исторические данные показывают, что это лабораторная работа уровня начальный с процентом завершения 85%. Он получил 100% положительных отзывов от учащихся.

Как программы завершаются сбоем

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

def add(x, y):
    return x + y

add(3, 4)               ## 7
add('Hello', 'World')   ## 'HelloWorld'
add('3', '4')           ## '34'

Если в функции есть ошибки, они появляются во время выполнения (в виде исключения).

def add(x, y):
    return x + y

>>> add(3, '4')
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for +:
'int' and 'str'
>>>

Для проверки кода акцентируется на тестировании (рассматривается позже).

Исключения

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

if name not in authorized:
    raise RuntimeError(f'{name} not authorized')

Чтобы перехватить исключение, используйте try-except.

try:
    authenticate(username)
except RuntimeError as e:
    print(e)

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

Исключения распространяются до первого совпадающего except.

def grok():
  ...
    raise RuntimeError('Whoa!')   ## Исключение возникает здесь

def spam():
    grok()                        ## Вызов, который вызовет исключение

def bar():
    try:
       spam()
    except RuntimeError as e:     ## Исключение поймано здесь
      ...

def foo():
    try:
         bar()
    except RuntimeError as e:     ## Исключение НЕ приходит сюда
      ...

foo()

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

def grok():...
    raise RuntimeError('Whoa!')

def bar():
    try:
      grok()
    except RuntimeError as e:   ## Исключение поймано здесь
        statements              ## Используйте эти инструкции
        statements
     ...

bar()

После обработки выполнение продолжается с первой инструкции после try-except.

def grok():...
    raise RuntimeError('Whoa!')

def bar():
    try:
      grok()
    except RuntimeError as e:   ## Исключение поймано здесь
        statements
        statements
     ...
    statements                  ## Продолжает выполнение здесь
    statements                  ## И продолжает здесь
 ...

bar()

Встроенные исключения

Есть около двух десятков встроенных исключений. Обычно название исключения говорит о том, что пошло не так (например, ValueError возникает, если вы подаете неправильное значение). Это не исчерпывающий список. Проверьте документацию для получения дополнительной информации.

ArithmeticError
AssertionError
EnvironmentError
EOFError
ImportError
IndexError
KeyboardInterrupt
KeyError
MemoryError
NameError
ReferenceError
RuntimeError
SyntaxError
SystemError
TypeError
ValueError

Значения исключений

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

raise RuntimeError('Invalid user name')

Это значение является частью экземпляра исключения, который помещается в переменную, переданную в except.

try:
 ...
except RuntimeError as e:   ## В `e` хранится выброшенное исключение
 ...

e является экземпляром типа исключения. Однако, при выводе оно часто выглядит как строка.

except RuntimeError as e:
    print('Failed : Reason', e)

Захват нескольких ошибок

Вы можете ловить разные виды исключений с использованием нескольких блоков except.

try:
...
except LookupError as e:
...
except RuntimeError as e:
...
except IOError as e:
...
except KeyboardInterrupt as e:
...

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

try:
...
except (IOError,LookupError,RuntimeError) as e:
...

Захват всех ошибок

Для захвата любого исключения используйте Exception следующим образом:

try:
  ...
except Exception:       ## Опасно. См. ниже
    print('An error occurred')

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

Неправильный способ ловить ошибки

Вот неправильный способ использовать исключения.

try:
    go_do_something()
except Exception:
    print('Computer says no')

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

Немного более разумный подход

Если вы собираетесь ловить все ошибки, это более разумный подход.

try:
    go_do_something()
except Exception as e:
    print('Computer says no. Reason :', e)

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

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

Переброс исключения

Используйте raise, чтобы распространить пойманную ошибку.

try:
    go_do_something()
except Exception as e:
    print('Computer says no. Reason :', e)
    raise

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

Лучшие практики при работе с исключениями

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

Блок finally

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

lock = Lock()
...
lock.acquire()
try:
  ...
finally:
    lock.release()  ## это всегда будет выполняться. В случае с исключением и без.

Обще используется для безопасного управления ресурсами (особенно блокировками, файлами и т.д.).

Блок with

В современном коде конструкция try-finally часто заменяется на блок with.

lock = Lock()
with lock:
    ## lock acquired
  ...
## lock released

более знакомый пример:

with open(filename) as f:
    ## Use the file
  ...
## File closed

with определяет контекст использования ресурса. При выходе из этого контекста ресурсы освобождаются. with работает только с определенными объектами, которые были специально разработаны для его поддержки.

Упражнение 3.8: Генерация исключений

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

Измените код так, чтобы генерировалось исключение, если передаются оба аргумента select и has_headers=False. Например:

>>> parse_csv('/home/labex/project/prices.csv', select=['name','price'], has_headers=False)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "fileparse.py", line 9, in parse_csv
    raise RuntimeError("select argument requires column headers")
RuntimeError: select argument requires column headers
>>>

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

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

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

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

Упражнение 3.9: Захват исключений

Функция parse_csv(), которую вы написали, используется для обработки всего содержимого файла. Однако, в реальности возможны поврежденные, отсутствующие или "грязные" данные в входных файлах. Попробуйте этот эксперимент:

>>> portfolio = parse_csv('missing.csv', types=[str, int, float])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "fileparse.py", line 36, in parse_csv
    row = [func(val) for func, val in zip(types, row)]
ValueError: invalid literal for int() with base 10: ''
>>>

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

Сообщение должно включать номер строки и информацию о причине неудачи. Чтобы протестировать функцию, попробуйте прочитать файл missing.csv выше. Например:

>>> portfolio = parse_csv('missing.csv', types=[str, int, float])
Строка 4: Не удалось преобразовать ['MSFT', '', '51.23']
Строка 4: Причина: invalid literal for int() with base 10: ''
Строка 7: Не удалось преобразовать ['IBM', '', '70.44']
Строка 7: Причина: invalid literal for int() with base 10: ''
>>>
>>> portfolio
[{'name': 'AA','shares': 100, 'price': 32.2}, {'name': 'IBM','shares': 50, 'price': 91.1}, {'name': 'CAT','shares': 150, 'price': 83.44}, {'name': 'GE','shares': 95, 'price': 40.37}, {'name': 'MSFT','shares': 50, 'price': 65.1}]
>>>

Упражнение 3.10: Отключение сообщений об ошибках

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

>>> portfolio = parse_csv('missing.csv', types=[str,int,float], silence_errors=True)
>>> portfolio
[{'name': 'AA','shares': 100, 'price': 32.2}, {'name': 'IBM','shares': 50, 'price': 91.1}, {'name': 'CAT','shares': 150, 'price': 83.44}, {'name': 'GE','shares': 95, 'price': 40.37}, {'name': 'MSFT','shares': 50, 'price': 65.1}]
>>>

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

Резюме

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