资讯专栏INFORMATION COLUMN

Python: 浅析 return 和 finally 共同挖的坑

wangzy2019 / 3452人阅读

摘要:初识相信每一个用过函数的童鞋肯定会用过语句顾名思义就是用来返回值给调用者例如输出结果对于上面的结果相信大家都不会感到意外那么加大点难度如果在语句还有代码呢那句代码会怎样呢结果是什么老司机肯定一眼就能看出结果但是对于尚在入门或者对不很了解的童

初识 return

  相信每一个用过Python函数的童鞋, 肯定会用过return语句, return顾名思义, 就是用来返回值给调用者, 例如:

def test():
    a = 2
    return a

s = test()
print s

# 输出结果
2

对于上面的结果, 相信大家都不会感到意外, 那么加大点难度, 如果在return语句还有代码呢? 那句代码会怎样呢?

def test():
    a = 2
    return a
    s = 3
    print s

s = test()
print s

# 结果是什么?

老司机肯定一眼就能看出结果, 但是对于尚在入门或者对return不很了解的童鞋, 可能就会懵逼了~ 后面的两句代码是否会被执行?
答案是: 不会执行

return正如它的名字那样, 当执行这句代码, 整个函数都会返回, 整个调用就算结束了~ 所以在return后面的代码, 都是不会被执行的!
  也正因为这个特性, 所以有种编码规范叫early return的编码规范就被倡导

它的意思大概就是: 当条件已经满足返回时, 就马上返回
举个例子来说明:

def test():
    a = 2
    if a > 2:
        result = "more than"
    else:
        result = "less than"
    return result

s = test()
print s

上面的代码应该比较容易理解, 就是根据a的值, 来决定返回的result是什么. 这样的编码相信也是大部分童鞋喜欢用的, 因为这样比较符合我们直觉, 然而, 这样写似乎有点浪费, 因为当第一个判断结束了, 如果结果为真, 就应该返回more than, 然后结束函数, 否则肯定就是返回less than, 所以我们可以把代码调整成这样:

def test():
    a = 2
    if a > 2:
        return "more than"
    else:
        return "less than"

s = test()
print s

甚至是:

def test():
    a = 2
    if a > 2:
        return "more than"
    return "less than"

s = test()
print s

结果都是和第一个写法是一样的! 第一次看到这样写法的童鞋, 可能会觉得比较难以接受, 甚至觉得可读性很差, 但是其实这样的写法, 我觉得反而会稍微好点. 因为:

运行的代码数少了, 调用方能更快得到结果

有利于减少嵌套的层数, 便于理解.

对于第2点在这需要解释下, 很多时候我们写得代码, 嵌套很深, 都是因为if/else的锅, 因为嵌套的if/else 比较多, 所以导致一堆代码都嵌套得比较深, 这样对于其他小伙伴, 简直就是灾难, 因为他们很可能在阅读这部分代码时, 就忘了前面的逻辑....
为了更加容易理解, 举个代码例子:

def test():
    a = 2
    if a > 2:
        result = "not 2"
    else:
        a += 2
        if a < 2:
            result = "not 2"
        else:
            for i in range(2):
                print "test ~"
            result = "Target !"
    return result

s = test()
print s

# 输出结果
test ~
test ~
Target !

代码简化优化版:

def test():
    a = 2
    if a > 2:
        return "not 2"
    
    a += 2
    if a < 2:
        return "not 2"
    
    for i in range(2):
        print "test ~"

    return "Target !"

s = test()
print s

# 输出结果
test ~
test ~
Target !

这样对比这来看, 应该能更好地理解为什么说early return能够减少嵌套的层数吧~ 有疑问欢迎留言讨论~

谈谈深坑

刚才花了比较长的篇幅去介绍return, 相信看到这里, 对于return应该有比较基本的理解了! 所以来聊聊更加迷惑的话题:

当 return 遇上 try..finally, 会怎样呢?

如果刚才有认真看的话, 会注意到一句话, 就是:

return 代表整个函数返回, 函数调用算结束

但事实真的这样吗? 通常这样问, 答案一般都不是 ~~
先来看看例子:

def test():
    try:
        a = 2
        return a
    except:
        pass

    finally:
        print "finally"

s = test()
print s

可以猜猜这句print a会不会打印? 相信很多童鞋都想了一会, 然后说不会~ 然而这个答案是错的, 真正的输出是:

