在多个上下文管理器上创建一个“with”块?

321

假设您有三个对象通过上下文管理器获取,例如锁、数据库连接和IP套接字。 您可以通过以下方式获取它们:

with lock:
   with db_con:
       with socket:
            #do stuff

但是有没有一种方法可以在一个代码块中完成呢?就像这样:

with lock,db_con,socket:
   #do stuff

此外,如果我们有一个长度未知的对象数组,其中包含具有上下文管理器的对象,那么是否有可能以某种方式执行以下操作:

a=[lock1, lock2, lock3, db_con1, socket, db_con2]
with a as res:
    #now all objects in array are acquired
如果答案是“否”,那是因为这样的需求意味着设计有问题,或者我应该在PEP中建议它吗? :-P

我已经开始了关于反转此问题的重复目标的元讨论。 - Graham
@timgeb 我已经开始了关于这个重复关闭的讨论。很抱歉没有早点通知你; 我忘记了。 - Graham
5个回答

540

Python 2.7,3.1及以上版本中,您可以编写:

with A() as X, B() as Y, C() as Z:
    do_something()

通常这是最好的方法,但如果你有一个长度未知的上下文管理器列表,你将需要以下其中一种方法。


Python 3.3中,你可以使用contextlib.ExitStack输入一个长度未知的上下文管理器列表:

with ExitStack() as stack:
    for mgr in ctx_managers:
        stack.enter_context(mgr)
    # ...

这允许您在将上下文管理器添加到ExitStack时创建它们,从而防止了可能存在的contextlib.nested问题(下面提到)。

contextlib2 提供了一个用于Python 2.6和2.7的ExitStack后移版本


Python 2.6及以下版本中,您可以使用contextlib.nested

from contextlib import nested

with nested(A(), B(), C()) as (X, Y, Z):
    do_something()

等同于:

m1, m2, m3 = A(), B(), C()
with m1 as X:
    with m2 as Y:
        with m3 as Z:
            do_something()

请注意,这与通常使用嵌套的with不完全相同,因为在进入上下文管理器之前,将最初调用A()B()C()。如果其中一个函数引发异常,则无法正确工作。

contextlib.nested在较新的Python版本中已被弃用,推荐使用以上方法。


4
@noam: 实际上,Python 3.1 版本中 nested 函数的文档字符串指出:“与使用多个管理器形式的 with 语句相比,此函数的一个优点是参数解包允许它与可变数量的上下文管理器一起使用,例如:with nested(*managers): do_something()”。 - interjay
12
一方面,它已被弃用,但另一方面,他们承认废弃模块相对于替代方案具有优势? - olamundo
9
通常情况下这不会有任何影响。但如果 B()C() 引发异常,则会产生 巨大 的影响。嵌套的 with 语句会调用 A().__exit__(),但 contextlib.nested() 不会! - Martijn Pieters
11
一个问题:使用简单的“with open(A) as a, open(B) as b:”语法风格,换行会使pep8标准工具报告似乎不可能满足要求。我使用反斜杠表示换行,因为用括号括起来的逗号分隔表达式会导致语法错误报告。使用反斜杠时,会出现“E127 continuation line over-indented”警告。我还没有找到一种方法可以在使用此语法的同时抑制所有警告。 - Darren Ringer
7
我曾经遇到过同样的问题。我也使用了反斜杠来实现这一点,虽然我很讨厌它。在初始的with语句后,将所有上下文管理器进行双重缩进。只需单缩进包装的内容即可。这对我而言可以通过flake8检查。 - sage88
显示剩余9条评论

94

从Python 3.10开始,您将能够使用括号形式的上下文管理器!感谢@iforapsy!

with (
    mock.patch('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') as a,
    mock.patch('bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb') as b,
    mock.patch('cccccccccccccccccccccccccccccccccccccccccc') as c,
):
    do_something()

对于Python版本小于3.10的情况

@interjay的回答是正确的。然而,如果你需要处理长上下文管理器,例如mock.patch上下文管理器,那么你很快会意识到你想要将它们分成多行。结果发现你不能用括号包裹它们,所以你必须使用反斜杠。这是它的样子:

with mock.patch('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') as a, \
        mock.patch('bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb') as b, \
        mock.patch('cccccccccccccccccccccccccccccccccccccccccc') as c:
    do_something()

10
为什么不允许在括号中换行呢?(我因此遇到了麻烦) - John Deighan
3
为避免使用反斜杠,您可以格式化它像这样(由于评论不允许换行,而且此问题已关闭以防止添加回答,因此我必须将其链接为图像)。 - Stef
2
mock.patch 应该被评估为一个对象。我们也可以写成 a_ctx = mock.patch('a'); ...; with a_ctx as a, ...: - ThorSummoner
3
好消息,各位。由于有了新的解析器,在Python 3.10中,括号内的上下文管理器将成为有效语法。 - iforapsy
1
@iforapsy 太棒了,我会研究一下并相应地更新我的答案。 - sage88
显示剩余6条评论

34

你的问题的第一部分可以在Python 3.1中实现。

With more than one item, the context managers are processed as if multiple with statements were nested:

with A() as a, B() as b:
    suite

is equivalent to

with A() as a:
    with B() as b:
        suite

Changed in version 3.1: Support for multiple context expressions


感谢回答!但这并没有完全回答我的问题:关于我提到的第二种情况,在不知道数组中有多少个管理器的情况下,如果将上下文管理器放入数组中是否也能在Python 3.X中实现类似with [cm1, cm2, cm3, cm4, cm5] as result: ....的效果呢? - olamundo
2
@noam: 为了解决你问题的第二部分,你可以编写一个类来包装多个资源,并为该类实现__enter____exit__。我不确定是否已经有标准库类可以完成这项工作。 - Mark Byers
@Mark 我认为这并不容易 - 这就是为什么contextlib.nested()被弃用的原因。如果在其他事物生成和上下文管理器激活之间发生了某些事情,可能会导致清理不按预期进行。 - glglgl

11

Python 3.3 中,可以通过使用 contextlib.ExitStack 来解决你问题的第二个部分。


9

继@sage88的回答之后,您可以在进入这些补丁之前始终将它们分配为有意义的变量名。

您可以将这些补丁创建为多行。

a_patch = mock.patch('aaaaaaa') 
b_patch = mock.patch('bbbbbbb')
c_patch = mock.patch('ccccccc') 
with a_patch as a, b_patch as b, c_patch as c:    
    do_something()

2
这样做很危险:如果第二个函数调用引发异常,因为您尚未进入with语句,所以第一个上下文管理器将不会关闭。 - interjay

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