在 @contextmanager 上显式调用 __enter__ 与 "with" 语句之间无法解释的区别

Unexplained difference between explicitly invoking __enter__ on @contextmanager vs "with" statement

我有一个上下文管理器类型 (Connection) 和一个 @contextmanager 修饰函数,它从 with 语句中生成该类型。

如果我在修饰函数上显式调用 __enter__,则 __exit__Connection 返回之前被调用。

这是代码:

from __future__ import print_function
from contextlib import contextmanager


class Connection(object):
    def __init__(self):
        self.closed = False

    def __enter__(self):
        print('Connection.__enter__')
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('Connection.__exit__')
        self.closed = True
        return False

    def __repr__(self):
        return "{}(closed={})".format(self.__class__.__name__, self.closed)


@contextmanager
def connect():
    with Connection() as c:
        print('connect yielding connection')
        yield c
        print('connect yielded connection')


def context():
    print("connect explicit call to __enter__")
    c = connect().__enter__()
    print("got connection via __enter__", c)
    print()

    print("connect implicit call to __enter__ via with")
    with connect() as c:
        print("got connection in 'with'", c)
    print("connection after 'with'", c)
    print()

    print("Connection explicit call to __enter__")
    c = Connection().__enter__()
    print("got connection via __enter__", c)
    print()

    print("Connection implicit call to __enter__ via with")
    with Connection() as c:
        print("got connection in with", c)
    print("connection after 'with'", c)


if __name__ == "__main__":
    context()

当 运行 输出:

connect explicit call to __enter__
Connection.__enter__
connect yielding connection
Connection.__exit__
got connection via __enter__ Connection(closed=True)

connect implicit call to __enter__ via with
Connection.__enter__
connect yielding connection
got connection in 'with' Connection(closed=False)
connect yielded connection
Connection.__exit__
connection after 'with' Connection(closed=True)

Connection explicit call to __enter__
Connection.__enter__
got connection via __enter__ Connection(closed=False)

Connection implicit call to __enter__ via with
Connection.__enter__
got connection in with Connection(closed=False)
Connection.__exit__
connection after 'with' Connection(closed=True)

比较以 "connect explicit call to __enter__" 和 "connect implicit call to __enter__ via with" 开头的序列。

当我在 @contextmanager 修饰函数上显式调用 __enter__ 时,为什么在连接返回给我之前调用 Connection.__exit__?为什么 "connect yielded connection" 从未打印过 - 这意味着它仍在 yield 语句中并且尚未离开 with 块 - 为什么 __exit__ 被调用?

您丢弃了上下文管理器,因此生成器对象符合回收条件。生成器的 __del__* 自动调用生成器上的 close,它在最后一个 yield 处引发 GeneratorExit,结束 with Connection() as c 块并触发 c.__exit__.

*从技术上讲,在 Python 2 上,这实际上发生在 lower-level 清理例程中,而不是 __del__.