手动调用__enter__和__exit__

时间:2014-10-29 16:25:06

标签: python python-3.x contextmanager

我用谷歌搜索calling __enter__ manually但没有运气。因此,让我们假设我有 MySQL 连接器类,它使用__enter____exit__函数(最初与with语句一起使用)来连接/断开数据库。

让我们有一个使用其中2个连接的类(例如用于数据同步)。 注意:这不是我的现实场景,但它似乎是最简单的例子

使这一切协同工作的最简单方法是这样的课程:

class DataSync(object):

    def __init__(self):
        self.master_connection = MySQLConnection(param_set_1)
        self.slave_connection = MySQLConnection(param_set_2)

    def __enter__(self):
            self.master_connection.__enter__()
            self.slave_connection.__enter__()
            return self

    def __exit__(self, exc_type, exc, traceback):
            self.master_connection.__exit__(exc_type, exc, traceback)
            self.slave_connection.__exit__(exc_type, exc, traceback)

    # Some real operation functions

# Simple usage example
with DataSync() as sync:
    records = sync.master_connection.fetch_records()
    sync.slave_connection.push_records(records)

:这样手动拨打__enter__ / __exit__可以(有什么不对)吗?

Pylint 1.1.0没有对此发出任何警告,也没有找到任何关于它的文章(在开始的谷歌链接)。

那么打电话:

try:
    # Db query
except MySQL.ServerDisconnectedException:
    self.master_connection.__exit__(None, None, None)
    self.master_connection.__enter__()
    # Retry

这是一个好/坏的做法?为什么呢?

3 个答案:

答案 0 :(得分:12)

你的第一个例子不是一个好主意:

  1. 如果slave_connection.__enter__引发异常,会发生什么:

    • master_connection获取其资源
    • slave_connection失败
    • DataSync.__enter__宣传异常
    • DataSync.__exit__无法运行
    • master_connection永远不会被清理干净!
    • 糟糕的可能性
  2. 如果master_connection.__exit__引发异常会怎样?

    • DataSync.__exit__提前结束
    • slave_connection永远不会被清理干净!
    • 糟糕的可能性
  3. contextlib.ExitStack可以在这里提供帮助:

    def __enter__(self):
        with ExitStack() as stack:
            stack.enter_context(self.master_connection)
            stack.enter_context(self.slave_connection)
            self._stack = stack.pop_all()
        return self
    
    def __exit__(self, exc_type, exc, traceback):
        self._stack.__exit__(self, exc_type, exc, traceback)
    

    提出同样的问题:

    1. 如果slave_connection.__enter__引发异常,会发生什么:

      • 退出with块,stack清除master_connection
      • 一切都好!
    2. 如果master_connection.__exit__引发异常会怎样?

      • 无所谓,slave_connection在调用之前被清理
      • 一切都好!
    3. 好的,如果slave_connection.__exit__抛出异常会怎样?

      • ExitStack确保调用master_connection.__exit__从属连接发生的任何事情
      • 一切都好!
    4. 直接调用__enter__没有错,但是如果你需要在多个对象上调用它,请确保正确清理!

答案 1 :(得分:11)

不,这没什么不对。标准库中甚至有一些地方可以执行此操作。与multiprocessing module

一样
class SemLock(object):

    def __init__(self, kind, value, maxvalue, *, ctx):
            ...
            try:
                sl = self._semlock = _multiprocessing.SemLock(
                    kind, value, maxvalue, self._make_name(),
                    unlink_now)
            except FileExistsError:
                pass
    ...

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

    def __exit__(self, *args):
        return self._semlock.__exit__(*args)

tempfile module

class _TemporaryFileWrapper:

    def __init__(self, file, name, delete=True):
        self.file = file
        self.name = name
        self.delete = delete
        self._closer = _TemporaryFileCloser(file, name, delete)

    ...

    # The underlying __enter__ method returns the wrong object
    # (self.file) so override it to return the wrapper
    def __enter__(self):
        self.file.__enter__()
        return self

    # Need to trap __exit__ as well to ensure the file gets
    # deleted when used in a with statement
    def __exit__(self, exc, value, tb):
        result = self.file.__exit__(exc, value, tb)
        self.close()
        return result

标准库示例不会为两个对象调用__enter__ / __exit__,但是如果您有一个对象负责创建/销毁多个对象的上下文而不是一个,为所有人调用__enter__ / __exit__都没问题。

唯一可能的问题是正确处理您正在管理的对象的__enter__ __exit__次调用的返回值。使用__enter__,您需要确保返回包装器对象的用户所需的state以从with ... as <state>:调用返回。使用__exit__,您需要决定是否要传播上下文中发生的任何异常(通过返回False),或者抑制它(通过返回True)。您的托管对象可以尝试以任何方式执行此操作,您需要确定对包装器对象有意义的内容。

答案 2 :(得分:0)

我仅回答标题,即调用__enter____exit__,例如从IPython提示。您可以这样操作:


ctx.__enter__()
ctx.__exit__(*sys.exc_info())