Исключения: try, except, else, finally
Что такое исключение
Исключение — сигнал «что-то пошло не так», который прерывает обычное выполнение и поднимается вверх по стеку вызовов, пока его кто-нибудь не перехватит. Если не перехватит никто — программа завершится и напечатает трассировку (traceback).
def parse_age(text):
return int(text)
def register(form):
age = parse_age(form["age"])
return f"Возраст {age}"
print(register({"age": "двадцать"}))
Как читать traceback
Читайте снизу вверх: последняя строка — тип и сообщение ошибки (ValueError: invalid literal...). Выше — цепочка вызовов, от внешнего к месту ошибки. Ищите первую строку из своего кода.
try / except
def divide(a, b):
try:
result = a / b
except ZeroDivisionError:
print(" делить на ноль нельзя")
return None
return result
print(divide(10, 2))
print(divide(1, 0))
Несколько типов и объект исключения
def get_item(data, key):
try:
return data[key]
except (KeyError, IndexError) as e: # несколько типов в кортеже
print(f" нет элемента: {type(e).__name__}: {e}")
except TypeError as e:
print(f" неверный тип ключа: {e}")
print(" аргументы исключения:", e.args)
get_item({"a": 1}, "b")
get_item([1, 2], 5)
get_item([1, 2], "x")
Переменная e после блока except удаляется (чтобы не держать ссылку на трассировку).
Полная форма: else и finally
try:
... # код, который может упасть
except SomeError:
... # обработка ошибки
else:
... # если исключения НЕ было
finally:
... # ВСЕГДА — при успехе, ошибке, return, break
def process(value):
print(f"process({value!r})")
try:
number = int(value)
except ValueError:
print(" except: не число")
else:
print(f" else: успех, {number} * 2 = {number * 2}")
finally:
print(" finally: очистка")
process("21")
process("abc")
Пройдите пример 👣 Пошагово, чтобы увидеть, по каким веткам идёт выполнение.
Зачем else, если можно написать код в конце try? Чтобы не перехватить случайно исключения из этого кода: в try оставляют только то, что действительно может бросить ожидаемую ошибку.
finally и return
def tricky():
try:
return "из try"
finally:
print("finally выполняется перед выходом")
print(tricky())
def very_tricky():
try:
return "из try"
finally:
return "из finally" # перекрывает return из try — так делать нельзя!
print(very_tricky())
return в finally
return (или break) в finally перекрывает и результат, и исключение — ошибка молча исчезнет. В Python 3.14 это предупреждение SyntaxWarning.
Иерархия исключений
Исключения — классы, и except ловит указанный класс и всех его наследников:
print(KeyError.__mro__)
try:
{}["x"]
except LookupError as e: # ловит и KeyError, и IndexError
print("LookupError поймал", type(e).__name__)
try:
open("/нет/такого/файла.txt")
except OSError as e:
print(type(e).__name__, e.errno, isinstance(e, FileNotFoundError))
Порядок except важен
Проверяются сверху вниз, срабатывает первый подходящий. Сначала конкретные исключения, потом общие: except Exception перед except ValueError сделает второй блок мёртвым кодом.
Чего не делать
def bad():
try:
reslt = 10 / 2
return result # опечатка: NameError
except: # голый except проглотит и её
return "что-то не так"
print(bad()) # ошибка спрятана, искать её будете долго
| Антипаттерн | Почему плохо | Как правильно |
|---|---|---|
except: / except BaseException: |
ловит Ctrl+C, SystemExit, опечатки | конкретные типы |
except Exception: pass |
ошибки исчезают бесследно | хотя бы logger.exception(...) |
огромный блок try |
непонятно, что именно упало | минимум кода в try |
| исключения для обычной логики в горячем цикле | дорого | проверка if, dict.get |
except Exception допустим на верхнем уровне (обработчик запроса, главный цикл воркера) — чтобы залогировать неожиданную ошибку и не уронить весь сервис.
raise: бросить или пробросить
def withdraw(balance, amount):
if amount <= 0:
raise ValueError(f"сумма должна быть положительной, получено {amount}")
if amount > balance:
raise ValueError("недостаточно средств")
return balance - amount
try:
withdraw(100, 500)
except ValueError as e:
print("Ошибка:", e)
def load(path):
try:
raise FileNotFoundError(path)
except FileNotFoundError:
print(" логируем и пробрасываем дальше")
raise # голый raise — то же исключение с исходным traceback
try:
load("config.yaml")
except FileNotFoundError as e:
print("наверху:", repr(e))
EAFP на практике
import os
# LBYL: проверка и действие разделены — между ними файл могут удалить
path = "settings.ini"
if os.path.exists(path):
print("читаю")
# EAFP: просто пробуем
try:
with open(path) as f:
print(f.read())
except FileNotFoundError:
print("файла нет — используем настройки по умолчанию")
Вопрос на собеседовании
«Зачем нужны else и finally в try?» — else выполняется, только если в try не было исключения: туда выносят код, ошибки которого не надо перехватывать этим except. finally выполняется всегда — для освобождения ресурсов (хотя чаще для этого используют with).
Итог
- Исключение поднимается по стеку до первого подходящего
except; traceback читают снизу вверх. try/except/else/finally: else — при успехе, finally — всегда.exceptловит класс и наследников; сначала конкретные, потом общие.- Не пишите голый
except, не глушите ошибки молча, держитеtryмаленьким. raiseбросает новое исключение, голыйraiseпробрасывает текущее.
Практика решено 0 из 5
Напишите safe_int(text, default=0): возвращает int(text), а если строка не число
(или передан None) — возвращает default. Перехватывайте только нужные исключения
(ValueError и TypeError), без голого except.
Какие тесты проверяют код (3)
assert safe_int("42") == 42 and safe_int(" -7 ") == -7
assert safe_int("abc") == 0 and safe_int("3.5", default=-1) == -1
assert safe_int(None) == 0
def safe_int(text, default=0):
try:
return int(text)
except (ValueError, TypeError):
return default
Напишите read_config(data, key): возвращает data[key] и в любом случае добавляет
строку "прочитано" в список data["_log"] (используйте finally).
Если ключа нет, KeyError должен вылететь наружу — не перехватывайте его.
Какие тесты проверяют код (2)
d = {"_log": [], "port": 80}; assert read_config(d, "port") == 80 and d["_log"] == ["прочитано"]
d = {"_log": []}
try:
read_config(d, "host")
ok = False
except KeyError:
ok = True
assert ok and d["_log"] == ["прочитано"], "KeyError должен вылететь, но лог — записаться"
def read_config(data, key):
try:
return data[key]
finally:
data["_log"].append("прочитано")
Что выведет код?
def f():
try:
return "try"
finally:
print("finally")
print(f())
Выполнится ли блок?
| выполнится | не выполнится | |
|---|---|---|
else, если в try не было исключения |
||
else, если в try было перехваченное исключение |
||
finally, если исключение не перехвачено ни одним except |
||
finally, если в try выполнился return |
||
второй except, если подошёл первый |
Почему except: без типа (и except BaseException:) считается плохой практикой?
Войдите, чтобы отмечать пройденные уроки.