Всем привет! Сегодня хотел бы обсудить очень простой, но, на мой взгляд, интересный вопрос по 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

Опуская излишние подробности, этот код ведёт себя так:

  1. Устанавливаем блок try и указываем, где находится finally.

  2. Загружаем константу и возвращаем значение.

  3. Выполняем некоторые вспомогательные действия.

  4. Наконец идёт блок finally (адреса 11, 14, 15), в которым мы снова загружаем константу и делаем ret.

При исполнении кода сначала отрабатывает часть в блоке try, а затем выполняется код из finally. Что же происходит, когда мы снова вызовем RETURN_VALUE? Правильно, мы просто перезапишем возвращаемое значение retval на новое. Ну а функция, разумеется, вернёт 2.

Как видите, даже несмотря кажущуюся неочевидность, Python, на мой взгляд, ведёт себя максимально понятно и логично: блок finally выполняется после блока try и его возвращаемое значение «более актуально». Однако, разумеется, на практике писать такой код я крайне не рекомендую ;-)