Что вернёт эта функция в Python?
- среда, 23 июня 2021 г. в 00:47:01
Всем привет! Сегодня хотел бы обсудить очень простой, но, на мой взгляд, интересный вопрос по 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
и его возвращаемое значение «более актуально». Однако, разумеется, на практике писать такой код я крайне не рекомендую ;-)