mailRe: RelaxWarnings and RelaxErrors.


Others Months | Index by Date | Thread Index
>>   [Date Prev] [Date Next] [Thread Prev] [Thread Next]

Header


Content

Posted by Chris MacRaild on August 15, 2006 - 12:22:
On Tue, 2006-08-15 at 15:58 +1000, Edward d'Auvergne wrote:
I'm not so keen about the 'try:' system.  For small programs it works
beautifully.  However within relax exceptions can be quite difficult
to handle.  For example handling an exception thrown by thread (in
threading mode this is a major headache that I haven't fully solved).
This will come up again if MPI is implemented.  Another issue is
exception handling when there is a mixture of Python and C.  It would
be useful if we could precisely define this behaviour.  A grep for try
statement should show how much of a hack the try system is within
relax.  If there are any useful properties of exceptions that are lost
though - then it may not be worth dropping the system (unless we can
mimic anything that seems useful).


I know very little about threading, and only slightly more about MPI, so
I don't really understand the basis of the problems, but it seems to me
that whatever happens with relax errors, there will always be builtin
Python exceptions that need to be dealt with. As such these are problems
that need to be resolved whatever is decided about relax errors. Given
that, the benefits of Python's exception handling are worth keeping
because we are stuck with their disadvantages.

The only problem with exceptions is that they break inner loops and
terminate all functions nested below the try statement so that
continued execution from within the loop or nested functions is not
possible - it must be restarted.  This can be both good and bad.  

I agree with all of this. My point is that when this behaviour is good,
it is very good. When it is bad, it is easily fixed by putting the error
prone code in an appropriate try statement. As long as the try statement
is at a sufficiently low level in stack, the error can be dealt with and
the opperation that failed can be continued or retried as appropriate. 


This means that while its easy to
generate classes of error with specific and complex behaviour, its not
easy to override that behaviour in a specific context.

The __call__() function can be defined specifically at runtime within
the 'error.py' file.  But if necessary any part of relax can override
it with a new function (although that should probably not be done).


This isn't quite what I mean. Consider some function in relax that is
called in many different contexts. Imagine that it is prone to a
particular type of error, which we signal by a RelaxError. Now it is
quite possible that the correct response to that error depends on the
context in which we are calling this function. eg. in some cases the
error might signal a fatal error in the data that the user must fix,
while in other contexts it might be something simple that can be fixed
silently and re-tried. If the error is an exception, in the first
context, the code which calls the function will simply do so without
catching the error so it is propagated to the user. In the second case,
the code will call the function in a try statement, with the fix in the
except clause. On the other hand, if relax errors are just ordinary
functions that are called when an error arises, there is no easy way of
dealing with these two cases - the error has the same effect independent
of the context from which it is called. It is this lack of flexibility
that concerns me when we talk about not using exceptions for our error
handling.

This could be rectified by a flag, so the following examples will have
very similar functionality to the try-raise-except paradigm but
without the breaking of inner loops and functions:

RelaxBadError('Hello', exit=1)   # Print the error to stderr and quit.
RelaxBadError('Hello', exit=0)   # Print the error to stderr but
continue operating as if nothing happened.

This allows the function in which the error occurs some flexibility in
error handling. My point above was that any function is ignorant of the
context in which it was called, so may well be ignorant of the
appropriate way of dealing with its own errors, because this may well be
dependent on that context. There are two possible ways of dealing with
this that I can see. One involves the Python Exception system, and the
other involves a complex series of error codes passed back and forwards
between functions. The fact that we don't have to resort to this latter
option is one of the reasons that Python code is so clear, simple and
easily maintained and debugged.

The other point is that we should never raise a RelaxError and then
continue as if nothing happened - the whole point of an error is to say
something unexpected has happened and must be dealt with in some way. If
the 'error' doesn't need dealing with (ie. if we can continue as if
nothing happened), then it is no error at all (possibly its worth a
warning, if it reflects something the user might want to be aware of, or
a debug statement if it might cause real errors later on). 

