浅析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实现字符串格式化的方法小结
Feb 20 Python
python数据类型_字符串常用操作(详解)
May 30 Python
浅析Python中return和finally共同挖的坑
Aug 18 Python
Python3安装Scrapy的方法步骤
Nov 23 Python
python实现批量解析邮件并下载附件
Jun 19 Python
CentOS 7 安装python3.7.1的方法及注意事项
Nov 01 Python
python操作gitlab API过程解析
Dec 27 Python
Python 实现自动完成A4标签排版打印功能
Apr 09 Python
python自定义函数def的应用详解
Jun 03 Python
Python with语句用法原理详解
Jul 03 Python
Python实时监控网站浏览记录实现过程详解
Jul 14 Python
python和opencv构建运动检测器的实现
Mar 03 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
PHP 获取MySQL数据库里所有表的实现代码
2011/07/13 PHP
PHP中__autoload和Smarty冲突的简单解决方法
2016/04/08 PHP
详解new function(){}和function(){}() 区别分析
2008/03/22 Javascript
10个新的最有前途的JavaScript框架
2009/03/12 Javascript
jquery validate.js表单验证的基本用法入门
2010/05/13 Javascript
基于jQuery的输入框无值自动显示指定数据的实现代码
2011/01/24 Javascript
lyhucSelect基于Jquery的Select数据联动插件
2011/03/29 Javascript
jQuery新闻滚动插件 jquery.roller.js
2011/06/27 Javascript
使用闭包对setTimeout进行简单封装避免出错
2013/07/10 Javascript
js保留两位小数使用toFixed实现
2013/07/29 Javascript
Javascript表格翻页效果的具体实现
2013/10/05 Javascript
jsPDF导出pdf示例
2014/05/02 Javascript
分享一个自己动手写的jQuery分页插件
2014/08/28 Javascript
js+html5实现canvas绘制网页时钟的方法
2016/05/21 Javascript
js如何判断是否在iframe中及防止网页被别站用iframe嵌套
2017/01/11 Javascript
jQuery ajax实现省市县三级联动
2021/03/07 Javascript
微信小程序实现tab切换效果
2017/11/21 Javascript
vue+elementui实现点击table中的单元格触发事件--弹框
2020/07/18 Javascript
JavaScript ES 模块的使用
2020/11/12 Javascript
[03:21]【TI9纪实】Old Boys
2019/08/23 DOTA
Python实现的概率分布运算操作示例
2017/08/14 Python
python实现csv格式文件转为asc格式文件的方法
2018/03/23 Python
Python cookbook(字符串与文本)针对任意多的分隔符拆分字符串操作示例
2018/04/19 Python
Window 64位下python3.6.2环境搭建图文教程
2018/09/19 Python
python中的print()输出
2019/04/12 Python
Pytorch释放显存占用方式
2020/01/13 Python
python 使用递归回溯完美解决八皇后的问题
2020/02/26 Python
如何基于python对接钉钉并获取access_token
2020/04/21 Python
初婚未育未抱养证明
2014/01/12 职场文书
公司证明怎么写
2014/09/22 职场文书
公务员党的群众路线教育实践活动学习心得体会
2014/10/30 职场文书
2015年团支书工作总结
2015/04/03 职场文书
民事答辩状范本
2015/05/21 职场文书
详解CSS不受控制的position fixed
2021/05/25 HTML / CSS
Python实现socket库网络通信套接字
2021/06/04 Python
python解析json数据
2022/04/29 Python