【问题标题】:Python docs have misleading explanation of return in finallyPython 文档最终对 return 有误导性的解释
【发布时间】:2020-01-08 05:10:49
【问题描述】:

我正在阅读 python 文档以改进我的核心 python,我正在阅读关于 errors and exceptions

在文档中它说

如果 finally 子句包含 return 语句,则 finally 子句的 return 语句将在 try 子句中的 return 语句之前执行,而不是

下面还提供了这个例子:

def bool_return():
    try:
        return True
    finally:
        return False

bool_return()

现在看这个例子,上面的陈述似乎很直截了当,但如果你稍微修改一下这个例子,让它看起来像这样:

def bool_return():
    try:
        return print("foo")
    finally:
        return False

bool_return()

现在,如果你运行它,你会看到foo 将被打印出来,并且 False 将被返回。现在文档说 finally 子句的 return 将执行 before,而 而不是, try 子句的 return 语句。如果是这样,那为什么我可以看到正在打印的 foo?

我用pycharm调试了这个sn-p,它显示首先执行try子句的return语句并打印字符串,然后由于return语句而返回None的输出,以及return语句在 finally 子句中将稍后执行,这是程序的最后一次返回,因此该函数会覆盖先前的返回并返回 False

我的问题是:

1) 为什么doc说finally子句的return语句在之前执行?

2) 为什么doc说finally子句的return语句被执行而不是 try子句的return语句?

我认为这两种说法都与现实情况相反。

编辑:

阅读@iBug 的答案后,现在很清楚print("foo") 是如何评估的,但None 没有返回。基本上,首先计算表达式,然后发生return。后来return False in finally 被执行。这清楚地说明了为什么我们会得到我们所做的输出。

不过,我看到 finally 中的 return False 是在 try 的 return print("foo") 之后执行的。

或者根据@iBug 的评论,10 RETURN_VALUE 完全被绕过了?

编辑

这已在文档中得到解决,现在返回的内容是正确的。但是,如果您想知道“如何”,请阅读所有 cmets 并仔细回答。

【问题讨论】:

  • 有时文档写得不太好。
  • return print("foo")实际上是2个单独的步骤,首先调用打印函数,然后将返回值None分配给return语句 - 然后 finally块返回语句覆盖先前的语句值。这有点过于简化了,但你明白我的意思
  • @IainShelvington:你可以把它分解成更多的步骤(加载print,加载"foo",调用函数,返回值,对于初学者),但所有这些步骤都是执行的一部分return 声明。返回值计算与实际返回值是分开的,但它与整个return 语句并不分开。
  • 措辞不好——怎么可能既是before又是instead of,这些是相互排斥的概念。
  • 这是文档PR here 中的最新更改,令人困惑的措辞似乎直接取自this github comment。有other problems从同一个PR中引入。

标签: python python-3.x try-except finally


【解决方案1】:
$ python3
Python 3.7.5 (default, Nov 20 2019, 09:21:52)
[GCC 9.2.1 20191008] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> def bool_return():
...     try:
...         return print("foo")
...     finally:
...         return False
...
>>> import dis
>>> dis.dis(bool_return)
  2           0 SETUP_FINALLY            8 (to 10)

  3           2 LOAD_GLOBAL              0 (print)
              4 LOAD_CONST               1 ('foo')
              6 CALL_FUNCTION            1
              8 RETURN_VALUE

  5     >>   10 LOAD_CONST               2 (False)
             12 RETURN_VALUE
>>>

正如您在上面看到的,return False 确实发生在 try 块中的 return 语句之前,但在计算了要返回的值之后。

我认为文档可能通过 return 语句 表示“返回的行为”,或者换句话说,它没有考虑返回值的计算,这当然会发生在它被退回之前。


要观察8 RETURN_VALUE是否被执行,可以在调试模式下编译CPython解释器并在GDB中运行。对于这个答案来说,分步指南太臃肿了,所以我会在这里给出一个大纲(Linux)。

  • 从官方来源(python.org 网站或 GitHub)获取 CPython 源代码
  • 配置调试版本./configure --with-pydebug(你可能也想给--prefix=/opt/python3-debug),makemake install
  • 在 GDB 中启动调试 Python:gdb /opt/python3-debug/bin/python3 和 (gdb) r
  • 像往常一样定义函数bool_return
  • Python/ceval.c中找到字符串RETURN_VALUE,记下行号(for 3.8.1, it's 1911)。
  • 发送SIGTRAP挂起Python解释器,在上一步的位置设置断点(b Python/ceval.c:1911),然后c
  • 观察断点到达两次,输出如下:
(gdb breakpoint info)
False
(gdb breakpoint info)
  • 观察你在 REPL 中输入的每个语句是如何到达一次断点的。这是为了得知上述步骤中的第二个断点是由Python REPL引起的,所以只有第一个断点来自函数中的return语句。

现在很清楚,函数中只执行了一个return,肯定是12 RETURN_VALUE,所以Python指令8 RETURN_VALUE根本没有执行。

【讨论】:

  • 我猜12 RETURN_VALUE是finally的return语句吧?在属于try子句的8 RETURN_VALUE之后,是不是这样?
  • @Eternal 是的。而实际的执行顺序是0→2→4→6→10→12,其中8是完全绕过的。
  • 我们怎么知道呢? @iBug
  • 我不认为它被绕过了,但我相信它被 finally 覆盖了,我认为这几乎可以解释一切。
  • 还是不清楚,它绕过或者覆盖 @iBug
猜你喜欢
  • 2019-12-13
  • 2011-07-26
  • 2013-04-14
  • 2013-08-02
  • 2013-09-04
  • 1970-01-01
  • 1970-01-01
  • 2020-03-03
  • 2017-10-08
相关资源
最近更新 更多