Python*与*语句完全等同于尝试 - (除外) - finally块?

Cla*_*ara 19 python with-statement contextmanager

我知道这已被广泛讨论,但我仍然找不到答案来证实这一点:with语句与在try - (除了)-finally块中调用相同代码相同,其中无论在__exit__函数中定义了什么上下文管理器放在finally块中?

例如 - 这两个代码片段完全相同吗?

import sys
from contextlib import contextmanager

@contextmanager
def open_input(fpath):
    fd = open(fpath) if fpath else sys.stdin
    try:
        yield fd
    finally:
        fd.close()

with open_input("/path/to/file"):
    print "starting to read from file..."
Run Code Online (Sandbox Code Playgroud)

同样如下:

def open_input(fpath):
    try:
        fd = open(fpath) if fpath else sys.stdin
        print "starting to read from file..."
    finally:
        fd.close()

open_input("/path/to/file")
Run Code Online (Sandbox Code Playgroud)

谢谢!

Vee*_*rac 27

我将暂时提到范围,因为它真的不太相关.

根据PEP 343,

with EXPR as VAR:
    BLOCK
Run Code Online (Sandbox Code Playgroud)

翻译成

mgr = (EXPR)
exit = type(mgr).__exit__  # Not calling it yet
value = type(mgr).__enter__(mgr)
exc = True
try:
    try:
        VAR = value  # Only if "as VAR" is present
        BLOCK
    except:
        # The exceptional case is handled here
        exc = False
        if not exit(mgr, *sys.exc_info()):
            raise
        # The exception is swallowed if exit() returns true
finally:
    # The normal and non-local-goto cases are handled here
    if exc:
        exit(mgr, None, None, None)
Run Code Online (Sandbox Code Playgroud)

正如你所看到的,type(mgr).__enter__正如你所期望的那样,而不是在try你的内心.

type(mgr).__exit__在退出时调用.唯一的区别是,当存在异常时,将采用该if not exit(mgr, *sys.exc_info())路径.withfinally子句可以做的不同,这使得能够内省和沉默错误.


contextmanager没有这个复杂得多.只是:

def contextmanager(func):
    @wraps(func)
    def helper(*args, **kwds):
        return _GeneratorContextManager(func, *args, **kwds)
    return helper
Run Code Online (Sandbox Code Playgroud)

然后看看有问题的课程:

class _GeneratorContextManager(ContextDecorator):
    def __init__(self, func, *args, **kwds):
        self.gen = func(*args, **kwds)

    def __enter__(self):
        try:
            return next(self.gen)
        except StopIteration:
            raise RuntimeError("generator didn't yield") from None

    def __exit__(self, type, value, traceback):
        if type is None:
            try:
                next(self.gen)
            except StopIteration:
                return
            else:
                raise RuntimeError("generator didn't stop")
        else:
            if value is None:
                value = type()
            try:
                self.gen.throw(type, value, traceback)
                raise RuntimeError("generator didn't stop after throw()")
            except StopIteration as exc:
                return exc is not value
            except:
                if sys.exc_info()[1] is not value:
                    raise
Run Code Online (Sandbox Code Playgroud)

已经省略了不重要的代码.

首先要注意的是,如果有多个yields,则此代码将出错.

这不会明显影响控制流程.

考虑__enter__.

try:
    return next(self.gen)
except StopIteration:
    raise RuntimeError("generator didn't yield") from None
Run Code Online (Sandbox Code Playgroud)

如果上下文管理器写得很好,这将永远不会超出预期.

一个区别是,如果发生器抛出StopIteration,RuntimeError将产生不同的错误().这意味着with如果您运行完全任意的代码,行为与正常情况不完全相同.

考虑一个无错误__exit__:

if type is None:
    try:
        next(self.gen)
    except StopIteration:
        return
    else:
        raise RuntimeError("generator didn't stop")
Run Code Online (Sandbox Code Playgroud)

唯一的区别是和以前一样; 如果您的代码抛出StopIteration,它将影响生成器,因此contextmanager装饰器会误解它.

这意味着:

from contextlib import contextmanager

@contextmanager
def with_cleanup(func):
    try:
        yield
    finally:
        func()

def good_cleanup():
    print("cleaning")

with with_cleanup(good_cleanup):
    print("doing")
    1/0
#>>> doing
#>>> cleaning
#>>> Traceback (most recent call last):
#>>>   File "", line 15, in <module>
#>>> ZeroDivisionError: division by zero

def bad_cleanup():
    print("cleaning")
    raise StopIteration

with with_cleanup(bad_cleanup):
    print("doing")
    1/0
#>>> doing
#>>> cleaning
Run Code Online (Sandbox Code Playgroud)

哪个不太重要,但可能.

最后:

else:
    if value is None:
        value = type()
    try:
        self.gen.throw(type, value, traceback)
        raise RuntimeError("generator didn't stop after throw()")
    except StopIteration as exc:
        return exc is not value
    except:
        if sys.exc_info()[1] is not value:
            raise
Run Code Online (Sandbox Code Playgroud)

这提出了同样的问题StopIteration,但有趣的是注意到最后一部分.

if sys.exc_info()[1] is not value:
    raise
Run Code Online (Sandbox Code Playgroud)

这意味着如果未处理异常,则回溯将保持不变.如果它已被处理但存在新的回溯,则会引发该回溯.

这完全符合规范.


TL; DR

  • with实际上比稍微更强大try...finally的是,with可以反思和沉默的错误.

  • 要小心StopIteration,否则你可以@contextmanager用来创建上下文管理器.

  • 一篇写得很好的文章强调了这些之间的细微差别(很少表现出来)也可以在这里找到(https://vorpus.org/blog/control-c-handling-in-python-and-trio/#id5 ) 给有兴趣的人。 (2认同)