Part 6

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

В программировании есть две базовые категории ошибок:

  1. Синтаксические ошибки, которые не дают программе выполниться
  2. Ошибки времени выполнения, которые останавливают выполнение

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

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

Проверка ввода

Многие ошибки, возникающие во время выполнения программы, связаны с недопустимым вводом. Некоторые примеры:

  • отсутствующие или пустые значения ввода в обязательных полях, например пустые строки, когда длина строки критична
  • отрицательные значения там, где принимаются только положительные, например -15 как количество ингредиента в рецепте
  • отсутствующие файлы или опечатки в именах файлов
  • слишком маленькие или слишком большие значения, например при работе с датами и временем
  • недопустимые индексы, например попытка обратиться к индексу 3 в строке "hey"
  • значения неправильного типа, например строки, когда ожидаются целые числа

К счастью, мы как программисты можем подготовиться к большинству ошибок. Рассмотрим программу, которая спрашивает у пользователя возраст и убеждается, что это допустимое число (в данном случае между 0 и 150):

age = int(input("Please type in your age: "))
if age >= 0 and age <= 150:
    print("That is a fine age")
else:
    print("This is not a valid age")
Пример вывода

Please type in your age: 25 That is a fine age

Пример вывода

Please type in your age: -3 This is not a valid age

Пока пользователь вводит целое число, наша проверка ввода, кажется, работает хорошо. Но что если он введет строку?

Пример вывода

Please type in your age: twenty-three ValueError: invalid literal for int() with base 10: 'twenty-three'

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

Исключения

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

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

Изменим пример выше так, чтобы программа была готова к исключению ValueError:

try:
    age = int(input("Please type in your age: "))
except ValueError:
    age = -1

if age >= 0 and age <= 150:
    print("That is a fine age")
else:
    print("This is not a valid age")
Пример вывода

Please type in your age: twenty-three This is not a valid age

Мы можем использовать блок try, чтобы отметить, что код внутри блока может вызвать ошибку. В операторе except сразу после блока указывается соответствующая ошибка. В примере выше мы обработали только исключение ValueError. Если бы у исключения была другая причина, выполнение все равно остановилось бы, несмотря на блоки try и except.

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

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

def read_integer():
    while True:
        try:
            input_str = input("Please type in an integer: ")
            return int(input_str)
        except ValueError:
            print("This input is invalid")

number = read_integer()
print("Thank you!")
print(number, "to the power of three is", number**3)
Пример вывода

Please type in an integer: three This input is invalid Please type in an integer: aybabtu This input is invalid Please type in an integer: 5 Thank you! 5 to the power of three is 125

Иногда достаточно поймать исключения с помощью структуры try-except, ничего с ними не делая. То есть в блоке except можно просто игнорировать ситуацию.

Если бы мы изменили пример выше так, чтобы принимать только целые числа меньше 100, результаты могли бы выглядеть так:

def read_small_integer():
    while True:
        try:
            input_str = input("Please type in an integer: ")
            number = int(input_str)
            if number < 100:
                return number
        except ValueError:
            pass # this command doesn't actually do anything

        print("This input is invalid")

number = read_small_integer()
print(number, "to the power of three is", number**3)
Пример вывода

Please type in an integer: three This input is invalid Please type in an integer: 1000 This input is invalid Please type in an integer: 5 Thank you! 5 to the power of three is 125

Теперь блок except содержит только команду pass, которая ничего не делает. Python не допускает пустые блоки, поэтому эта команда необходима.

Loading

Типичные ошибки

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

ValueError

Эта ошибка часто возникает, когда аргумент, переданный функции, каким-то образом недопустим. Например, вызов функции float("1,23") вызывает ошибку, потому что в Python дробная часть всегда отделяется точкой, а здесь используется запятая.

TypeError

Эта ошибка возникает, когда значение имеет неправильный тип. Например, вызов функции len(10) вызывает TypeError, потому что функция len требует значение, длину которого можно вычислить, например строку или список.

IndexError

Эта распространенная ошибка возникает при попытке сослаться на индекс, которого не существует. Например, выражение "abc"[5] вызывает IndexError, потому что у этой строки нет индекса 5.

ZeroDivisionError

Как следует из названия, эта ошибка возникает при попытке делить на ноль, что, как мы знаем из математики, всегда плохая идея. Например, если мы пытаемся определить среднее арифметическое значений в списке по формуле sum(my_list) / len(my_list), но длина нашего списка равна нулю, возникнет эта ошибка.

Исключения при работе с файлами

Некоторые распространенные ошибки при работе с файлами: FileNotFoundError (при попытке обратиться к файлу, которого не существует), io.UnsupportedOperation (при попытке выполнить операцию с файлом, которая не поддерживается режимом, в котором файл открыт) или PermissionError (у программы нет необходимых прав доступа к файлу).

Обработка нескольких исключений сразу

К каждому блоку try может быть привязано более одного блока except. Например, следующая программа может обработать и FileNotFoundException, и PermissionError:

try:
    with open("example.txt") as my_file:
        for line in my_file:
            print(line)
except FileNotFoundError:
    print("The file example.txt was not found")
except PermissionError:
    print("No permission to access the file example.txt")

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


try:
    with open("example.txt") as my_file:
        for line in my_file:
            print(line)
except:
    print("There was an error when reading the file.")

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

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

try:
    with open("example.txt") as my_file:
        for line in myfile:
            print(line)
except:
    print("There was an error when reading the file.")

Блок except может скрыть фактическую ошибку: проблема здесь была вызвана не обработкой файла как таковой, а именем переменной, которое было написано с ошибкой. Без блока except была бы показана выброшенная ошибка, и причину было бы проще найти. Поэтому обычно хорошая идея — использовать только блоки except, явно объявленные для определенных типов ошибок.

Передача исключений

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

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

def testing(x):
    print(int(x) + 1)

try:
    number = input("Please type in a number: ")
    testing(number)
except:
    print("Something went wrong")
Пример вывода

Please type in a number: three Something went wrong

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

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

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

В следующем примере есть функция, которая вычисляет факториалы (например, факториал числа 5 равен 1 * 2 * 3 * 4 * 5). Если аргумент, переданный функции, отрицательный, функция вызывает ошибку:

def factorial(n):
    if n < 0:
        raise ValueError("The input was negative: " + str(n))
    k = 1
    for i in range(2, n + 1):
        k *= i
    return k

print(factorial(3))
print(factorial(6))
print(factorial(-1))
Пример вывода
6 720 Traceback (most recent call last): File "test.py", line 11, in print(factorial(-1)) File "test.py", line 3, in factorial raise ValueError("The input was negative: " + str(n)) ValueError: The input was negative: -1
Loading
Loading
Вы дошли до конца этого раздела! Перейти к следующему разделу:

Текущие баллы можно посмотреть в синем индикаторе в правом нижнем углу страницы.