如何在“with”语句中跳出或退出?

73

我只想在特定条件下退出一个with语句:

with open(path) as f:
    print 'before condition'
    if <condition>: break #syntax error!
    print 'after condition'

当然,上述方法行不通。有没有一种方法可以做到这一点呢?(我知道我可以反转条件:if not <condition>: print 'after condition'——是否有类似于上述的方式?)


3
我好奇为什么在你的情况下,使用 if not <condition> 是不可取的。 - senderle
7
@senderle 我认为必须缩进整个代码块会降低可读性并且掩盖了代码流程(特别是在像这种情况下,其中分支语句是个例外的情况)。想象一下有三个以上的条件语句,每个之后都有代码块... - jmilloy
1
算了吧...这里的回答都不值得麻烦,哈哈 - Yan King Yin
13个回答

96

with有问题吗?向问题投掷更多可使用with的对象!


class fragile(object):
    class Break(Exception):
      """Break out of the with statement"""

    def __init__(self, value):
        self.value = value

    def __enter__(self):
        return self.value.__enter__()

    def __exit__(self, etype, value, traceback):
        error = self.value.__exit__(etype, value, traceback)
        if etype == self.Break:
            return True
        return error

只需将您要使用with的表达式包装在fragile中,并使用raise fragile.Break在任何地方跳出!

with fragile(open(path)) as f:
    print 'before condition'
    if condition:
        raise fragile.Break
    print 'after condition'

这种设置的好处

  • 使用with语句,只需要使用with语句。不会将您的函数包装在一个语义上误导人的一次性“循环”或狭隘专门化的函数中,并且不会强制您在with之后进行额外的错误处理。
  • 保持本地变量可用,而无需将它们传递给包装函数。
  • 可嵌套!

with fragile(open(path1)) as f:
    with fragile(open(path2)) as g:
        print f.read()
        print g.read()
        raise fragile.Break
        print "This wont happen"
    print "This will though!"

