Python中继承上下文管理器的惯用方式是什么?

21

Python教我们使用__enter____exit__清理对象。如果我需要创建一个使用对象必须使用上下文管理器的对象怎么办?想象一下:

from database1 import DB1
from database2 import DB2

通常,它们会被这样使用:

with DB1() as db1, DB2() as db2:
    db1.do_stuff()
    db2.do_other_stuff()

无论发生什么,db1db2都将运行其__exit__函数,并清理连接、刷新等。
当我把所有内容放在一个类中时,我该怎么做?这显然是不正确的,因为db1db2的上下文管理器在块结束时运行,如评论中所指出的。
class MyApp(object):
    def __enter__(self):
        with DB1() as self.db1, DB2() as self.db2:
            return self
    def __exit__(self, type, value, traceback):
        self.db1.__exit__(self, type, value, traceback)
        self.db2.__exit__(self, type, value, traceback)

我甚至考虑做这样的事情: 实际上,这看起来是个好主意(经过一些清理):

class MyApp(object):
    def __init__(self):
        self.db1 = DB1()
        self.db2 = DB2()
    def __enter__(self):
        self.db1.__enter__()
        self.db2.__enter__()
        return self
    def __exit__(self, type, value, traceback):
        try:
            self.db1.__exit__(self, type, value, traceback)
        except:
            pass
        try:
            self.db2.__exit__(self, type, value, traceback)
        except:
            pass

编辑:修复了代码。


你的第一次尝试没有意义 - with DB1() ... 块在 MyApp.__enter__ 完成之前就结束了;self.db1MyApp.__exit__ 开始之前就已经退出了。 - jonrsharpe
第二个选项几乎是好的,如果您添加异常处理,它将是可以接受的(这样,如果第二个__enter__或第一个__exit__失败,则其他连接不会挂起)。话虽如此,请考虑contextmanager - bereal
1
@jonrsharpe,我知道这就是我第一次问的原因。 - user37203
1
你读过例如https://dev59.com/217Va4cB1Zd3GeqPN-KR吗? - jonrsharpe
3个回答

7

我会选择第二种解决方案,但也要处理数据库错误:

import sys

class MyApp(object):
    def __init__(self):
        self.db1 = DB1()
        self.db2 = DB2()
    def __enter__(self):
        self.db1.__enter__()
        try:
            self.db2.__enter__()
        except:
            self.db1.__exit__(None, None, None) # I am not sure with None
            raise
        return self
    def __exit__(self, type, value, traceback):
        try:
            self.db1.__exit__(type, value, traceback)
        finally:
            self.db2.__exit__(type, value, traceback)

第一个在__enter__中调用__exit__,因为使用了with,所以不起作用。

编辑:还要看看 @Ming 的 答案。在许多情况下更短。


你应该在类中将变量设置为None吗? - John Ruddell
1
你的__enter__函数没有return self - user37203
@user37203 谢谢,已经更正了。@JohnRuddell 你能解释一下为什么吗? - User
在上下文中,__init__首先运行,然后是__enter__。可能与运行顺序有关的混淆? - user37203
1
当调用外部的__exit__方法时,也不需要单独传递self参数。看起来可能有一个更正正在等待编辑批准,因为编辑队列已满。 - Will

6
大多数上下文管理器可以使用@contextmanager装饰器编写。您编写一个只有一个yield的函数,yield之前是您的“enter”函数,yield之后是您的“exit”函数。由于生成器的实现方式,如果一个yield在with语句中,则with语句不会在yield处退出。
例如:
from contextlib import contextmanager

class SomeContextManager:
    def __init__(self, name):
        self.name = name
    def __enter__(self):
        print("enter", self.name)
        return self
    def __exit__(self, ex_type, value, traceback):
        print("exit", self.name)

class SomeContextManagerWrapper:
    def __init__(self, *context_managers):
        self.context_managers = context_managers
    @property
    def names(self):
        return [cm.name for cm in self.context_managers]

@contextmanager
def context_manager_combiner():
    print("context_manager_combiner entering")
    with SomeContextManager("first") as a, SomeContextManager("second") as b:
        yield SomeContextManagerWrapper(a, b)
    print("context_manager_combiner exiting")

with context_manager_combiner() as wrapper:
    print("in with statement with:", wrapper.names)

输出:

context_manager_combiner entering
enter first
enter second
in with statement with: ['first', 'second']
exit second
exit first
context_manager_combiner exiting

1
这是最好的答案,但我需要一些可以应用于类实例化的东西。我不太确定如何使用contextmanager来做到这一点。 - user37203
您可以在yield语句中返回一个类,该类包装了您的数据库,并具有用于操作数据库的相关方法。 - Dunes

3

这取决于你想要实现的目标。其中一种可能性是先构建个别上下文管理器,然后将它们与标准库的contextlib.nested相结合。这将为您提供一个像示例中的MyApp一样的单个对象,但以DRY(不重复自己)的方式利用现有的标准库。


这很不错!你能添加源代码吗?可以使用类来完成吗? - User

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