Epitome:
- return doesn’t really return, it pushes value to stack, then jumps to finally, which was set up by
SETUP_FINALLY
before. - after executing finally,
END_FINALLY
returns.
Let’s be forthright, what will the following code print?
def func():
try:
100/0
except ZeroDivisionError as e:
print 'Zero cannot be divisor'
return
finally:
print 'finally'
if __name__ == "__main__":
func()
The answer is:
Zero cannot be divisor
finally
It seems that finally
executes after the return
statement. Is it a truth? We’d better change the code a little to penetrate the result.
#!/usr/bin/env python2
# coding=utf-8
from dis import dis
def func():
try:
100/0
except ZeroDivisionError as e:
print 'Zero cannot be divisor'
return
finally:
print 'finally'
if __name__ == "__main__":
dis(func)
Next point, we can see,
6 0 SETUP_FINALLY 44 (to 47)
3 SETUP_EXCEPT 12 (to 18)
7 6 LOAD_CONST 1 (100)
9 LOAD_CONST 2 (0)
12 BINARY_DIVIDE
13 POP_TOP
14 POP_BLOCK
15 JUMP_FORWARD 25 (to 43)
8 >> 18 DUP_TOP
19 LOAD_GLOBAL 0 (ZeroDivisionError)
22 COMPARE_OP 10 (exception match)
25 POP_JUMP_IF_FALSE 42
28 POP_TOP
29 STORE_FAST 0 (e)
32 POP_TOP
9 33 LOAD_CONST 3 ('Zero cannot be divisor')
36 PRINT_ITEM
37 PRINT_NEWLINE
10 38 LOAD_CONST 0 (None)
41 RETURN_VALUE
>> 42 END_FINALLY
>> 43 POP_BLOCK
44 LOAD_CONST 0 (None)
12 >> 47 LOAD_CONST 4 ('finally')
50 PRINT_ITEM
51 PRINT_NEWLINE
52 END_FINALLY
53 LOAD_CONST 0 (None)
56 RETURN_VALUE
For those who didn’t use dis
module before:
The dis module supports the analysis of CPython bytecode by disassembling it. According to the above output, first column is line number, the second is offset of bytecode, followed by the name of bytecode. The forth column presents parameter and the one in parenthesis is the result after bytecode’s processing parameter.
As we can see, after RETURN_VALUE
in line 10, the END_FINALLY
would be executed. That doesn’t meet our expectation. What’s more weird, there is another RETURN_VALUE
in line 12 after END_FINALLY
. Since we get here, the source code is the only thing that we can ask for help.
// ceval.c
TARGET_NOARG(RETURN_VALUE)
{
retval = POP();
why = WHY_RETURN;
goto fast_block_end;
}
fast_block_end:
while (why != WHY_NOT && f->f_iblock > 0) {
/* Peek at the current block. */
PyTryBlock *b = &f->f_blockstack[f->f_iblock - 1];
assert(why != WHY_YIELD);
if (b->b_type == SETUP_LOOP && why == WHY_CONTINUE) {
why = WHY_NOT;
JUMPTO(PyInt_AS_LONG(retval));
Py_DECREF(retval);
break;
}
/* Now we have to pop the block. */
f->f_iblock--;
while (STACK_LEVEL() > b->b_level) {
v = POP();
Py_XDECREF(v);
}
if (b->b_type == SETUP_LOOP && why == WHY_BREAK) {
why = WHY_NOT;
JUMPTO(b->b_handler);
break;
}
if (b->b_type == SETUP_FINALLY ||
(b->b_type == SETUP_EXCEPT &&
why == WHY_EXCEPTION) ||
b->b_type == SETUP_WITH) {
if (why == WHY_EXCEPTION) {
PyObject *exc, *val, *tb;
PyErr_Fetch(&exc, &val, &tb);
if (val == NULL) {
val = Py_None;
Py_INCREF(val);
}
/* Make the raw exception data
available to the handler,
so a program can emulate the
Python main loop. Don't do
this for 'finally'. */
if (b->b_type == SETUP_EXCEPT ||
b->b_type == SETUP_WITH) {
PyErr_NormalizeException(
&exc, &val, &tb);
set_exc_info(tstate,
exc, val, tb);
}
if (tb == NULL) {
Py_INCREF(Py_None);
PUSH(Py_None);
} else
PUSH(tb);
PUSH(val);
PUSH(exc);
}
else {
if (why & (WHY_RETURN | WHY_CONTINUE))
PUSH(retval);
v = PyInt_FromLong((long)why);
PUSH(v);
}
why = WHY_NOT;
JUMPTO(b->b_handler);
break;
}
} /* unwind stack */
/* End the loop if we still have an error (or return) */
if (why != WHY_NOT)
break;
READ_TIMESTAMP(loop1);
} /* main loop */
assert(why != WHY_YIELD);
/* Pop remaining stack entries. */
while (!EMPTY()) {
v = POP();
Py_XDECREF(v);
}
if (why != WHY_RETURN)
retval = NULL;
RETURN_VALUE
does not return really, it simply assigns popped value from stack to retval
, set why
to WHY_RETURN
, then go to fast_block_end
. This pathetic scapegoat would push retval
from RETURN_VALUE
into stack, convert why
to Long
and push it into stack too, then jump to b->b_handler
. Well, what’s b_handler
pointing to?
// ceval.c
// SETUP_FINALLY
TARGET_WITH_IMPL(SETUP_LOOP, _setup_finally)
TARGET_WITH_IMPL(SETUP_EXCEPT, _setup_finally)
TARGET(SETUP_FINALLY)
_setup_finally:
{
/* NOTE: If you add any new block-setup opcodes that
are not try/except/finally handlers, you may need
to update the PyGen_NeedsFinalizing() function.
*/
PyFrame_BlockSetup(f, opcode, INSTR_OFFSET() + oparg,
STACK_LEVEL());
DISPATCH();
}
// END_FINALLY
PREDICTED(END_FINALLY);
TARGET_NOARG(END_FINALLY)
{
v = POP();
if (PyInt_Check(v)) {
why = (enum why_code) PyInt_AS_LONG(v);
assert(why != WHY_YIELD);
if (why == WHY_RETURN ||
why == WHY_CONTINUE)
retval = POP();
}
else if (PyExceptionClass_Check(v) ||
PyString_Check(v)) {
w = POP();
u = POP();
PyErr_Restore(v, w, u);
why = WHY_RERAISE;
break;
}
else if (v != Py_None) {
PyErr_SetString(PyExc_SystemError,
"'finally' pops bad exception");
why = WHY_EXCEPTION;
}
Py_DECREF(v);
break;
}
// frameobject.c
/* Block management */
void
PyFrame_BlockSetup(PyFrameObject *f, int type, int handler, int level)
{
PyTryBlock *b;
if (f->f_iblock >= CO_MAXBLOCKS)
Py_FatalError("XXX block stack overflow");
b = &f->f_blockstack[f->f_iblock++];
b->b_type = type;
b->b_level = level;
b->b_handler = handler;
}
SETUP_FINALLY
calls PyFrame_BlockSetup
to setup a block with type, level, handler. From the output of dis
(notice the arrows before END_FINALLY
), we can guess this handler is END_FINALLY
. END_FINALLY
gets retval
and why
, pushed by RETURN_VALUE
, from stack, and returns.
Now we can articulate all the scratch:
- return doesn’t really return, it pushs value to stack, then jumps to finally, which was set up by
SETUP_FINALLY
before. - after executing finally,
END_FINALLY
returns.
Reference