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


Posted in Python onAugust 18, 2017

前言

本文主要给大家介绍了在Python中return和finally共同存在的坑,以及填坑经验,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧。

初识 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_FINALLY 和 SETUP_EXCEPT, 这个对应的就是finally和try,虽然finally在try后面, 虽然我们通常帮他们看成一个整体, 但是他们在实际上却是分开的... 因为我们重点是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储存的值retval再push压回栈, 同时将why转换成long再压回栈, 然后有设置了下why,接着就是屁颠屁颠去执行刚才SETUP_FINALLY设置的b_handler代码了~ 当这这段bhandler代码执行完, 就再通过END_FINALLY去做回该做的事, 而这里就是, return retval

结论

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

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Python 相关文章推荐
python远程登录代码
Apr 29 Python
用Python的Tornado框架结合memcached页面改善博客性能
Apr 24 Python
python实现的简单FTP上传下载文件实例
Jun 30 Python
django如何连接已存在数据的数据库
Aug 14 Python
网易2016研发工程师编程题 奖学金(python)
Jun 19 Python
python障碍式期权定价公式
Jul 19 Python
18个Python脚本可加速你的编码速度(提示和技巧)
Oct 17 Python
keras实现theano和tensorflow训练的模型相互转换
Jun 19 Python
python对execl 处理操作代码
Jun 22 Python
如何使用Python处理HDF格式数据及可视化问题
Jun 24 Python
Python SMTP发送电子邮件的示例
Sep 23 Python
Python+kivy BoxLayout布局示例代码详解
Dec 28 Python
Python多进程multiprocessing用法实例分析
Aug 18 #Python
利用python求相邻数的方法示例
Aug 18 #Python
Python3.5编程实现修改IIS WEB.CONFIG的方法示例
Aug 18 #Python
Python 中 Virtualenv 和 pip 的简单用法详解
Aug 18 #Python
Python3编程实现获取阿里云ECS实例及监控的方法
Aug 18 #Python
浅谈django开发者模式中的autoreload是如何实现的
Aug 18 #Python
Python绑定方法与非绑定方法详解
Aug 18 #Python
You might like
法兰绒滤网冲泡
2021/03/03 冲泡冲煮
BBS(php &amp; mysql)完整版(八)
2006/10/09 PHP
PHP 多维数组排序实现代码
2009/08/05 PHP
php中inlcude()性能对比详解
2012/09/16 PHP
php验证是否是md5编码的简单代码
2014/04/01 PHP
PHP使用Alexa API获取网站的Alexa排名例子
2014/06/12 PHP
高质量PHP代码的50个实用技巧必备(上)
2016/01/22 PHP
php可变长参数处理函数详解
2017/02/22 PHP
php实现简单加入购物车功能
2017/03/07 PHP
PHP进阶学习之命名空间基本用法分析
2019/06/18 PHP
js 数组克隆方法 小结
2010/03/20 Javascript
js 判断checkbox是否选中的实现代码
2010/11/23 Javascript
javascript工具库代码
2012/03/29 Javascript
JS下拉缓冲菜单示例代码
2013/08/30 Javascript
jQuery实现简易的天天爱消除小游戏
2015/10/16 Javascript
JavaScript与java语言有什么不同
2016/09/22 Javascript
小程序自定义日历效果
2018/12/29 Javascript
JS模拟实现京东快递单号查询
2020/11/30 Javascript
python计算圆周率pi的方法
2015/07/11 Python
Python 列表(List) 的三种遍历方法实例 详解
2017/04/15 Python
Python3解决棋盘覆盖问题的方法示例
2017/12/07 Python
Python异常处理操作实例详解
2018/08/28 Python
详解Python装饰器
2019/03/25 Python
Python中使用pypdf2合并、分割、加密pdf文件的代码详解
2019/05/21 Python
Python 实现取多维数组第n维的前几位
2019/11/26 Python
使用python采集Excel表中某一格数据
2020/05/14 Python
Matplotlib中%matplotlib inline如何使用
2020/07/28 Python
Python selenium爬取微信公众号文章代码详解
2020/08/12 Python
英国复古和经典球衣网站:Vintage Football Shirts
2018/10/05 全球购物
KOHLER科勒美国官网:国际著名卫浴橱柜领先品牌
2020/06/27 全球购物
酒会邀请函
2015/01/31 职场文书
销售内勤岗位职责范本
2015/04/13 职场文书
村官2015年度工作总结
2015/10/14 职场文书
创业计划书之儿童理发店
2019/09/27 职场文书
Keras多线程机制与flask多线程冲突的解决方案
2021/05/28 Python
Python简易开发之制作计算器
2022/04/28 Python