
Всем привет! Сегодня хотел бы обсудить очень простой, но, на мой взгляд, интересный вопрос по Python и его внутреннему устройству. Как вы думаете, что вернёт эта функция:
def foo(): try: return 1 finally: return 2
Если вам интересно, что получится в результате и как это работает, добро пожаловать под кат.
Прежде чем давать ответ, давайте разберёмся, что происходит. Для начала рассмотрим самую простую функцию:
def foo(): return 1
Распечатаем её байт код:
import dis dis.dis(foo)
Мы увидим следующий вывод:
2 0 LOAD_CONST 1 (1) 3 RETURN_VALUE
Рассмотрим по шагам:
LOAD_CONSTзагружает константу (в нашем случае1) и кладет её на вершину стека.RETURN_VALUEвозвращает в вызывающий код значение с вершины стека.
Подробнее о байт-коде Python и его командах рассказано тут.
Что же скрывается за мифической фразой «возвращает в вызывающий код»? На самом деле, никакой магии не происходит. Если обратиться к исходному коду CPython, то можно увидеть следующие строчки:
switch (opcode) { ... case RETURN_VALUE: { retval = POP(); why = WHY_RETURN; goto fast_block_end; } ... }
Как видите, всё очень просто и понятно: мы сохраняем в переменной retval значение с вершины стека и переходим к выходу из текущего блока.
Теперь мы готовы посмотреть на байт-код функции из нашего исходного примера. Как же она устроена внутри?
2 0 SETUP_FINALLY 8 (to 11) 3 3 LOAD_CONST 1 (1) 6 RETURN_VALUE 7 POP_BLOCK 8 LOAD_CONST 0 (None) 5 >> 11 LOAD_CONST 2 (2) 14 RETURN_VALUE 15 END_FINALLY
Опуская излишние подробности, этот код ведёт себя так:
Устанавливаем блок
tryи указываем, где находитсяfinally.Загружаем константу и возвращаем значение.
Выполняем некоторые вспомогательные действия.
Наконец идёт блок
finally(адреса 11, 14, 15), в которым мы снова загружаем константу и делаемret.
При исполнении кода сначала отрабатывает часть в блоке try, а затем выполняется код из finally. Что же происходит, когда мы снова вызовем RETURN_VALUE? Правильно, мы просто перезапишем возвращаемое значение retval на новое. Ну а функция, разумеется, вернёт 2.
Как видите, даже несмотря кажущуюся неочевидность, Python, на мой взгляд, ведёт себя максимально понятно и логично: блок finally выполняется после блока try и его возвращаемое значение «более актуально». Однако, разумеется, на практике писать такой код я крайне не рекомендую ;-)
