This is completely incorrect with respect to Rust, both historically and practically. Rust started with unwinding, and then grew opt-in abort machinery for those use cases where it matters. And, for what seems like the millionth time, unwinding does not equal exceptions; this is like saying that C has exceptions because of setjmp/longjmp. Unwinding exists in Rust solely to give one the ability to fail gracefully, running destructors that might have valuable side effects and allowing Rust-written subsystems to avoid taking down the entire system. For an example of the latter, Skylight, who ships a Ruby gem written in Rust, "catches" any unwinding so that an errant failure in their gem does not bring down a client's entire Ruby process. I have been writing Rust for years and, without exaggeration or (heh) exception, have never once seen the ability to "catch" unwinding used as a basis for constructing ad-hoc recoverable exception-handling, especially since abort-on-panic is an entirely valid mode for Rust applications (this is how Dropbox's backend Rust is compiled, for example) and completely obviates any sane attempt to use panics for recoverable error handling.
[0]: https://github.com/urfave/negroni/blob/master/recovery.go#L1...
- Exception -> Rust's panic
Reality:
- Runtime assert -> Rust's panic
- Exception -> Rust's Result (ie error monad)
Ehhh... `with` is a crude substitute for destructors. C++ is a whole language designed around RAII. Python really is not.
You do make a good point that the handling of exceptions in destructors in C++ is problematic. But you can actually throw from destructors, as long as the destructor wasn't called during unwind for another throw. I'm of the opinion that the inability to throw again during unwind is a glaring bug in the standard, since it presumes people can know if their code is going to throw, which is like presuming people know where their bugs are. But the reasonable way to work around it is to squelch exceptions from destructors when another exception is already in-flight. This is usually fine, as the "secondary exceptions" thrown during an unwind are usually side-effects of the "primary exception" anyway.
For example
File.open(args) do |file|
# use file
end
And the implementation is simply def self.open(*args)
file = File.new(*args)
begin
yield file
ensure
file.close
end
endAn other is that Python's "context managers" are reified which means you can pass them around until you ultimately use them. It's a tradeoff.
@contextlib.contextmanager
def open(*args)
# file open
try:
yield file
finally:
file.close()
with open(args) as file:
# use fileBut as nice as it is, to me it's just syntax sugar over the wrong abstraction instead of something beautiful in it's own right.
def self.open
File.open(...) { |f| yield f }
end
Unless they need custom destructor actions, in which case presumably they'd have to implement that manually in python too.What's wrong with indenting? You're entering a scope; indenting is appropriate.
The primary use case ExitStack is not to reduce indentation levels; it's to support "a variable number of context managers and other cleanup operations in a single with statement".
You wrote "The easiest way to handle opens and closes is thus exception-safe".
Consider if you need to merge up to 50 files containing sorted lines, where the result must also be sorted, and where you should not load all of the data into memory first. (Sorted according to UTF-8.) How would you do it? Not so easy, is it?
With ExitStack you could write it as something like the following sketch:
filenames = [ list, of, file, names]
with ExitStack() as stack:
files = [stack.enter_context(open(fname, "rb")) for fname in filenames]
with open("merged_lines.txt", "wb") as output:
output.writelines(heapq.merge(*files))
As you say, the great thing about this with clause is that it Just Works.A side-effect of making ExitStack work is that it might help if the indentation depth is a problem, which mappu apparently has experienced.
As another example of where Python registers a cleanup rather than use a context manager, see https://docs.python.org/3/library/unittest.html#unittest.Tes... .
I ended up an OnExit() context that does:
if conn is not None:
ctx.call(conn.close) from contextlib import contextmanager
@contextmanager
def tag(name):
print "<%s>" % name
yield
print "</%s>" % name
>>> with tag("h1"):
... print("foo")
...
<h1>
foo
</h1>It does not, it works like a C++ destructor, the context management code is static, and anything it needs is provided as a parameter.
While "ad-hoc" context managers are possible (and quite trivial in fact):
1. I don't remember ever wanting one
2. they would not be convenient owing to Python's limited lambdas so
3. you'd probably implement them using a ~decorator on a nested function instead which
4. may look quite odd to most Python developers
Remember, "with" isn't always required - you can also write the code using a try/finally.
If the implied close generates an exception, everything still works reasonably. __exit__ has a "traceback" parameter, which indicates an exception is in progress. An exit operation should do its closeout, then re-raise the exception sent as the traceback, if any. This works even when a destructor raises an exception. (A typical case is closing a network connection. If the other end dies, close will generate an error, which should not be lost.) The default use of "with" in Python thus comes with correct single and nested error handling.
That's not true of RAII or Go's "defer". In C++'s destructors, you can't return an error code, and an exception in a destructor is usually a big problem.
It's hard, but not impossible, for a deferred call in Go to report an error. The deferred function can muck with the parameters of the function which called it. This only works if the deferred function was written specifically for the context in which it was called; just deferring a "close" won't do this. So you don't get proper error handling by default.
(Languages seem to need exceptions. Both Go and Rust started with an always-aborts panic mechanism. That was too drastic in practice, and in both languages, panicking was made recoverable and provided with unwinding machinery. So both languages now have the heavy machinery for exceptions, without the language support for them. This results in kludges to simulate exceptions.)