Исключения B: else и finally — полная структура
- Объяснить, когда выполняется каждый из четырёх блоков — try, except, else, finally
- Использовать
elseдля кода, который выполняется, только если ошибки не было - Использовать
finallyдля действий по очистке, которые должны происходить всегда - Предсказывать порядок печати в программе с полным try
Что мы изучим?
В предыдущем уроке мы использовали try и except. В структуре есть ещё две части:
else— выполняется, только если ошибки не было.finally— выполняется всегда, с ошибкой или без.
Зачем это нужно?
Представьте код, который открывает соединение с базой данных (темы бэкенда и баз данных), выполняет действие и закрывает соединение. Что будет, если действие упадёт посередине? Если закрытие записано после действия, оно не будет выполнено, и соединение останется открытым. После сотни таких сбоев сервер упадёт, потому что у него закончились соединения.
finally гарантирует, что очистка произойдёт в любом случае. else сохраняет try маленьким и делает понятным, что выполняется только после успеха.
Объяснение с основ
Четыре блока
| Блок | Когда выполняется | Для чего |
|---|---|---|
try | Всегда начинают с него | Код, который может упасть |
except | Только если выброшена ошибка подходящего типа | Обработка ошибки |
else | Только если в try не было выброшено ни одной ошибки | Продолжение, зависящее от успеха |
finally | Всегда, в конце, в любом случае | Очистка: закрытие файлов, соединений, завершающее сообщение |
try = вы пытаетесь испечь торт. except = если он сгорел, вы заказываете торт в пекарне. else = если он получился, вы его украшаете (сгоревший торт не украшают!). finally = в любом случае вы выключаете духовку и моете посуду.
Зачем else, а не просто продолжить в try?
Код продолжения можно положить внутрь try. Но тогда, если этот код упадёт с той же ошибкой, except по ошибке поймает её и обработает не ту проблему. else делает ясным: внутри try — только рискованная строка, а остальное — снаружи.
finally выполняется, даже когда…
- Была ошибка, и её поймали.
- Была ошибка, и её не поймали.
finallyвыполняется, а затем программа падает. - В середине
tryстоитreturn.finallyвыполняется до того, как функция действительно вернёт значение.
Синтаксис
try:
# risky code
except SomeError:
# runs only if SomeError happened
else:
# runs only if NO error happened in try
finally:
# runs ALWAYS, lastОбязательный порядок: try → except (один или несколько) → else (необязательно) → finally (необязательно). Можно также try + finally без except.
Первый пример кода
week-2/full_try.py:
def convert(text):
print("--- converting:", text)
try:
number = int(text)
except ValueError:
print("except: not a number")
else:
print("else: success, doubled =", number * 2)
finally:
print("finally: done with", text)
convert("21")
convert("hi")Вывод:
--- converting: 21
else: success, doubled = 42
finally: done with 21
--- converting: hi
except: not a number
finally: done with hiРазбор кода построчно
| Блок | convert("21") | convert("hi") |
|---|---|---|
try: number = int(text) | Успешно: number = 21 | ValueError |
except ValueError: | Пропускается | Выполняется |
else: | Выполняется: печатает 42 | Пропускается |
finally: | Выполняется | Выполняется |
else и except никогда не выполняются вместе: либо ошибка была, либо нет. А finally выполняется в обоих случаях.
Ещё один пример
«Соединение», которое нужно закрыть. Здесь мы имитируем соединение с внешним ресурсом (например, с базой данных) и видим, что finally закрывает его и тогда, когда что-то падает посередине, и тогда, когда есть return. week-2/connection_demo.py:
def open_connection():
print("[connection opened]")
def close_connection():
print("[connection closed]")
def fetch_price(product_name, prices):
open_connection()
try:
price = prices[product_name]
except KeyError:
print("No such product:", product_name)
return None
else:
print("Found", product_name)
return price
finally:
close_connection()
catalog = {"bread": 8, "milk": 6}
print("Result:", fetch_price("milk", catalog))
print()
print("Result:", fetch_price("cake", catalog))Вывод:
[connection opened]
Found milk
[connection closed]
Result: 6
[connection opened]
No such product: cake
[connection closed]
Result: NoneРазбор:
catalog = {"bread": 8, "milk": 6}— это dict (словарь): каждому ключу ("bread") сопоставлено значение (8).prices["milk"]возвращает 6. Подробно изучим его в теме структур данных и функций. Пока достаточно знать, что обращение к несуществующему ключу выбрасываетKeyError.- Обратите внимание:
returnесть и вexcept, и вelse. И всё же[connection closed]печатается доResult. То естьfinallyвыполняется после того, какreturn«решён», но до того, как функция действительно возвращает значение. - Без
finallyпришлось бы писатьclose_connection()перед каждымreturn. Легко забыть, и много повторений.
Типичная ошибка
Ошибка 1: класть очистку после try/except, а не в finally.
open_connection()
try:
price = prices[name]
except KeyError:
print("missing")
return None
close_connection() # never reached when except returns!В случае KeyError оператор return выходит из функции, и close_connection() не выполняется. Соединение остаётся открытым. Именно это решает finally.
Ошибка 2: код продолжения внутри try и except, который ловит не ту ошибку.
try:
age = int(text)
category = ["child", "adult"][age // 18] # can also raise IndexError... or ValueError?
except ValueError:
print("Age must be a number")Если строка продолжения упадёт, например с IndexError для возраста 40 (40 // 18 = 2, а индекса 2 нет), сообщение будет совершенно не по делу. Положите внутрь try только int(text), а продолжение — в else.
Ошибка 3: думать, что finally «ловит» ошибку. Нет. Если подходящего except нет, finally выполняется, а после этого программа падает с Traceback.
Практика
Упражнения опираются только на то, что пройдено в этом уроке и в предыдущих. Сначала попробуйте сами, и только потом открывайте решение.
Упражнение 1: Предскажите вывод
Напишите, что будет напечатано для каждого вызова, — до запуска:
def half(text):
try:
number = int(text)
except ValueError:
print("not a number:", text)
else:
print("half is", number / 2)
finally:
print("checked", text)
half("8")
half("x")Показать решение
def half(text):
try:
number = int(text)
except ValueError:
print("not a number:", text)
else:
print("half is", number / 2)
finally:
print("checked", text)
half("8")
half("x")half is 4.0
checked 8
not a number: x
checked x- Для
"8"ошибки нет, поэтому выполняетсяelse, а за нимfinally. - Для
"x"выбрасываетсяValueError, поэтому выполняетсяexcept— аelseне выполняется.finallyвыполняется в обоих случаях.
Упражнение 2: finally выполняется даже после return
def check(number):
try:
if number > 0:
return "positive"
return "not positive"
finally:
print("done checking", number)
print(check(5))
print(check(-2))Что будет напечатано и в каком порядке? Объясните почему.
Показать решение
def check(number):
try:
if number > 0:
return "positive"
return "not positive"
finally:
print("done checking", number)
print(check(5))
print(check(-2))done checking 5
positive
done checking -2
not positivefinally выполняется до того, как функция действительно возвращает значение, — даже когда внутри try есть return. Поэтому «done checking» печатается раньше значения, которое вернула функция.
Упражнение 3: Четыре блока
Напишите функцию parse_quantity(text), которая использует все четыре блока:
try— преобразует текст в число;except— печатаетinvalid: <text>;else— печатаетquantity: <number>;finally— печатает---.
parse_quantity("3")
parse_quantity("three")Ожидаемый вывод:
quantity: 3
---
invalid: three
---Показать решение
def parse_quantity(text):
try:
quantity = int(text)
except ValueError:
print("invalid:", text)
else:
print("quantity:", quantity)
finally:
print("---")
parse_quantity("3")
parse_quantity("three")quantity: 3
---
invalid: three
---Код, который выполняется только при полном успехе, находится в else, а не внутри try, — так try остаётся маленьким и ловит только строку, которая действительно может упасть.
Упражнение 4: Закрываем то, что открыли
Функции open_register() и close_register() открывают и закрывают кассу. Напишите функцию sell(prices, position), которая открывает кассу, возвращает цену на выбранной позиции (позиция — индекс списка, начинается с 0) и всегда закрывает кассу — даже когда позиции не существует. Для несуществующей позиции возвращается None.
def open_register():
print("register opened")
def close_register():
print("register closed")
print(sell([10, 20, 30], 1))
print(sell([10, 20, 30], 7))Ожидаемый вывод:
register opened
register closed
20
register opened
register closed
NoneПоказать решение
def open_register():
print("register opened")
def close_register():
print("register closed")
def sell(prices, position):
open_register()
try:
return prices[position]
except IndexError:
return None
finally:
close_register()
print(sell([10, 20, 30], 1))
print(sell([10, 20, 30], 7))register opened
register closed
20
register opened
register closed
NoneЭто именно задача finally: очистка — закрыть открытый ресурс, как бы ни завершилась функция.
Упражнение 5: else или внутри try?
Два разработчика написали один и тот же код двумя способами:
# A # B
try: try:
number = int(text) number = int(text)
print(100 / number) except ValueError:
except ValueError: print("not a number")
print("not a number") else:
print(100 / number)В чём разница и какая версия лучше? Что произойдёт в каждой из них, когда text равен "0"?
Показать решение
- В A деление тоже находится внутри
try. В B внутриtryтолько преобразование, а деление перенесено вelse. - Когда
textравен"0", обе версии выбрасываютZeroDivisionError, который не ловит ни одинexcept, и программа падает. Но в B совершенно ясно, чтоexcept ValueErrorотносится только к преобразованию. - B лучше:
tryкак можно меньше. Такexceptне ловит по ошибке ошибки из других строк, и легко увидеть, что делает каждая часть.
Итоги
try— рискованный код.except— только если произошла подходящая ошибка.else— только если ошибки не было.finally— всегда.exceptиelseникогда не выполняются вместе.finallyвыполняется даже послеreturn, а также когда ошибка не поймана (и тогда программа падает после него).elseсохраняетtryмаленьким и сфокусированным.finally— место для очистки: закрытия файлов и соединений.