这样,如果您想要两者都终止,就不需要创建一个新的函数来包装外部的with

  • 根本不需要重构:只需使用fragile包装您已有的内容,就可以开始了!
  • 此设置的缺点

    • 实际上没有使用“break”语句。并非所有情况都能成功;)

    1
    我认为这是比大多数其他设置更好的设置。它不需要重构代码,而且fragile()是可重用的。 - Mikhail
    1
    刚刚回来看到这个,我很喜欢。 - jmilloy
    1
    我认为一个更大的缺点是,它使得调用您包装的上下文管理器的方法有点笨拙。我喜欢这个通用解决方案,但我会将其实现为静态函数,通过包装当前存在的__enter__和__exit__来进行猴子补丁。也许还有一些花哨的方式可以使用装饰器来实现这一点... - phil_20686
    2
    它很聪明,使用了装饰器模式(我最喜欢的),但是它使用异常处理来控制流程。我还没有下定决心。 - Jacob Zimmerman
    1
    @madtyn,文档字符串被视为语句,因此我们可以省略 pass。在这种情况下保留或省略 pass 是个人喜好的问题。 - Orez
    显示剩余4条评论

    57

    最好的方法是将其封装在一个函数中,然后使用 return:

    def do_it():
        with open(path) as f:
            print 'before condition'
            if <condition>:
                return
            print 'after condition'
    

    7
    这会返回,但并没有完全“跳出”with语句。用户必须意识到,使用这种方法时无论如何都会执行__exit__ - Asclepius

    18

    这是一个古老的问题,但这是一个方便的“可中断作用域”习语的应用程序。只需将您的with语句嵌入其中:

    for _ in (True,):
        with open(path) as f:
            print 'before condition'
            if <condition>: break
            print 'after condition'
    

    这个成语创建了一个“循环”,始终只执行一次,唯一目的是将一个代码块封装在一个范围内,以便可以有条件地中断。在 OP 的情况下,它是用来封装上下文管理器调用的,但它也可以是任何需要有条件跳出的有界语句序列。

    被接受的答案很好,但是这种技术可以在不需要创建函数的情况下完成相同的操作,而这种操作并不总是方便或者所期望的。


    我喜欢这个代码明显只执行一次(而不是while True变体),并且你还为这个习惯用语取了一个名字。 - jmilloy

    12

    我认为你应该重新构思逻辑:

    with open(path) as f:
        print 'before condition checked'
        if not <condition>:
            print 'after condition checked'
    

    7
    @jmilloy,我认为这至少是与将with语句封装在函数中并使用return或引发异常一样好的解决方案。坦率地说,在许多情况下,我认为这将是最优雅和易读的解决方案。 - senderle
    6
    同意100%。然而,这并不是问题的关键。 - jmilloy
    5
    好的,如果我们讨论Python不允许的假设性的差劣结构,我们可能会抱怨缺乏一个像样的goto语句 :-) http://entrian.com/goto/ - K. Brafford

    8

    由于 break 只能在循环内部发生,因此在 with 中,您的选项有些受限:

    • 返回(将 "with" 和相关语句放在函数内部)
    • 退出(退出程序 - 可能不理想)
    • 异常(在 "with" 内生成异常,在下面捕获)

    如果您可以将 with 和相关语句(以及其他内容)隔离在一个函数内部,那么拥有一个函数并使用 return 可能是最干净和最简单的解决方案。

    否则,在需要时在 with 内部生成异常,立即在 with 下方/外部捕获以继续进行其余代码。

    更新:正如 OP 在下面的评论中建议的那样(也许是开玩笑?),您还可以将 with 语句包装在一个循环中,以使 break 生效 - 虽然这会导致语义上的误导。因此,虽然它是一种可行的解决方案,但可能不是推荐的方法)。


    是的,我知道;我想知道是否有一些确实有效的东西。 - jmilloy
    @Matthias 是的,我也考虑过这个问题——把它封装在一个函数中似乎更加清晰。 - jmilloy
    @Levon 我实际上尝试了使用 return 进行检查,但你只能从函数块中返回,而不能从 with 块中返回。我相信你知道这一点,但你的写法让人觉得 return 只会停止 with 块中剩余代码的执行,而实际上它还会阻止同一函数中 with 块后面的任何代码的执行。 - jmilloy
    1
    事实上,我经常希望with块像try块一样,可以干净地跟随异常情况。如果可以的话,这就是我会做的。 - jmilloy
    @Levon 我想我们可以在你的列表中添加一个选项:像 for i in [0] 这样在里面包装一个愚蠢的循环,这样你就可以打破它。 - jmilloy
    @jmilloy 我想这是真的..如果是这样,我会使用while True:作为结构。但从语义上来说,对于以后查看代码的其他人来说,这可能会产生误导。我认为在函数内部使用return(如果with+statements可以这样工作的话),然后跟随异常是最好的选择。 - Levon

    5

    这个问题是在Python 3.4出现之前提出的,但在3.4中,您可以使用contextlib.suppress来抑制自己的个人异常。

    看看这个(可以直接运行)代码:

    from contextlib import suppress
    
    class InterruptWithBlock(UserWarning):
        """To be used to interrupt the march of a with"""
    
    condition = True
    with suppress(InterruptWithBlock):
        print('before condition')
        if condition: raise InterruptWithBlock()
        print('after condition')
    
    # Will not print 'after condition` if condition is True.
    

    使用问题中的代码,您可以执行以下操作:

    with suppress(InterruptWithBlock) as _, open(path) as f:
        print('before condition')
        if <condition>: raise InterruptWithBlock()
        print('after condition')
    
    

    注意:如果您仍在使用3.4之前的版本,您仍然可以轻松地创建自己的suppress上下文管理器。

    太棒了。我将其更新为被接受的答案,因为Python >= 3.4现在已经普遍成为标准。 - jmilloy

    3
    f = open("somefile","r")
    for line in f.readlines():
           if somecondition: break;
    f.close()
    

    我认为你不能打破with...你需要使用循环...或者只需执行其他人提到的函数方法。

    越简单越好 :) - Le Droid

    2
    作为简写代码片段:
    class a:
        def __enter__(self):
            print 'enter'
        def __exit__(self ,type, value, traceback):
            print 'exit'
    
    for i in [1]:
        with a():
            print("before")
            break
            print("after")
    

    ...

    enter
    before
    exit
    

    1
    这让人想起 Python C API 如何有时将宏的内容放在 do-while 循环中,只不过那是为了在末尾得到分号。 - Mad Physicist
    使用元组(如上面的 https://dev59.com/E2gu5IYBdhLWcg3wnoWC#43795918)还是列表(如此处)更有效率? - Marcel Waldvogel

    1
    使用 while True:
    while True:
        with open(path) as f:
            print 'before condition'
            if <condition>: 
                break 
            print 'after condition n'
        break
    

    1

    为此,存在一个名为__exit__()的函数。语法如下:

    with VAR = EXPR:
      try:
        BLOCK
      finally:
        VAR.__exit__()
    

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接