On the Beauty of Python's ExitStack (2015)
rath.org
rath.org
with open("foo.txt") as foo_file, open("bar.txt") as bar_file, open("baz.txt", "w") as baz_file:
baz_file.write(foo_file.read() + bar_file.read()) with (open("foo.txt") as foo_file,
open("bar.txt") as bar_file,
open("baz.txt", "w") as baz_file):
baz_file.write(foo_file.read() + bar_file.read())C#8 then allowed using-statements that do not require blocks, which then are disposed when leaving the current block.
Pre C#8, you had to have a block, similar to how with-statement requires a block aka indentation
using (var res = OpenSomeResource()) {
res.doSomething();
// res.Dispose() implicitly called
}
Now you can have this (which is a bit like a go-style Defer but for blocks) using var res = OpenSomeResource();
res.doSomething();
// res.Dispose() implicitly called at end of current block
Of course, you have to take the potentially longer lifetime into account. This way looks a lot like C++s RAII with constructors and destructors (except the destructor here is the IDisposible.Dispose implementation). def do_thing():
with open('foo', 'r') as f
with a_lock
f.read()
frobnicate()
I guess the issue is that there is less of a clear idea of scope in Python, what does the above construct do at a module level? What does it do inside another with block? A lot of special rules that don't conveniently fit into something else unlike what I suppose C# has.This is actually vaguely reminiscent of auto release pools in Cocoa.
def do_thing():
with a_lock
check_f_cache_while_locked() # like this
with open('foo', 'r') as f
populate_cache(f) @contextmanager
def multi(*cms):
if len(cms) > 0:
with cms[0]:
with multi(*cms[1:]):
yield
else:
yield
# use with 0..N items, it behaves the same
with multi(...):
pass
Prior to finding ExitStack, I've frequently seen people either give up and do something obviously wrong, or try to manually manage their own stack of __enter__ and __exit__ funcs (which works, but exception handling is non-obvious and often loses useful info). Personally I prefer the recursive route, since it very obviously behaves the same as if you wrote the nesting out by hand. It even retains accurate stack traces "naturally", unlike most non-ExitStack tactics I've seen through the years. ExitStack's stack-correcting tactic is... a bit more "interesting"[3], though AFAIK it achieves roughly the same stack trace in the end.Since ExitStack exists, and is likely more efficient (by how much, I have no idea), it's probably a better choice for most situations. And it's just broadly a bit nicer for things that yield resources.
It's always worth browsing stdlib documentation to find gems like this, there are quite a few neat things in most languages that seem fairly unknown.
[1]: https://docs.python.org/3/library/contextlib.html#supporting...
[2]: https://repl.it/@Groxx/TepidRosybrownLocatorprogram#main.py
[3]: https://github.com/python/cpython/blob/master/Lib/contextlib...
[0]: https://docs.python.org/2.7/library/contextlib.html#contextl...
Seems like the __new__/__init__ issues exist with ExitStack as well, unlike with `with a, b, c`? So it's not really resolved / improved behaviorally, though ExitStack does have a few new uses (e.g. managing non-context-manager resources).
However, you can run into "maximum recursion depth exceeded" issues if the number of context managers is extremely large. ExitStack handles this situation nicely [1].
Note that I'm not saying that using so many context managers is a good idea ;-) In our case, using ExitStack (which also has a backport to Python 2 [2]) was the simplest fix though.
[1] https://github.com/freininghaus/notes/blob/master/misc-noteb... or https://nbviewer.jupyter.org/github/freininghaus/notes/blob/...
[2] https://contextlib2.readthedocs.io/en/stable/#contextlib2.Ex...
* [ExitStack] easily scales up to many resources (including a dynamic number that's acquired in a loop)
ctx_mgr = foo() if bar else nullcontext()
with ctx_mgr:
...
if you like. with (ctx_mgr := foo() if bar else nullcontext()):
...https://github.com/vertexproject/synapse/blob/master/synapse...
It grabs a transaction on an arbitrary number of databases, then yields.
https://docs.python.org/3/library/contextlib.html#contextlib...
BTW, Python does have the `__del__` special method. It's just that almost all usage of it would be better implemented via context manager.
Particularly note "finalize provides a straight forward way to register a cleanup function to be called when an object is garbage collected"
The limitation that I still run into is when a class needs to have some cleanup code added to it. In C++, you just add a destructor. In Python, you are forced to choose between non-deterministic release of resources through __del__, or changing the interface by requiring users to use a "with" block. Neither of those is a particularly satisfying option.
If you really want to keep the same interface, add a layer of abstraction. The current interface becomes a wrapper around the underlying context manager.
https://docs.python.org/3.6/library/weakref.html#comparing-f...