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

yipeiwu_com6年前Python基础

前言

本文主要给大家介绍了在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中垃圾回收和del语句详解

Python中的垃圾回收算法是采用引用计数, 当一个对象的引用计数为0时, Python的垃圾回收机制就会将对象回收 a = "larry" b = a larry这个字符串对象,...

Python用list或dict字段模式读取文件的方法

前言 Python用于处理文本数据绝对是个利器,极为简单的读取、分割、过滤、转换支持,使得开发者不需要考虑繁杂的流文件处理过程(相对于JAVA来说的,嘻嘻)。博主自己工作中,一些复杂的文...

python SQLAlchemy 中的Engine详解

python SQLAlchemy 中的Engine详解

先看这张图,这是从官方网站扒下来的。 Engine 翻译过来就是引擎的意思,汽车通过引擎来驱动,而 SQLAlchemy 是通过 Engine 来驱动,Engine 维护了一个连接池(...

使用sklearn进行对数据标准化、归一化以及将数据还原的方法

使用sklearn进行对数据标准化、归一化以及将数据还原的方法

在对模型训练时,为了让模型尽快收敛,一件常做的事情就是对数据进行预处理。 这里通过使用sklearn.preprocess模块进行处理。 一、标准化和归一化的区别 归一化其实就是标准化的...

Python按钮的响应事件详解

Python按钮的响应事件详解

import sys from PyQt5 import QtWidgets from PyQt5.QtWidgets import QMainWindow from test im...