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

Синтаксические ошибки

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

Например, выведем сообщение на экран, но пропустим закрывающую скобку при вызове функции print():

print("Эта инструкция написана не очень правильно"

Запустив этот код, вы увидите примерно такое сообщение о синтаксической ошибке:

Traceback (most recent call last):
  File "/home/irina/projects/task.py", line 2
    print("Эта инструкция написана не очень правильно"
         ^
SyntaxError: '(' was never closed

Python выводит трассировку (англ. traceback) с информацией не только о самой ошибке, но и о её месте. Так line 2 означает, что исключение вызвал код в строке 2.

Исключения

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

Давайте напишем программу, которая запрашивает у пользователя два числа и выводит на экран результат деления первого числа на второе:

number1 = float(input("Пожалуйста, введите первое число: "))
number2 = float(input("Пожалуйста, введите второе число: "))
result = number1 / number2
print(f"{number1} / {number2} = {result}")

Допустим, пользователь ввёл числа 1 и 2, тогда программа завершится без ошибок:

Пожалуйста, введите первое число: 1
Пожалуйста, введите второе число: 2
1 / 2 = 0.5

Но если в качестве второго числа был введён 0, то выполнение программы прервётся на строке result = number1 / number2 и будет вызвано исключение ZeroDivisionError:

Пожалуйста, введите первое число: 1
Пожалуйста, введите второе число: 0
Traceback (most recent call last):
  File "/home/irina/toss a coin/Архив/Учебник.py", line 3, in <module>
    result = number1 / number2
             ~~~~~~~~^~~~~~~~~
ZeroDivisionError: float division by zero

Эта же программа может прерваться и из-за другого исключения, если пользователь вместо числа введёт строку, которую невозможно преобразовать в число:

Пожалуйста, введите первое число: Цифра 1
Traceback (most recent call last):
  File "/home/irina/toss a coin/Архив/Учебник.py", line 1, in <module>
    number1 = int(input("Пожалуйста, введите первое число: "))
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'Цифра 1'

Так как строку "Цифра 1" нельзя преобразовать в целое число, выполнение программы прерывается на первой же строке и вызывается исключение ValueError.

Типы исключений

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

Давайте рассмотрим некоторые стандартные исключения.

1. NameError (с англ. – ошибка имени) – используется объект (переменная или функция), который еще не были создан.

print(username)
# Ошибка: NameError: name 'username' is not defined

Здесь переменной name еще не было присвоено значение, поэтому её нельзя вывести на экран.

2. TypeError (с англ. – ошибка типа) – операция применяется к объекту не того типа:

"10" + 10
# Ошибка: TypeError: can only concatenate str (not "int") to str

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

3. ValueError (с англ. – ошибка значения) – операция применяется к объекту правильного типа, но недопустимого значения:

int("Число 25")
# Ошибка: ValueError: invalid literal for int() with base 10: 'Число'

Здесь строку "Число 25" невозможно преобразовать в число, так как она содержит недопустимые буквенные символы.

4. ZeroDivisionError (с англ. – ошибка деления на ноль) – деление на ноль:

result = 10.5 / 0
# Ошибка: ZeroDivisionError: float division by zero

Здесь всё совсем как в математике: на ноль делить нельзя.

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

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

try:
    действия_которые_могут_привести_к_исключению
except ТипИсключения:
    действия_если_возникло_исключение
else:
      действия_если_не_возникло_исключение
finally:
      действия_которые_будут_выполнены_в_любом_случае

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

Кроме этого, данная конструкция включает в себя необязательные блоки else и finally. Блок else выполняется только в том случае, если код в блоке try был выполнен без исключений. А блок finally выполняется всегда, независимо от того, было ли исключение.

Как мы уже убедились, деление на ноль приводит к исключению ZeroDivisionError, а преобразование в число строки в неправильном формате – к исключению ValueError. Поэтому давайте перепишем пример программы для деления двух чисел, используя try/except и данные типы исключений:

try:
    number1 = int(input("Пожалуйста, введите первое целое число: "))
    number2 = int(input("Пожалуйста, введите второе целое число: "))
    result = number1 / number2
    print(f"{number1} / {number2} = {result}")
except ZeroDivisionError:
    print("На ноль делить нельзя!")
except ValueError:
    print("Пожалуйста, введите целое число!")

Теперь если пользователь захочет поделить на ноль, программа всё равно продолжит работу:

Пожалуйста, введите первое число: 1
Пожалуйста, введите второе число: 0
На ноль делить нельзя!

Также такой код обрабатывает ввод некорректного значения и исключение ValueError:

Пожалуйста, введите первое целое число: пять 
Пожалуйста, введите целое число!

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

try:
    number1 = int(input("Пожалуйста, введите первое число: "))
    number2 = int(input("Пожалуйста, введите второе число: "))
    result = number1/number2
    print(f"{number1} / {number2} = {result}")
except (ValueError, ZeroDivisionError):
     print("Упс! Что-то пошло не так!")

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

try:
    number1 = int(input("Пожалуйста, введите первое число: "))
    number2 = int(input("Пожалуйста, введите второе число: "))
    result = number1/number2
    print(f"{number1} / {number2} = {result}")
except:
    print("Упс! Что-то пошло не так!")

В таком случае блок except обрабатывает все возникающие исключения. При делении на ноль мы увидим сообщение "Упс! Что-то пошло не так!":

Пожалуйста, введите первое число: 1
Пожалуйста, введите второе число: 0
Упс! Что-то пошло не так!

Такое же, как и при вводе значения, не являющегося числом:

Пожалуйста, введите первое число: пять
Упс! Что-то пошло не так!

Но для того, чтобы понимать, какое исключение было перехвачено, с помощью конструкции Exception as e можно сохранить информацию об исключении в переменную e:

try:
    number1 = int(input("Пожалуйста, введите первое число: "))
    number2 = int(input("Пожалуйста, введите второе число: "))
    result = number1/number2
    print(f"{number1} / {number2} = {result}")
except Exception as e:
    print(f"Упс! Что-то пошло не так: {e}") 

Значение переменной e может быть выведено на экран:

Пожалуйста, введите первое число: 1
Пожалуйста, введите второе число: 0
Упс! Что-то пошло не так: division by zero

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

Необязательный блок else

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

try:
    number1 = int(input("Пожалуйста, введите первое число: "))
    number2 = int(input("Пожалуйста, введите второе число: "))
    result = number1/number2
    print(f"{number1} / {number2} = {result}")
except ValueError:
    print("Вы должны ввести число!")
except ZeroDivisionError:
    print(f"На ноль делить нельзя!")  
else:
    print(f"Увеличим результат деления в 1000 раз: {result * 1000}")

Здесь результат деления будет увеличен в 1000 раз, если код в блоке try был выполнен полностью:

Пожалуйста, введите первое число: 1
Пожалуйста, введите второе число: 2
1 / 2 = 0.5
Увеличим результат деления в 1000 раз: 500.0

Но если возникло исключение и был выполнен код из блока except, то код в блоке else игнорируется:

Пожалуйста, введите первое число: 1
Пожалуйста, введите второе число: 0
На ноль делить нельзя!

Такая структура позволяет не помещать всю логику в блок try.

Необязательный блок finally

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

try:
    number1 = int(input("Пожалуйста, введите первое число: "))
    number2 = int(input("Пожалуйста, введите второе число: "))
    result = number1/number2
    print(f"{number1} / {number2} = {result}")
except ValueError:
    print("Вы должны ввести число!")
except ZeroDivisionError:
    print(f"На ноль делить нельзя!")  
else:
    print(f"Увеличим результат деления в 1000 раз: {result * 1000}")
finally:
    print("Спасибо за использование нашего калькулятора!")

Сообщение из блока finally будет выведено на экран, как в случае корректной работы программы и отсутствия исключений:

Пожалуйста, введите первое число: 1
Пожалуйста, введите второе число: 2
1 / 2 = 0.5
Спасибо за использование нашего калькулятора!

Так и если было перехвачено какое-то исключение:

Пожалуйста, введите первое число: 1  
Пожалуйста, введите второе число: 0
На ноль делить нельзя!
Спасибо за использование нашего калькулятора!

Принудительный вызов исключений

Иногда возникает необходимость принудительно вызвать исключение в коде. Это может быть нужно, например, для оповещения о недопустимом введённых данных, или же для повторной генерации уже пойманного исключения. Для этой цели в Python используется оператор raise:

raise ТипИсключения("Сообщение об ошибке")

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

Например, мы можем вызывать исключение при вводе отрицательного числа:

number = int(input("Введите число: "))    
if number < 0:
    # Вызываем исключение, если число отрицательное
    raise ValueError("Число должно быть положительным!")

Если мы запустим этот код и введём отрицательное число, например, -10, то это приведет к аварийному завершению программы с трассировкой, указывающей на наше принудительно вызванное исключение:

Введите число: -10
Traceback (most recent call last):
  File "/home/irina/euthymia/test.py", line 4, in <module>
    raise ValueError("Число должно быть положительным!")
ValueError: Число должно быть положительным!

Как видите, это выглядит точно так же, как и любое другое исключение, автоматически вызванное Python.

Также оператор raise часто используется внутри блока except для повторного вызова уже перехваченного исключения. Это полезно, когда мы хотим выполнить какие-то действия логированию (записи информации) ошибки в блоке except, но при этом всё равно хотим вызвать соответствующее исключение:

number = int(input("Введите число: "))
try:
    result = 10 / number
except ZeroDivisionError:
    print("Была перехвачена попытка деления на ноль")
    # Мы обработали исключение, но хотим, чтобы оно пошло дальше
    raise

Если пользователь введёт число 0, то будет выведено сообщение о перехвате попытки деления на ноль, после чего вызвано исключение ZeroDivisionError:

Введите число: 0
Была перехвачена попытка деления на ноль
Traceback (most recent call last):
  File "/home/irina/euthymia/test.py", line 3, in <module>
    result = 10 / number
             ~~~^~~~~~~~
ZeroDivisionError: division by zero

Без использования оператора raise программа бы завершилась корректно и просто было выведена строка с уведомлением об ошибке.

Рекомендации по обработке исключений

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

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

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

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

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

Примеры

Пример 1. Выбор пункта меню в программе

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

print("Выберите пункт меню")
print("1. Показать новости")
print("2. Просмотреть профиль")
print("3. Выйти")
 
menu_choice = 0 # Переменная для хранения выбора пользователя
while True:
    try:
        choice_str = input("Введите номер пункта меню: ")
        # Попытка преобразовать ввод в целое число
        menu_choice = int(choice_str)
    except ValueError:
        # Обрабатываем, если строка не может быть преобразована в число
        print("Ошибка: Вы должны ввести номер пункта (1, 2 или 3)")
    else:
        # Если ввод успешно преобразован в число, проверяем его диапазон
        if 1 <= menu_choice <= 3:
            print(f"Вы выбрали пункт {menu_choice}.")
            break # Выходим из цикла, если выбор корректен
        else:
            print("Ошибка: Такого пункта нет. Введите 1, 2 или 3")
    finally:
        # Выводится после каждой попытки выбора
        print("Попытка выбора пункта меню завершена")
 
print("Программа продолжает работу с вашим выбором")

Вывод:

Выберите пункт меню
1. Показать новости
2. Просмотреть профиль
3. Выйти
Введите номер пункта меню: номер?
Ошибка: Вы должны ввести номер пункта (1, 2 или 3)
Попытка выбора пункта меню завершена.
Введите номер пункта меню: 1
Вы выбрали пункт 1
Попытка выбора пункта меню завершена
Программа продолжает работу с вашим выбором

Пример 2. Расчёт среднего балла

Программа запрашивает баллы по трём предметам. Если пользователь вводит нечисловое значение для любого из баллов, программа сообщает об ошибке и просит ввести баллы заново.

while True:
    try:
        print("Пожалуйста, введите баллы:")
        score1_str = input("Балл за первый предмет: ")
        score1 = int(score1_str)

        score2_str = input("Балл за второй предмет: ")
        score2 = int(score2_str)
        score3_str = input("Балл за третий предмет: ")
        score3 = int(score3_str)
 
        # Если все баллы успешно преобразованы
        average_score = (score1 + score2 + score3) / 3
        print("==========================")
        print(f"Средний балл: {average_score:.2f}")
        break # Выходим из цикла, если все баллы введены корректно
 
    except ValueError:
        # Обрабатываем, если балл не является целым числом
        print("Ошибка: Все баллы должны быть целыми числами")
    except Exception as e:
        # Обрабатываем любые другие неожиданные ошибки
        print(f"Произошла непредвиденная ошибка: {e}")
    finally:
        # Этот блок выполняется после каждой попытки ввода баллов
        print("Попытка ввода баллов завершена")

Вывод:

Пожалуйста, введите баллы:
Балл за первый предмет: 97
Балл за второй предмет: 84
Балл за третий предмет: 78
==========================
Средний балл: 86.33
Попытка ввода баллов завершена

Итоги

  • Синтаксические ошибки – это ошибки, возникающие из-за нарушений правил языка (опечатки, пропущенные символы). Код, содержащий синтаксические ошибки, не будет запущен.
  • Исключения – это ошибки, возникающие во время выполнения программы, приводящие к её аварийному завершению.
  • Конструкция try/except перехватывает и обрабатывает исключения, предотвращая аварийное завершение программы. Код, который может вызвать исключение, помещается в блок try. Если исключение возникает, управление передаётся в блок except.
  • Необязательный блок else после try/except выполняется только в том случае, если код в блоке try был выполнен без исключений.
  • Необязательный блок finally после try/except выполняется всегда, независимо от того, было ли исключение.
  • Ключевое слово raise позволяет принудительно вызвать исключение.

Задания для самопроверки

1. Найдите и исправьте ошибки в следующем коде:

age = int(input("Сколько тебе лет? ")
if age >= 18
  print("Ты совершеннолетний и можешь голосовать)
else:
  print("Ты ещё несовершеннолетний, тебе нужно подождать")
age = int(input("Сколько тебе лет? "))
if age >= 18:
  print("Ты совершеннолетний и можешь голосовать")
else:
  print("Ты ещё несовершеннолетний, тебе нужно подождать")

Пропущено двоеточие в условии if age > 18 и пропущена закрывающая кавычка в строке "Ты совершеннолетний и можешь голосовать".

2. Рассмотрите следующую трассировку ошибки:

Traceback (most recent call last):
  File "/home/user/script.py", line 5, in calculate
    result = number1 / number2
ZeroDivisionError: division by zero

На какой строке кода возникло исключение? Что является причиной возникновения ошибки?

Исключение вызвал код на строке 5. Причиной является деление на ноль.

3. Запросите у пользователя строку и преобразуйте её в целое число. Если пользователь ввёл не число, то выведите на экран строку "Не удалось преобразовать в число".

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

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

1

1

Число 2

Не удалось преобразовать в число

-12

-12

try:
    user_input = input("Пожалуйста, введите целое число: ")
    number = int(user_input)
    print(f"Вы ввели число: {number}")
except ValueError:
    print("Не удалось преобразовать в число")

4. В каком случае будет выполнен блок else в конструкции try/except/else?

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

5. Запросите у пользователя возраст, и если возраст больше или равен 16, то выведите сообщение "Доступ разрешён", иначе – "Доступ запрещён". Если введённую строку нельзя преобразовать в число, то выведите сообщение "Введённое значение должно быть целым числом". После обработки ввода, независимо от того, было ли введенное значение корректным числом или нет, выведите сообщение "Проверка возраста завершена".

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

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

16

Доступ разрешён

Проверка возраста завершена

14

Доступ запрещён

Проверка возраста завершена

Всегда восемнадцать

Введённое значение должно быть целым числом

Проверка возраста завершена

try:
    age = int(input("Пожалуйста, введите ваш возраст: "))
except ValueError:
    print("Введённое значение должно быть целым числом")
else:
    print("Доступ разрешён." if age >= 16 else "Доступ запрещён")
finally:
    print("Проверка возраста завершена")