Python中的神坑return和finally

Posted qijunl1995-0816

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python中的神坑return和finally相关的知识,希望对你有一定的参考价值。

初识 return

返回一个值给调用者

def test():
    a = 2
    return a


s = test()
print(s)
#结果为2

如果return后面还有代码呢

def test():
    a = 2
    return a

    b = 3
    return b


s = test()
print(s)
#结果依然为2,显然第一个return后面的代码没有执行
return 代表整个函数返回, 函数调用算结束
return + try..finally, 会怎样呢?
def test():
    try:
        a = 2
        return a
    except Exception as e:
        print(hahaha)
    finally:
        print(finally)


s = test()
print(s)

结果:

finally
2

现在借助偷窥神器dis来一探究竟,挖掘最深处的秘密.

import dis
def test():
    try:
        a = 2
        return a
    except Exception as e:
        print(hahaha)
    finally:
        print(finally)


print(dis.dis(test))

结果显示:

 10           0 SETUP_FINALLY           56 (to 58)
              2 SETUP_EXCEPT             8 (to 12)

 11           4 LOAD_CONST               1 (2)
              6 STORE_FAST               0 (a)

 12           8 LOAD_FAST                0 (a)
             10 RETURN_VALUE

 13     >>   12 DUP_TOP
             14 LOAD_GLOBAL              0 (Exception)
             16 COMPARE_OP              10 (exception match)
             18 POP_JUMP_IF_FALSE       52
             20 POP_TOP
             22 STORE_FAST               1 (e)
             24 POP_TOP
             26 SETUP_FINALLY           14 (to 42)

 14          28 LOAD_GLOBAL              1 (print)
             30 LOAD_CONST               2 (hahaha)
             32 CALL_FUNCTION            1
             34 POP_TOP
             36 POP_BLOCK
             38 POP_EXCEPT
             40 LOAD_CONST               0 (None)
        >>   42 LOAD_CONST               0 (None)
             44 STORE_FAST               1 (e)
             46 DELETE_FAST              1 (e)
             48 END_FINALLY
             50 JUMP_FORWARD             2 (to 54)
        >>   52 END_FINALLY
        >>   54 POP_BLOCK
             56 LOAD_CONST               0 (None)

 16     >>   58 LOAD_GLOBAL              1 (print)
             60 LOAD_CONST               3 (finally)
             62 CALL_FUNCTION            1
             64 POP_TOP
             66 END_FINALLY
             68 LOAD_CONST               0 (None)
             70 RETURN_VALUE
None
1. 第一列是代码在文件的行号
2. 第二列字节码的偏移量
3. 字节码的名字
4. 参数
5. 字节码处理参数最终的结果

在字节码中可以看到, 依次是SETUP_FINALLYSETUP_EXCEPT, 这个对应的就是finallytry,虽然finallytry后面, 虽然我们通常帮他们看成一个整体, 但是他们在实际上却是分开的... 因为我们重点是finally, 所以就单单看SETUP_FINALLY

// ceval.c
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();
        }


// fameobject.c
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 就是调用下PyFrame_BlockSetup去创建一个Block, 然后为这个Block设置:

  1. b_type (opcode 也就是SETUP_FINALLY)

  2. b_level

  3. b_handler (INSTR_OFFSET() + oparg)

handler 可能比较难理解, 其实看刚才的 dis 输出就能看到是哪个, 就是 13 >> 31 LOAD_CONST 2 (‘finally‘), 这个箭头就是告诉我们跳转的位置的, 为什么会跳转到这句呢? 因为6 0 SETUP_FINALLY 28 (to 31)已经告诉我们将要跳转到31这个位置~~~

如果这个搞清楚了, 那就再来继续看 return, return对应的字节码是: RETURN_VALUE, 所以它对应的源码是:

// ceval.c
TARGET_NOARG(RETURN_VALUE)
        {
            retval = POP();
            why = WHY_RETURN;
            goto fast_block_end;
        }

原来我们以前理解的return是假return! 这个return并没有直接返回嘛, 而是将堆栈的值弹出来, 赋值个retval, 然后将why设置成WHY_RETURN, 接着就跑路了! 跑到一个叫fast_block_end;的地方~, 没办法, 为了揭穿真面目, 只好掘地三尺了:

while (why != WHY_NOT && f->f_iblock > 0) {
            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 */

在这需要回顾下刚才的一些知识, 刚才我们看了return的代码, 看到它将why设置成了 WHY_RETURN, 所以在这么一大串判断中, 它只是走了最后面的else, 动作也很简单, 就是将刚才return储存的值retvalpush压回栈, 同时将why转换成long再压回栈, 然后有设置了下why,接着就是屁颠屁颠去执行刚才SETUP_FINALLY设置的b_handler代码了~ 当这这段bhandler代码执行完, 就再通过END_FINALLY去做回该做的事, 而这里就是, return retval

总结:

所以, 我们应该能知道为什么当我们执行了return代码, 为什么finally的代码还会先执行了吧, 因为return的本质, 就是设置whyretval, 然后goto到一个大判断, 最后根据why的值去执行对应的操作! 所以可以说并不是真的实质性的返回. 希望我们往后再用到它们的时候, 别再掉坑里!

 

dis模块研究一下!

原文:https://segmentfault.com/a/1190000010701665

 
 

以上是关于Python中的神坑return和finally的主要内容,如果未能解决你的问题,请参考以下文章

LeetCode的神坑

十细说NULL导致的神坑,让人防不胜防

flask celery 的神坑

Js 类型方面的神坑

labview 调用 matlab script的神坑! Error 1050 occurred at LabVIEW

当半年不碰的webpack + vuejs项目重见天日时遇到的神坑!