__init__ vs __enter__ in context managers
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__ calledabout to enter with 1__enter__ calledin with 1__exit__ calledabout to enter with 2__enter__ calledin 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@contextmanagerdef 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
Antti Haapalas answer is perfectly fine. I just wanted to elaborate a bit on the usage of arguments (like myClass(* args)
) since that was somewhat unclear to me (retrospective I ask myself why....)
Using arguments for initialising your class in a with
statement is not different from using the class the usual way.The calls will happen in the following order:
__init__
(allocation of the class)__enter__
(enter context)__exit__
(leaving context)
Simple Example:
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()
Output:
__init__ called: 42__enter__ called do something with 42__exit__ called
If you want to make sure that your calls can (almost) only be used in a context (e.g. to force the call to __exit__
), see the stackoverflow post here. In the comments you will also find a answer to the question how to use arguments even then.