浅析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根据出生日期获得年龄的方法
Mar 31 Python
详解supervisor使用教程
Nov 21 Python
python+django加载静态网页模板解析
Dec 12 Python
Python实现翻转数组功能示例
Jan 12 Python
关于Django ForeignKey 反向查询中filter和_set的效率对比详解
Dec 15 Python
PyQt5实现从主窗口打开子窗口的方法
Jun 19 Python
如何利用Python开发一个简单的猜数字游戏
Sep 22 Python
Django中自定义模型管理器(Manager)及方法
Sep 23 Python
Python字符串格式化f-string多种功能实现
May 07 Python
python之json文件转xml文件案例讲解
Aug 07 Python
python读取mnist数据集方法案例详解
Sep 04 Python
python实现会员信息管理系统(List)
Mar 18 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
一个高ai的分页函数和一个url函数
2006/10/09 PHP
PHP zlib扩展实现页面GZIP压缩输出
2010/06/17 PHP
php中用数组的方法设置cookies
2011/04/21 PHP
php中mysql模块部分功能的简单封装
2011/09/30 PHP
PHP创建单例后台进程的方法示例
2017/05/23 PHP
PHP使用PDO、mysqli扩展实现与数据库交互操作详解
2019/07/20 PHP
YII2框架使用控制台命令的方法分析
2020/03/18 PHP
JQuery获取元素文档大小、偏移和位置和滚动条位置的方法集合
2010/01/12 Javascript
解决jquery中美元符号命名冲突问题
2014/01/08 Javascript
Javascript中call与apply的学习笔记
2014/09/22 Javascript
jQuery数据类型小结(14个)
2016/01/08 Javascript
如何解决hover在ie6中的兼容性问题
2016/12/15 Javascript
Vue.js开发环境快速搭建教程
2017/03/17 Javascript
JS实现点击链接切换显示隐藏内容的方法
2017/10/19 Javascript
mocha的时序规则讲解
2019/02/16 Javascript
Node.js开发之套接字(socket)编程入门示例
2019/11/05 Javascript
js实现特别简单的钟表效果
2020/09/14 Javascript
[01:14]3.19DOTA2发布会 三代刀塔人第二代
2014/03/25 DOTA
[56:12]LGD vs Optic Supermajor小组赛D组胜者组决赛 BO3 第一场 6.3
2018/06/04 DOTA
Python中字典的基本知识初步介绍
2015/05/21 Python
spyder常用快捷键(分享)
2017/07/19 Python
将字典转换为DataFrame并进行频次统计的方法
2018/04/08 Python
Django rest framework实现分页的示例
2018/05/24 Python
Python读取mat文件,并保存为pickle格式的方法
2018/10/23 Python
python psutil监控进程实例
2019/12/17 Python
Python导入数值型Excel数据并生成矩阵操作
2020/06/09 Python
基于python和flask实现http接口过程解析
2020/06/15 Python
python3 googletrans超时报错问题及翻译工具优化方案 附源码
2020/12/23 Python
Python 爬取淘宝商品信息栏目的实现
2021/02/06 Python
英国骑行、跑步、游泳、铁人三项运动装备专卖店:Wiggle
2016/08/23 全球购物
Supersmart英国:欧洲市场首批食品补充剂供应商之一
2018/05/05 全球购物
Happy Socks英国官网:购买五颜六色的袜子
2020/11/03 全球购物
俄罗斯电动工具和设备购物网站:Vseinstrumenti.ru
2020/11/12 全球购物
体育教师工作总结的自我评价
2013/10/10 职场文书
销售经理岗位职责
2014/03/16 职场文书
干部作风建设年活动剖析材料
2014/10/23 职场文书