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

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

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

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

Ответы

Ответ 1

Насколько я понимаю, методы __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

Ответ 2

Ответ Антти Хаапаласа в порядке. Я просто хотел немного рассказать об использовании аргументов (например, 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. В комментариях вы также найдете ответ на вопрос, как использовать аргументы даже тогда.