开发者

changes inside try-except persist after exception is caught

开发者 https://www.devze.com 2023-02-05 15:14 出处:网络
Since I\'ve learned of exception-handling the first time (not in Python), I was under the impression that when you begin a try block, it\'s like you start writing in a sandbox: if an exception occurs

Since I've learned of exception-handling the first time (not in Python), I was under the impression that when you begin a try block, it's like you start writing in a sandbox: if an exception occurs, everything that happened inside the try block will be like it never happened. To my naive surprise, I noticed this isn't true, or not in the way I thought, at least in Python. Here's my experiment in Python:

>>> a = range(5)
>>> a
[0, 1, 2, 3, 4]
>>> try:
...     a.append(5)
...     oops
... except:
...     raise
... 
Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
NameError: name 'oops' is not defined
>>> print a
[0, 1, 2, 3, 4, 5]

As you can see, I changed the list inside the try block, then trigger an error, which is raised. I was expecting to se开发者_C百科e the list in its original form, [0, 1, 2, 3, 4], but the a.append(5) persisted.

Were my expectations wrong in the first place? Maybe partially wrong expectations(there may be a sandbox, but it doesn't act like that)?


You just discovered that exceptions are not a silver bullet for error handling.

Exceptions do not protect you from state changes... whatever was completed without errors before the exception is thrown will have to be undone. This is how exception work in Python, C++, Java and many other languages as well.

Sometimes you may have a sort of "external" general protection: for example if all you do is changes to a database that supports transactions then you can have the top-level catching statement to do a "rollback" instead of committing the changes and you get the protection you're looking for. If there is no such a natural "wall" protecting from partial state change then things are much more difficult to handle correctly.

The very reason that operations that have been completed will not be undone is what makes the use of exceptions not trivial as the complexity of the problems scales up.

Normally code can be classified exception "safe" at several levels:

  1. In case of an exception everything is ruined and not even a clean exit or restart is possible. This is what is normally classified as NOT exception safe.

  2. In case of an exception the code will not complete its job, and the state of the subsystem (class instance, library) is invalid. You however can restart safely (e.g. you can destroy the instance or reinitialize the library). This is the very minimum exception safety.

  3. In case of an exception the code will not complete its job, and the state of the subsystem will be valid but unspecified. The calling code may try to inspect the current state and keep using the subsystem instead of reinitializing it, for example. Just a little better than 2.

  4. In case of an exception the code will do nothing, leaving the program state untouched. So either the request is completed without errors or an error signal is returned to the caller and nothing has been altered. This is of course the best behavior.

The biggest problem with exception handling is that even if you have two very safe type-4 pieces A and B still the simple sequential composition AB is not safe because in case of a problem in B you must also undo whatever A has completed. Also if it's possible to get an exception when performing 1/A (i.e. when un-doing what A was able to complete) then you're in BIG trouble, because you cannot neither do B, nor restoring the state as it was before (this means it's just impossible to implement AB as a type-4 operation).

In other words good bricks won't make trivial making good constructions (in respect to exception safety).


Your expectation is wrong in just about any language that supports exceptions - there is no all-or-nothing semantics associated with try blocks (although you may have transactional concepts in some languages, e.g. if there is support for transactional memory).

It's only that the part of the try block after the exception is not executed anymore.


Yes, your assumptions were incorrect. One nice thing you can do, though, is get certain types cleaned up using with. See Python's tutorial.

The only place I've seen your idea of sandboxed blocks that are rolled back on errors is in functional languages.


Your expectations are wrong. Perhaps it would be a nice feature (it exists under the name transactions - e.g. in SQL - but it's rare in programming languages and most research happens in languages nobody ever uses), but it's not possible in any language anyone uses (much less efficently - since the compiler/interpreter generally knows very little about what the programmer's doing, you'd have to save the entire program's state and restore that, and that would still miss all side effects outside of the interpreter, e.g. file I/O).

A try block simply means "try to do this, and do skip to doing this if you fail". Note that only the second half is special - if an exception occurs outside of a try block, execution jumps up the call graph up to the next try, or - if there are none - to some global handler that prints the exception and ends execution of the program.

0

精彩评论

暂无评论...
验证码 换一张
取 消