$ sudo teach IT
Модуль 6 · Функции · Урок 6.7

Исключения: try, except, raise

Учим программу не падать при ошибках: ловим их через try/except, поднимаем сами через raise и разбираемся, что выполнится в любом случае

Теория~30 минутНовичокtry / exceptelse / finallyraiseValueErrorобработка ошибок

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

Зачем программе уметь обрабатывать ошибки

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

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

В программировании такая нештатная ситуация называется исключением (exception, в переводе — "исключительный случай"). Когда исключение возникает и его никто не ловит, Python выводит в консоль подробный отчёт о том, что случилось и в какой строке — он называется трассировкой (traceback).

Посмотрим на простую программу, которая ничего не проверяет:

x = int(input("Введите число: "))
print(10 / x)

Если пользователь введёт 0 или слово вроде яблоко, программа немедленно остановится и выведет в консоль трассировку. Выглядит она примерно так — каждая строка идёт с новой строки, снизу самая важная, последняя:

Traceback (most recent call last): File "program.py", line 1, in <module> x = int(input("Введите число: ")) ValueError: invalid literal for int() with base 10: 'яблоко'

  • Traceback (most recent call last) — заголовок: "путь вызовов, последний самый свежий". Показывает цепочку мест, через которые программа дошла до ошибки.
  • File "program.py", line 1 — в каком файле и на какой строке всё сломалось.
  • x = int(input(...)) — сама строка кода, где произошла проблема.
  • ValueError: invalid literal for int() ... — тип ошибки (ValueError) и её описание: Python не смог превратить строку 'яблоко' в целое число.

Программа упала: строка print(10 / x) вообще не выполнилась. Пользователь вместо понятного сообщения увидел стену незнакомого текста. Это и есть проблема, которую решает механизм исключений.

Синтаксис try / except

Представьте страховку: вы заранее договариваетесь, что если что-то пойдёт не так, сработает конкретный план действий, а не хаос. В Python роль такой страховки играет конструкция try-except: вы заранее говорите, что делать, если код внутри "попробовать" не сработает.

try (англ. "попробовать") — это блок, куда помещают потенциально опасный код. except (англ. "за исключением") — блок, который выполнится, если в try возникло исключение. Если исключения не было, блок except вообще пропускается.

try:
    x = int(input("Введите число: "))
    print(10 / x)
except:
    print("Что-то пошло не так")
  • try: — открывает блок, в котором Python будет "наблюдать" за ошибками.
  • x = int(input(...)) и print(10 / x) — тот же рискованный код, что и раньше, только теперь он под присмотром.
  • except: — блок без указания типа ошибки. Он перехватит любое исключение, какое бы ни случилось внутри try.
  • print("Что-то пошло не так") — выполнится вместо падения программы, если в try случилась любая ошибка.

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

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

Перехват конкретного исключения

Вместо ловли "чего угодно" правильнее сказать Python точно, какую ошибку вы ждёте. У каждой типичной проблемы в Python есть своё имя — тип исключения. Например, когда текст не удаётся превратить в число, возникает исключение ValueError. Указав его после except, вы ловите только эту конкретную ситуацию.

try:
    x = int(input("Введите число: "))
    print(10 / x)
except ValueError:
    print("Это не число")
  • except ValueError: — ловит только ошибки типа ValueError (неудачное преобразование в число).
  • Если пользователь введёт яблоко — сработает именно этот блок, выведется Это не число.
  • А если пользователь введёт 0 — произойдёт другая ошибка, ZeroDivisionError, а не ValueError. Её этот except не поймает, и программа всё равно упадёт с трассировкой — потому что для деления на ноль мы обработчик пока не написали.

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

Несколько блоков except

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

try:
    x = int(input("Введите число: "))
    print(10 / x)
except ValueError:
    print("Введите число, а не текст")
except ZeroDivisionError:
    print("На ноль делить нельзя")
  • Python выполняет try сверху вниз и, если возникла ошибка, проверяет блоки except по порядку — тоже сверху вниз.
  • Заходит в первый подходящий блок: если ошибка — ValueError, сработает первый except, если ZeroDivisionError — второй.
  • Остальные блоки except при этом пропускаются, даже если формально тоже могли бы подойти.

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

Текст ошибки: except ... as e

Когда врач ставит диагноз, он не просто говорит "вы больны" — он называет конкретную причину. Так и Python: у каждого исключения есть не только тип, но и текстовое описание причины. Чтобы получить его, добавьте после типа ошибки слово as и имя переменной.

try:
    x = int(input("Введите число: "))
    print(10 / x)
except ValueError as e:
    print(f"Ошибка ввода: {e}")
except ZeroDivisionError as e:
    print(f"Математическая ошибка: {e}")
  • except ValueError as e: — если возникла ошибка ValueError, Python кладёт объект этой ошибки в переменную e.
  • f"Ошибка ввода: {e}" — при выводе объекта ошибки внутри f-строки он превращается в свой текст-описание, например invalid literal for int() with base 10: 'яблоко'.
  • Имя e — просто договорённость, можно назвать переменную и по-другому, но e (от слова error) встречается почти везде.

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

Блок else: если всё прошло гладко

Иногда нужен код, который должен выполниться только тогда, когда в try вообще не было проблем. Формально его можно просто дописать после блока try, но тогда, если в этом коде случится ошибка того же типа, её незаметно поймает тот же except, хотя ловить должен был только исходный код. Чтобы разделить эти зоны ответственности, у try-except есть необязательный блок else.