And
there are only two real behaviours I see as necessary for an error
system:  printing a message to stderr and terminating execution.
Possibly returning an error code to the shell would be useful as well.

Here I disagree. Exception handling systems must be able to alter
program flow in appropriate ways so that the event causing the exception
can be dealt with. When we are dealing only with fatal user errors, then
printing a message and termination is quite adequate, but we also need
to be able to deal well with errors that can be silently fixed, and this
requires flow control.


I'm not sure how this could be achieved. The defining feature of an
exception is that it stops the normal program flow and starts looking
for an appropriate handling mechanism (ie. an appropriate except
statement) by propagating up the stack of function calls, examining each
calling function in turn for the applicable handler. Calling a function,
by definition, can only extend the stack downwards, it cannot regress up
the stack in the way an exception does.

I think that the raise statement generates a traceback object which
keeps a copy of the stack at the position of the raise statement - as
well as a few other details.  However if the RelaxBadError, etc are
called the current stack at that position is the sum of that usually
held in the traceback and all the downstream functions.  The
separation is where the forth element of the stack 'stack[i][3]'
matches RelaxBadError, etc.  This is an easy trimming of the stack.
During normal script execution I would like to further trim the stack.
 When an error occurs in a user function the traceback includes all
the functions occurring within the prompt interface.  These really
don't need to be printed if the error occurs further downstream.  This
type of trimming isn't possible within the exception system.

Adding a few more elements to the stack as RelaxBadError does isn't a
problem as these can be ignored (I'm sure Exception does the same but
just uses the stack stored in the traceback object).  The only
difference is that the handling mechanism with the Exception objects
is above in the stack whereas in new system the handling mechanism is
lower down in the stack.  This, importantly, allows continuous
operation if desired.

Another reason is that a RelaxError based on the Exception class is
difficult when scripting.  The try statement will catch it and ignore
it, but the execution of the script cannot be continued.  Then again,
is there a situation where ignoring errors and continuing program
execution would be useful?

I dont follow this. Try/except statements will only catch the exceptions
you tell them to catch, all others will be propagated. Clearly it is a
bad idea to cast except statements too broadly, but its an easy problem
to fix in the code.

The raise statement breaks the inner loops and function calls.  It's
just not possible to continue the script execution.  This is because
within 'prompt/interpreter.py' the 'try' function attempts to run the
'execfile()' function which executes the script.  A raise statement
within relax kills the execfile() function.  If the raise statement
occurs 1/3 of the way through the script - it's not possible to catch
this and then continue with the other 2/3's because we have moved
above the execfile() position of the stack.  The try-raise-except
paradigm is a very strong breaking system where you can only partially
mitigate the damage using the try statement.  Smooth continuous
execution is just not possible.

Of course, once you get to the try statement in prompt/interpreter.py it
is too late to go back, but there is nothing to stop you putting a try
satement in the inner loop itself, if that is the best place for it.
Then the exception can be caught, fixed and everything will continue,
just as you want. Exceptions should only get to the try statement in
prompt/interpreter.py if there is no way of fixing the problem. If that
is the case then we don't ever want to continue the script, because we
know there is a problem that can't be fixed.


In addition to the options we've already considered, there is the
possibility of coding appropriately rich behaviour into a replacement
function for sys.excepthook. This is the function that is called when 
an
exception is not caught by an appropriate handler. It's default
behaviour is to print the traceback and display the error. It is 
passed
both the exception class and the specific instance that was raised, as
well as the traceback object. This should be enough to get some quite
rich behaviour without too much hacking, I think.

I wonder, what is more important - the 'raise' semantics, the
Exception objects, and all the related behaviour or simply the
printing out to stderr?  All the ancillary relax specific behaviour
can be implemented in both systems.  But what are we trying to really
achieve with the RelaxErrors?


Again I make the distinction between user level errors and exceptions.
From my point of view the important thing that errors must achieve for
the user is clear communication of the problem and useful debugging
features like saving state. Clearly Exception objects cannot achieve all
of this on their own, but I think we can code Exception handling
features into relax which do achieve these things. Downgrading an error
to a warning is more difficult to achieve, but is not necessary if
warnings are used where appropriate. I can only imagine wanting to
downgrade an error if I didn't think it should have been an error in the
first place, so better to raise a warning at that point.

