Насколько я понимаю, методы __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
утверждением не отличаются от использования класса обычного способа. Вызовы будут выполняться в следующем порядке:
-
__init__
(выделение класса) -
__enter__
(введите контекст) -
__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. В комментариях вы также найдете ответ на вопрос, как использовать аргументы даже тогда.