try:
    x = int(input("Введите число: "))
    result = 10 / x
except ValueError:
    print("Это не число")
except ZeroDivisionError:
    print("На ноль нельзя")
else:
    print(f"Результат: {result}")
  • else: — ставится после всех блоков except, на том же уровне отступа, что и try.
  • Код внутри else выполнится, только если во всём блоке try ни одна строка не вызвала исключение.
  • print(f"Результат: {result}") — здесь мы уверены, что переменная result точно существует и посчитана правильно, ведь если бы деление сорвалось, до else выполнение бы просто не дошло.

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

Блок finally: выполняется в любом случае

После эксперимента в лаборатории убирают за собой — независимо от того, получился опыт или нет. В Python за такую "уборку в любом случае" отвечает блок finally: он выполнится и если ошибки не было, и если она случилась, и даже если она случилась и её никто не поймал.

try:
    x = int(input("Введите число: "))
    print(10 / x)
except ZeroDivisionError:
    print("На ноль нельзя")
finally:
    print("Попытка деления завершена")
  • finally: — ставится последним, после всех except (и else, если он есть).
  • print("Попытка деления завершена") — выведется при любом сценарии: успешном делении, пойманной ошибке или даже незнакомой ошибке, которую вы не обработали.

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

try:
    рискованный код
except ТипОшибки:
    что делать при этой ошибке
else:
    что делать, если ошибок не было
finally:
    что сделать в любом случае

raise: вызвать исключение самому

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

def set_age(age):
    if age < 0:
        raise ValueError("Возраст не может быть отрицательным")
    print(f"Возраст установлен: {age}")

try:
    set_age(-5)
except ValueError as e:
    print(f"Ошибка: {e}")
  • def set_age(age): — обычная функция с одним параметром, как вы уже писали раньше.
  • raise ValueError("Возраст не может быть отрицательным") — создаёт исключение типа ValueError с заданным текстом и сразу же его "выбрасывает". Строки после raise внутри функции уже не выполнятся.
  • set_age(-5) внутри try — вызов функции, который приводит к raise внутри неё.
  • Поднятое исключение вылетает из функции наружу и попадает в ближайший подходящий except — в данном случае в except ValueError as e, который печатает Ошибка: Возраст не может быть отрицательным.

Если бы вызов set_age(-5) не был обёрнут в try-except, программа упала бы с трассировкой — точно так же, как при встроенных ошибках Python. raise использует ту же самую систему исключений, просто запускаете её вы сами.

Справочник: распространённые исключения

Все встроенные исключения Python в конечном счёте являются разновидностями одного общего типа — Exception. Поэтому если очень нужно поймать почти любую ошибку, пишут except Exception, а не голый except: — так вы хотя бы не перехватите системные сигналы вроде остановки программы по Ctrl+C. Но злоупотреблять этим не стоит: чем конкретнее тип, тем понятнее код.

Вот исключения, с которыми вы будете сталкиваться чаще всего:

Исключение Когда возникает Пример
ValueError Тип значения правильный, а само значение не подходит int("abc")
TypeError Операция применена не к тому типу данных "5" + 3
ZeroDivisionError Деление на ноль 10 / 0
IndexError Индекс за пределами списка или строки [1, 2][5]
KeyError Такого ключа нет в словаре d["нет"]
FileNotFoundError Файл с таким именем не найден open("нет.txt")

Частые ошибки

Голый except: прячет всё подряд

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

Широкий except выше узкого

Если except Exception стоит раньше except ValueError, до второго блока выполнение никогда не дойдёт — первый перехватит всё сам. Конкретные типы всегда должны идти выше общих.

Использование переменной из try после ошибки

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

try-except вместо простой проверки

Не стоит оборачивать в try-except то, что проще предотвратить обычным if — например, if b != 0: перед делением.

Что важно запомнить

  • try оборачивает рискованный код, except — ловит и обрабатывает ошибку, если она случилась.
  • Всегда указывайте конкретный тип исключения — голый except: прячет даже опечатки в коде.
  • Несколько except проверяются по порядку сверху вниз, срабатывает первый подходящий — конкретные типы ставьте выше общих.
  • except ТипОшибки as e даёт доступ к тексту ошибки через переменную e.
  • else выполняется, только если в try вообще не было исключений.
  • finally выполняется всегда — и при ошибке, и без неё. Используется для завершающих действий.
  • raise ТипОшибки("текст") позволяет самим поднять исключение, когда данные некорректны по смыслу.
  • except Exception ловит почти любую ошибку, но злоупотреблять этим не стоит.

Проверьте себя

6 вопросов

Безопасное деление

Напишите функцию safe_divide(a, b), которая получает два значения в виде строк a и b и должна поделить одно на другое.

  • Если a или b нельзя превратить в целое число — верните строку "Ошибка: нужно вводить числа".
  • Если после преобразования второе число оказалось равно нулю — верните строку "Ошибка: деление на ноль недопустимо".
  • Если всё в порядке — верните результат деления.

Например, safe_divide("10", "2") должна вернуть 5.0, а safe_divide("10", "0") — "Ошибка: деление на ноль недопустимо".

Валидация возраста

Premium

Безопасный парсер числа из строки

Premium