Both systems can be modified to suit.  I would never recommend
downgrading an error when using the prompt or scripting but it may be
useful within some other interface, for example a GUI.

Exceptions must achieve something very different inside relax - they
need to communicate unexpected results up the stack of function calls to
the point where they can be dealt with. Exceptions only become errors
when there is no way of dealing with them. As far as I am aware, the
only reasonable way of achieving this is through the Python Exception
propagation mechanism.

I think this has been successfully accomplished with the probationary
RelaxErrorSystem class.  The relevant part of the stack is printed to
stderr, the error message is printed to stderr, and the program
terminates.  However when debugging is turned on, the state is also
saved.

None of this communicates unexpected results up the stack. They are very
good for communicating with the user, but cannot communicate errors to
the code that might be able to fix them

  I've also put into place the warning system which doesn't
print the stack, prints the warning to stderr, and the program
continues execution.  I've also added two functions - one upgrades the
warnings so that sys.exit(1) is called, the other downgrades the
errors so that sys.exit(1) is not called.  The interesting property of
this new system is that smooth continuous operation is guaranteed.
This is because it moves down the stack (which can be reversed) rather
than up as the raise statement mandates (which cannot be reversed).
The more interesting property from my point of view is that the
warning and error systems are very similar.  The Python Exception and
Warning systems are incompatible.

Because they do two very different things. Warnings communicate a
potentially dangerous state to the user without affecting program flow.
Exceptions communicate some event (usually unexpected) that needs to be
dealt with by the program. This often requires that program flow be
disrupted in quite specific ways, so that the affected opperation can be
backed up and tried again. Exceptions only become errors that the user
should know anything about if the program has no way of dealing with the
exception. The systems are incomatible because they need to achieve two
very different things. That said I don't much like the Python warnings
system, and I suspect yours will serve relax's purposes much better.
Where I dissagree is with an exception system that tries to work in the
same way.

  Therefore it makes it very easy to
terminate the program using a flag such as '--pedantic' to enable the
'warning_pedantic' function.

The only two features of the Python Exception system that I can see
are the printing of the stack to stderr and the printing of the error
message to stderr.  


Again, the feature which defines an exception is that it controls
program flow in such a way as to allow the program every opportunity to
fix the problem before bothering the user with it. Yes this means that
you loose the ability to continue blindly through the error, but that is
the point - the error needs to be fixed, then you can think about how to
continue or repaet what you were trying to do. Ideally all of this will
be done programaticaly, with the user kept in blissful ignorance. Only
where this is not possible is it desirable to start printing error
mesages and terminating.


Prevention of an error can be done within both
systems.

Yes, but prevention is not the goal of an exception handling system. 
If we could prevent all errors, then there would be no need for 
exception handling. Keep in mind that it is often easier to ask 
forgivness than permission. That is it is often better to fix an error
after the event than to try to do exhaustive error checking before hand.

This is one of the reasons why exception handling in other languages is
often different than in Python. Most languages tend to prefer the 'look 
before you leap' idiom of error handling. There are several good reasons 
why python prefers a more forgiveness based approach. Some relate to the 
things I've said above, others include:
1) It is often impossible to anticipate all neccessary checks
2) A long list of checks diminishes the readability and maintainabity of
code
3) In something as complex as relax, the relevant state may change between 
when the checks occured and the operation is attempted.

To try and sum up my position: I like the Warnings system as it is 
proposed - it will probably offer more clarity and flexibility than the 
Python Warnings. I'm open to the idea that the proposed RelaxError system 
do the job of handling communication with the user and debugging jobs when
fatal errors arise. I'm not at all happy with the idea that the proposed
RelaxError system might replace Python Exceptions for internal exception 
handling within relax. I believe that exceptions need to do a job which 
is fundamentally different from the user communications role of the 
proposed system.

Chris




Related Messages


Powered by MHonArc, Updated Wed Aug 16 10:00:34 2006