__init__ vs __enter__ в контекстных менеджерах

Насколько я понимаю, методы __init__() и __enter__() диспетчера контекста вызываются ровно один раз каждый, один за другим, не оставляя никаких шансов для выполнения какого-либо другого кода между ними. Какова цель разделения их на два метода и что я должен вложить в каждый из них?

Изменить: извините, не обращал внимания на документы.

Изменить 2: на самом деле, я запутался, потому что думал о декораторе @contextmanager. Диспетчер контекста, созданный с помощью @contextmananger, можно использовать только один раз (генератор будет исчерпан после первого использования), поэтому часто они записываются с вызовом конструктора внутри оператора with; и если бы это был единственный способ использовать оператор with, мой вопрос имел бы смысл. Конечно, в действительности менеджеры контекста носят более общий характер, чем то, что может создать @contextmanager; в частности, менеджеры контекста, как правило, могут использоваться повторно. Надеюсь, на этот раз я понял?


person max    schedule 21.09.2016    source источник
comment
Вы путаете создание диспетчера контекста с вводом контекста. Они различны, и вы можете использовать один и тот же диспетчер контекста более одного раза.   -  person Martijn Pieters    schedule 21.09.2016


Ответы (2)


Насколько я понимаю, методы __init__() и __enter__() диспетчера контекста вызываются ровно один раз каждый, один за другим, не оставляя никаких шансов для выполнения какого-либо другого кода между ними.

И ваше понимание неверно. __init__ вызывается, когда объект создается, __enter__, когда он вводится с помощью оператора with, и это две совершенно разные вещи. Часто бывает так, что конструктор вызывается напрямую при инициализации with, без промежуточного кода, но это не обязательно.

Рассмотрим этот пример:

class Foo:
    def __init__(self):
        print('__init__ called')
    def __enter__(self):
        print('__enter__ called')
        return self
    def __exit__(self, *a):
        print('__exit__ called')

myobj = Foo()

print('\nabout to enter with 1')
with myobj:
    print('in with 1')

print('\nabout to enter with 2')
with myobj:
    print('in with 2')

myobj можно инициализировать отдельно и ввести в несколько with блоков:

Вывод:

__init__ called

about to enter with 1
__enter__ called
in with 1
__exit__ called

about to enter with 2
__enter__ called
in with 2
__exit__ called

Более того, если бы __init__ и __enter__ не были разделены, было бы невозможно даже использовать следующее:

def open_etc_file(name):
    return open(os.path.join('/etc', name))

with open_etc_file('passwd'):
    ...

поскольку инициализация (в open) явно отделена от записи with.


Менеджеры, созданные contextlib.manager, являются однократными, но снова может быть построен вне блока with. Возьмем пример:

from contextlib import contextmanager

@contextmanager
def tag(name):
    print("<%s>" % name)
    yield
    print("</%s>" % name)

вы можете использовать это как:

def heading(level=1):
    return tag('h{}'.format(level))

my_heading = heading()
print('Below be my heading')
with my_heading:
     print('Here be dragons')

вывод:

Below be my heading
<h1>
Here be dragons
</h1>

Однако, если вы попытаетесь повторно использовать my_heading (и, следовательно, tag), вы получите

RuntimeError: generator didn't yield
person Antti Haapala    schedule 21.09.2016
comment
Ой, как-то во всех примерах, которые я вспомнил, вызов конструктора происходит внутри оператора with (например, with Foo()...). Теперь все имеет смысл. Спасибо - person max; 21.09.2016
comment
Ой, подождите, а как насчет @contextmanager? Поскольку он полагается на генератор, не исчерпывает ли он его при первом использовании и, таким образом, делает невозможным повторное использование объекта? - person max; 21.09.2016
comment
Так что правильно будет сказать, что с помощью @contextmanager декоратора можно создать только подмножество полезных менеджеров контекста; остальные требуют явного написания класса, который соответствует API диспетчера контекста? - person max; 21.09.2016
comment
Будьте осторожны с формулировками. __init__ на самом деле не конструктор, а инициатор в Python. __new__ - конструктор. Вы можете видеть в сигнатуре метода __init__, что если self передается в качестве первого параметра, конструктор уже был вызван, иначе у вас не было бы экземпляра для self. - person CheradenineZK; 25.02.2021

Ответ Антти Хаапаласа прекрасен. Я просто хотел немного подробнее рассказать об использовании аргументов (например, myClass(* args)), поскольку это было для меня несколько непонятно (ретроспективно, я спрашиваю себя, почему ....)

Использование аргументов для инициализации вашего класса в операторе with не отличается от использования класса обычным способом. Звонки будут происходить в следующем порядке:

  1. __init__ (размещение класса)
  2. __enter__ (введите контекст)
  3. __exit__ (выход из контекста)

Простой пример:

class Foo:
    def __init__(self, i):
        print('__init__ called: {}'.format(i))
        self.i = i
    def __enter__(self):
        print('__enter__ called')
        return self
    def do_something(self):
        print('do something with {}'.format(self.i))
    def __exit__(self, *a):
        print('__exit__ called')

with Foo(42) as bar:
    bar.do_something()

Вывод:

__init__ called: 42
__enter__ called
    do something with 42
__exit__ called

Если вы хотите убедиться, что ваши вызовы могут (почти) использоваться только в контексте (например, для принудительного вызова __exit__), см. Сообщение stackoverflow здесь. В комментариях вы также найдете ответ на вопрос, как использовать аргументы уже тогда.

person SeparateReality    schedule 26.11.2017