As far as I understand, __init__() and __enter__() methods of the context manager are called exactly once each, one after another, leaving no chance for any other code to be executed in between.
And your understanding is incorrect. __init__ is called when the object is created, __enter__ when it is entered with with statement, and these are 2 quite distinct things. Often it is so that the constructor is directly called in with initialization, with no intervening code, but this doesn't have to be the case.
Consider this example:
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 can be initialized separately and entered in multiple with blocks:
Output:
__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
Furthermore if __init__ and __enter__ weren't separated, it wouldn't be possible to even use the following:
def open_etc_file(name):
return open(os.path.join('/etc', name))
with open_etc_file('passwd'):
...
since the initialization (within open) is clearly separate from with entry.
The managers created by contextlib.manager are single-entrant, but they again can be constructed outside the with block. Take the example:
from contextlib import contextmanager
@contextmanager
def tag(name):
print("<%s>" % name)
yield
print("</%s>" % name)
you can use this as:
def heading(level=1):
return tag('h{}'.format(level))
my_heading = heading()
print('Below be my heading')
with my_heading:
print('Here be dragons')
output:
Below be my heading
<h1>
Here be dragons
</h1>
However, if you try to reuse my_heading (and, consequently, tag), you will get
RuntimeError: generator didn't yield