Необъяснимая разница между явным вызовом __enter__ в @contextmanager и выражении "with"

У меня есть тип менеджера контекста (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)

Сравните последовательность, начинающуюся с "подключить явный вызов к __enter__", чтобы "подключить неявный вызов к __enter__ через с".

Когда я явно призываю __enter__ на @contextmanager украшенная функция, почему Connection.__exit__ звонил до того как мне вернули соединение? И почему "соединение с выходом" никогда не печатается - это означает, что оно все еще находится в операторе yield и еще не покинуло with блок - так почему __exit__ называется?

1 ответ

Решение

Вы отказались от диспетчера контекста, поэтому объект генератора получил право на восстановление. Генератор __del__* автоматически звонит close на генераторе, который поднимает GeneratorExit в точке последнего yield, заканчивая with Connection() as c блокировка и запуск c.__exit__,

* технически, на Python 2, это на самом деле происходит в процедуре очистки более низкого уровня, чем __del__,

Другие вопросы по тегам