Исключения 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 = 21ValueError
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 positive

finally выполняется до того, как функция действительно возвращает значение, — даже когда внутри 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 — место для очистки: закрытия файлов и соединений.

Официальная документация