浅析Python中return和finally共同挖的坑
前言
本文主要给大家介绍了在Python中return和finally共同存在的坑,以及填坑经验,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧。
初识return
相信每一个用过Python函数的童鞋,肯定会用过return语句,return顾名思义,就是用来返回值给调用者,例如:
deftest(): a=2 returna s=test() prints #输出结果 2
对于上面的结果,相信大家都不会感到意外,那么加大点难度,如果在return语句还有代码呢?那句代码会怎样呢?
deftest(): a=2 returna s=3 prints s=test() prints #结果是什么?
老司机肯定一眼就能看出结果,但是对于尚在入门或者对return不很了解的童鞋,可能就会懵逼了~后面的两句代码是否会被执行?
答案是:不会执行
return正如它的名字那样,当执行这句代码,整个函数都会返回,整个调用就算结束了~所以在return后面的代码,都是不会被执行的!
也正因为这个特性,所以有种编码规范叫earlyreturn的编码规范就被倡导
它的意思大概就是:当条件已经满足返回时,就马上返回
举个例子来说明:
deftest(): a=2 ifa>2: result='morethan' else: result='lessthan' returnresult s=test() prints
上面的代码应该比较容易理解,就是根据a的值,来决定返回的result是什么.这样的编码相信也是大部分童鞋喜欢用的,因为这样比较符合我们直觉,然而,这样写似乎有点浪费,因为当第一个判断结束了,如果结果为真,就应该返回morethan,然后结束函数,否则肯定就是返回lessthan,所以我们可以把代码调整成这样:
deftest(): a=2 ifa>2: return'morethan' else: return'lessthan' s=test() prints
甚至是:
deftest(): a=2 ifa>2: return'morethan' return'lessthan' s=test() prints
结果都是和第一个写法是一样的!第一次看到这样写法的童鞋,可能会觉得比较难以接受,甚至觉得可读性很差,但是其实这样的写法,我觉得反而会稍微好点.因为:
- 运行的代码数少了,调用方能更快得到结果
- 有利于减少嵌套的层数,便于理解.
对于第2点在这需要解释下,很多时候我们写得代码,嵌套很深,都是因为if/else的锅,因为嵌套的if/else比较多,所以导致一堆代码都嵌套得比较深,这样对于其他小伙伴,简直就是灾难,因为他们很可能在阅读这部分代码时,就忘了前面的逻辑....
为了更加容易理解,举个代码例子:
deftest(): a=2 ifa>2: result='not2' else: a+=2 ifa<2: result='not2' else: foriinrange(2): print'test~' result='Target!' returnresult s=test() prints #输出结果 test~ test~ Target!
代码简化优化版:
deftest(): a=2 ifa>2: return'not2' a+=2 ifa<2: return'not2' foriinrange(2): print'test~' return'Target!' s=test() prints #输出结果 test~ test~ Target!
这样对比这来看,应该能更好地理解为什么说earlyreturn能够减少嵌套的层数吧~有疑问欢迎留言讨论~
谈谈深坑
刚才花了比较长的篇幅去介绍return,相信看到这里,对于return应该有比较基本的理解了!所以来聊聊更加迷惑的话题:
当return遇上try..finally,会怎样呢?
如果刚才有认真看的话,会注意到一句话,就是:
return代表整个函数返回,函数调用算结束
但事实真的这样吗?通常这样问,答案一般都不是~~
先来看看例子:
deftest(): try: a=2 returna except: pass finally: print'finally' s=test() prints
可以猜猜这句printa会不会打印?相信很多童鞋都想了一会,然后说不会~然而这个答案是错的,真正的输出是:
finally 2
有木有觉得仿佛看见了新大陆,在一开始的例子中,return后面的语句没有被执行,但是在这里,相隔那么远,却依旧没有忘记,这或许就是"真爱"吧!
然而就是因为这种"真爱",总是会让一堆新老司机掉坑里..然后还不知道为毛..
为了避免它们再继续借用打着"真爱"的幌子,欺负我们,让我们一起来揭开这"真爱"的真面目!
于是,我们得借助偷窥神器:dis,想想都有点小兴奋!
importdis deftest(): try: a=2 returna except: pass finally: print'finally' printdis.dis(test)
输出比较长,单独写:
#输出结果 60SETUP_FINALLY28(to31) 3SETUP_EXCEPT14(to20) 76LOAD_CONST1(2) 9STORE_FAST0(a) 812LOAD_FAST0(a) 15RETURN_VALUE 16POP_BLOCK 17JUMP_FORWARD7(to27) 9>>20POP_TOP 21POP_TOP 22POP_TOP 1023JUMP_FORWARD1(to27) 26END_FINALLY >>27POP_BLOCK 28LOAD_CONST0(None) 13>>31LOAD_CONST2('finally') 34PRINT_ITEM 35PRINT_NEWLINE 36END_FINALLY 37LOAD_CONST0(None) 40RETURN_VALUE
这边简单说着这些列所代表的意思:
1.第一列是代码在文件的行号
2.第二列字节码的偏移量
3.字节码的名字
4.参数
5.字节码处理参数最终的结果
在字节码中可以看到,依次是SETUP_FINALLY和SETUP_EXCEPT,这个对应的就是finally和try,虽然finally在try后面,虽然我们通常帮他们看成一个整体,但是他们在实际上却是分开的...因为我们重点是finally,所以就单单看SETUP_FINALLY
//ceval.c TARGET(SETUP_FINALLY) _setup_finally: { /*NOTE:Ifyouaddanynewblock-setupopcodesthat arenottry/except/finallyhandlers,youmayneed toupdatethePyGen_NeedsFinalizing()function. */ PyFrame_BlockSetup(f,opcode,INSTR_OFFSET()+oparg, STACK_LEVEL()); DISPATCH(); } //fameobject.c void PyFrame_BlockSetup(PyFrameObject*f,inttype,inthandler,intlevel) { PyTryBlock*b; if(f->f_iblock>=CO_MAXBLOCKS) Py_FatalError("XXXblockstackoverflow"); 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>>31LOAD_CONST2('finally'),这个箭头就是告诉我们跳转的位置的,为什么会跳转到这句呢?因为60SETUP_FINALLY28(to31)已经告诉我们将要跳转到31这个位置~~~
如果这个搞清楚了,那就再来继续看return,return对应的字节码是:RETURN_VALUE,所以它对应的源码是:
//ceval.c TARGET_NOARG(RETURN_VALUE) { retval=POP(); why=WHY_RETURN; gotofast_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){ /*Peekatthecurrentblock.*/ 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; } /*Nowwehavetopoptheblock.*/ 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); } /*Maketherawexceptiondata availabletothehandler, soaprogramcanemulatethe Pythonmainloop.Don'tdo thisfor'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; } }/*unwindstack*/
在这需要回顾下刚才的一些知识,刚才我们看了return的代码,看到它将why设置成了WHY_RETURN,所以在这么一大串判断中,它只是走了最后面的else,动作也很简单,就是将刚才return储存的值retval再push压回栈,同时将why转换成long再压回栈,然后有设置了下why,接着就是屁颠屁颠去执行刚才SETUP_FINALLY设置的b_handler代码了~当这这段bhandler代码执行完,就再通过END_FINALLY去做回该做的事,而这里就是,returnretval
结论
所以,我们应该能知道为什么当我们执行了return代码,为什么finally的代码还会先执行了吧,因为return的本质,就是设置why和retval,然后goto到一个大判断,最后根据why的值去执行对应的操作!所以可以说并不是真的实质性的返回.希望我们往后再用到它们的时候,别再掉坑里!
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。