finally
2

有木有觉得仿佛看见了新大陆, 在一开始的例子中, return后面的语句没有被执行, 但是在这里, 相隔那么远, 却依旧没有忘记, 这或许就是"真爱"吧!

然而就是因为这种"真爱", 总是会让一堆新老司机掉坑里..然后还不知道为毛..

为了避免它们再继续借用打着"真爱"的幌子, 欺负我们, 让我们一起来揭开这"真爱"的真面目!

于是, 我们得借助偷窥神器: dis, 想想都有点小兴奋!

import dis
def test():
    try:
        a = 2
        return a
    except:
        pass

    finally:
        print "finally"

print dis.dis(test)

输出比较长, 多带带写:

# 输出结果
  6           0 SETUP_FINALLY           28 (to 31)
              3 SETUP_EXCEPT            14 (to 20)

  7           6 LOAD_CONST               1 (2)
              9 STORE_FAST               0 (a)

  8          12 LOAD_FAST                0 (a)
             15 RETURN_VALUE        
             16 POP_BLOCK           
             17 JUMP_FORWARD             7 (to 27)

  9     >>   20 POP_TOP             
             21 POP_TOP             
             22 POP_TOP             

 10          23 JUMP_FORWARD             1 (to 27)
             26 END_FINALLY         
        >>   27 POP_BLOCK           
             28 LOAD_CONST               0 (None)

 13     >>   31 LOAD_CONST               2 ("finally")
             34 PRINT_ITEM          
             35 PRINT_NEWLINE       
             36 END_FINALLY         
             37 LOAD_CONST               0 (None)
             40 RETURN_VALUE  

这边简单说着这些列所代表的意思:

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设置:

b_type (opcode 也就是SETUP_FINALLY)

b_level

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的值去执行对应的操作! 所以可以说并不是真的实质性的返回. 希望我们往后再用到它们的时候, 别再掉坑里!

欢迎各位大神指点交流, QQ讨论群: 258498217
转载请注明来源: https://segmentfault.com/a/11...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/40740.html

相关文章

  • 【Flutter组件】仿微信左滑删除组件与透明背景时的处理

    摘要:本文用到的组件这是一个简单版仿微信的左滑删除的组件左滑删除组件地址仓库地址问题在上,使用仿微信样式的左滑删除组件时如果这一行的背景色是透明的,就会出现如下问题透明的下可以看到删除和编辑按钮,我们就需要处理一下。 本文用到的组件: 这是一个简单版仿微信的左滑删除的组件: # 左滑删除组件 left_scroll_actions: any pub地址:https://pub.dartlan...

    mingzhong 评论0 收藏0
  • 浅析Otto框架,并与EventBus对比

    摘要:它有发布者,订阅者这两个主要对象。的最佳实践就是通过反射牺牲了微小的性能,同时极大的降低了程序的耦合度。官网和应用场景框架的主要功能是帮助我们来降低多个组件通信之间的耦合度的解耦。 前两天在公众号里发了一篇有关EventBus的文章《玩转EventBus,详解其使用》,有读者和开发者反馈说没有OTTO好用。确实是,各有优缺点吧,那今天就有必要再讲一下Otto事件框架。 OTTO是Squ...

    EsgynChina 评论0 收藏0
  • JAVA HashMap源码浅析

    摘要:类的属性和构造函数二的初始化构造方法这是的构造函数之一,其他构造函数都引用这个构造函数进行初始化。在构造函数中不会对数组进行初始化,只有在等操作方法内会进行判断是否要初始化或扩容。其作用是保证的效率。 引言 HashMap在键值对存储中被经常使用,那么它到底是如何实现键值存储的呢? 一 Entry Entry是Map接口中的一个内部接口,它是实现键值对存储关键。在HashMap中,有E...

    xuxueli 评论0 收藏0
  • 一起学设计模式 - 单例模式

    摘要:懒汉非线程安全,需要用一定的风骚操作控制,装逼失败有可能导致看一周的海绵宝宝饿汉天生线程安全,的时候就已经实例化好,该操作过于风骚会造成资源浪费单例注册表初始化的时候,默认单例用的就是该方式特点私有构造方法,只能有一个实例。 单例设计模式(Singleton Pattern)是最简单且常见的设计模式之一,主要作用是提供一个全局访问且只实例化一次的对象,避免多实例对象的情况下引起逻辑性错...

    Keven 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<