Comparison with False - something I don't understand

Mark Wooding mdw at distorted.org.uk
Fri Dec 3 09:31:43 EST 2010


Steven D'Aprano <steve+comp.lang.python at pearwood.info> writes:

> On Thu, 02 Dec 2010 16:35:08 +0000, Mark Wooding wrote:
> > There are better ways to handle errors than Python's exception system.
>
> I'm curious -- what ways would they be?

The most obvious improvement is resumable exceptions.

In general, recovering from an exceptional condition requires three
activities:

  * doing something about the condition so that the program can continue
    running;

  * identifying some way of rejoining the program's main (unexceptional)
    flow of control; and

  * actually performing that transfer, ensuring that any necessary
    invariants are restored.

Python's `try ... finally' helps with the last; but Python intertwines
the first two, implementing both with `try ... except'.  The most
important consequence of this is that the logic which contains knowledge
about how to fix the condition must be closer to the code that
encountered the condition than the resume point is.  It's therefore hard
to factor out high-level policy about fixing conditions from the
relatively tedious business of providing safe points at which to resume
main execution.  (Effectively, each section of your program which wants
to avail itself of some high-level condition-fixing policy has to
provide its own protocol for expressing and implementing them.)

Phew.  That was abstract.  Can I come up with some examples?

I've been writing a (sort of) compiler recently.  When it encounters
errors, it reports a message to the user containing the position it had
reached in the source, updates a counter so that it can report a summary
at the end of the run and produce a sensible exit status, and then
attempts to carry on compiling as best it can.

The last bit -- attempting to carry on as best it can -- needs local
knowledge of what the compiler's doing and what actually went wrong.  If
the parser expected to find a delimiter, maybe it should pretend that it
found one, for example.

The other stuff, printing messages, updating counters, and so on, is all
done with some condition handlers wrapped around the meat of the
compiler.  That's written only once.  Everything that signals errors,
including standard I/O functions like open-a-file, gets the same
treatment.

(The remaining missing ingredient is a fluid variable which tracks the
current position in the source and is updated by the scanner; bits of
the compiler's semantic analysis machinery will temporarily focus
attention on other parts of the source using locations they saved during
the parse.  Implementing fluids in Python can be done with a context
manager: if you don't care about concurrency then you can use simple
variables; otherwise it's little fiddly and the syntax isn't very
comfortable, but it's still possible.)

A more familiar example, maybe, is the good old DOS `abort/retry/fail'
query.  Implementing such a thing in Python, as a general policy for
handling I/O errors, isn't possible.  Viewed narrowly, this is probably
a good thing: the old DOS query was very annoying.  But the difficulty
of implementing this policy illustrates the missing functionality.  And,
of course, if DOS had a proper resumable exception system, programs
could have overridden the annoying query.

In general, the code which discovers an exceptional condition may have
several options for how to resume.  It might be possible to ignore the
situation entirely and carry on regardless (`false alarm!').  It might
be possible to try again (`transient failure').  Alas, the logic which
is capable of implementing these options is usually too low-level and
too close to the action to be able to decide among them sensibly.
(Maybe a human user should be consulted -- but that can drag in user
interface baggage into a low-level networking library or whatever.)
Resumable exceptions provide a way out of this mess by separating the
mechanics of resumption from policy of which resumption option to
choose.

It's easy to show that a resumable exception system can do everything
that a nonresumable system (like Python's) can do (simply put all of the
recovery logic at the resume point); but the converse is not true.

There are some other fringe benefits to resumable exceptions.

  * It's usual to report a stack backtrace or similar if an exception
    occurs but nothing manages to resume execution.  If unwinding the
    stack is intertwined with working out how to resume execution, then
    whenever you /try/ to run an applicable handler, you have to reify
    the stack context and stash it somewhere in case the handler doesn't
    complete the job.  This makes raising exceptions more expensive than
    they need to be.

  * You can use the same mechanism for other kinds of communication with
    surrounding context.  For example, Python occasionally emits
    `warnings', which have their own rather simple management system
    (using global state, so it's hard to say `don't issue MumbleWarnings
    while we frob the widgets' in a concurrent program).  A resumable
    exceptions system could easily integrate warnings fully with other
    kinds of conditions (and avoid the concurrency problems).  You could
    also use it for reporting progress indications, for example.

Of course, there's a downside.  Resumable exceptions aren't the usual
kind, so people aren't used to them.  I'm not sure whether resumable
exceptions are actually more complicated to understand: there are more
named concepts involved, but they do less and their various roles are
clearer and less tangled.  (The `handler' for an exceptional condition
can be called just like a function.  Python doesn't have nonlocal flow
control distinct from its exception system, but a nonlocal transfer to a
resumption point isn't conceptually very complicated.)

> 1. return a sentinel value or error code to indicate an exceptional case 
> (e.g. str.find returns -1);

This works fine if you consider failure as being unexceptional.  If
you're not actually expecting to find that substring, and have something
sensible to do if it wasn't there, a report that it wasn't there isn't
really exceptional.

(I think I use str.find more frequently than str.index, so it's nice
that there are both.)

> 2. raise an exception (e.g. nearly everything else in Python);

Raising exceptions is a more complicated business than this suggests.
Python made some specific design decisions regarding its exception
system; they're pretty common choices, but not, I think, the best ones.

> 3. set an error code somewhere (often a global variable) and hope the 
> caller remembers to check it;

That's less common than you might think.  Usually there's some sentinel
value to tell you to look at the global error code.  (Obvious examples
where there isn't a clear sentinel: strtol and friends, math.h.)

I think we can agree that this sucks.

Someone else mentioned Erlang.  An Erlang system is structured as a
collection of `processes' (they don't have any shared state, so they
aren't really `threads') which communicate by sending messages to each
other.  If an Erlang process encounters a problem, it dies, and a
message is sent to report its demise.  Erlang processes are very cheap,
it's not unusual for a system to have tens of thousands of them.

> plus some de facto techniques sadly in common use:
>
> 4. dump core;
>
> 5. do nothing and produce garbage output.
>
> What else is there?

You missed `6. assume that erroneous input is actually executable code
and transfer control to it', which is a popular approach in C.

-- [mdw]



More information about the Python-list mailing list