Вы уже писали функции, которые принимают параметры, что-то считают и возвращают результат. Но что, если внутри что-то пошло не так — пользователь ввёл не то, что ожидалось, или число получилось таким, что на него нельзя делить? До сих пор в такой ситуации программа просто останавливалась, и весь код после этого места не выполнялся. Разберёмся, как сделать программу устойчивой: перехватывать проблемы и продолжать работу, а не падать.
Зачем программе уметь обрабатывать ошибки
Представьте кассира в магазине. Покупатель протягивает мятую купюру, которую аппарат не может считать. Хороший кассир не закрывает кассу и не уходит домой — он замечает проблему, просит другую купюру и продолжает обслуживать очередь. Плохой кассир теряется и останавливает работу всей кассы.
Программы должны вести себя как хороший кассир. Пользователь вводит буквы вместо числа, число получилось таким, что на него нельзя делить — таких ситуаций много. Если не предусмотреть их заранее, 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") — "Ошибка: деление на ноль недопустимо".