try finally return python_Python :浅析 return 和 finally 共同挖的坑

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

来源:Lin_R

segmentfault.com/a/1190000010701665

初识 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后面的代码, 都是不会被执行的!

也正因为这个特性, 所以有种编码规范叫early return的编码规范就被倡导。

它的意思大概就是: 当条件已经满足返回时, 就马上返回

举个例子来说明:

deftest():

a=2

ifa>2:

result='more than'

else:

result='less than'

returnresult

s=test()

prints

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

deftest():

a=2

ifa>2:

return'more than'

else:

return'less than'

s=test()

prints

甚至是:

deftest():

a=2

ifa>2:

return'more than'

return'less than'

s=test()

prints

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

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

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

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

为了更加容易理解, 举个代码例子:

deftest():

a=2

ifa>2:

result='not 2'

else:

a+=2

ifa<2:

result='not 2'

else:

foriinrange(2):

print'test ~'

result='Target !'

returnresult

s=test()

prints

# 输出结果

test~

test~

Target!

代码简化优化版:

deftest():

a=2

ifa>2:

return'not 2'

a+=2

ifa<2:

return'not 2'

foriinrange(2):

print'test ~'

return'Target !'

s=test()

prints

# 输出结果

test~

test~

Target!

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

谈谈深坑

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

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

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

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

但事实真的这样吗? 通常这样问, 答案一般都不是 ~~

先来看看例子:

deftest():

try:

a=2

returna

except:

pass

finally:

print'finally'

s=test()

prints

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

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

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

第一列是代码在文件的行号

第二列字节码的偏移量

字节码的名字

参数

字节码处理参数最终的结果

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

//ceval.c

TARGET(SETUP_FINALLY)

_setup_finally:

{

/*NOTE:Ifyou add anynewblock-setup opcodes that

arenottry/except/finallyhandlers,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,inttype,inthandler,intlevel)

{

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;

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){

/*Peek at the currentblock.*/

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 theblock.*/

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 exceptiondata

available to thehandler,

soaprogram can emulate the

Python mainloop.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;

}

}/*unwindstack*/

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

结论

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

责任编辑:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值