Handling Exceptions
Ø Exception handling is a mechanism for
stopping normal program flow and continuing at some surrounding context or code
block.
Ø The event of
interrupting normal flow is called the act of raising an exception.
Ø In some enclosing context the raised
exception must be handled upon which control flow if transferred to the exception handler. If an exception propagates up the call stack to the start of
the program, then an unhandled exception will cause the program to terminate.
Ø And exception
object containing information about where and why an exceptional event
occurred is transported from the point at which the exception was raised to the exception handler so that the handler can
interrogate the exception object and take appropriate action.
Ø Try-
Except construct can
be used to handle exception. Both the try and except keywords introduce new
blocks. The try block contains code that could raise an exception, and the
except block contains the code which performs error handling in the event that
an exception is raised.
Ø Each try block can have multiple
corresponding except blocks, which intercept exceptions of different types.
Ø When multiple exception handlers have same
code duplication we can collapsing them into one using the ability of the
except statement to accept a tuple of exception types.
def
convert(s):
try:
x
= int(s)
print("conversion success")
except(ValueError,TypeError):
print("Conversion failed")
Ø Almost anything that goes wrong with the
Python program results in an exception, but some
such as IndentationError, SyntaxError, and NameError are the result of
programmer errors, which should be identified and
corrected during development rather than handled at runtime. The fact
that these things are exceptions is mostly useful if you're creating a Python
development tool such as a Python IDE, embedding Python itself in a larger
system to support application scripting, or designing a plug-in system, which
dynamically loads code.
Ø The
pass Statement: It’s a special statement which does
precisely nothing. It's a NOOP, and its only purpose is to allow us to construct syntactically permissible blocks which are
semantically empty.
Ø Named
Reference to exception Object: To get ahold of the exception object and interrogate it for
more details of what went wrong, we can get a named reference to the exception
object by tacking an “as” clause onto the end of the except statement.
Ex:
Ex:
In [108]: import sys
...: def convert(s):
...: try:
...: x = int(s)
...: print("conversion success")
...: except(ValueError,TypeError) as e:
...: print("Conversion failed:
{}".format(str(e)),file=sys.stderr)
...: return -1
In [109]: convert("Sukul")
Conversion
failed: invalid literal for int() with base 10: 'Sukul'
Out[109]: -1
Out[109]: -1
Above
shows how to print to standard error. First we import sys module and pass
sys.stderr as the keyword argument called file to print function.
Also note that exception objects can be converted to strings using the str constructor.
Also note that exception objects can be converted to strings using the str constructor.
Ø Re-raising
Exceptions: We
can re-raise the exception object we're currently handling simply by using the
‘raise’ statement at the end. Without a parameter, raise
simply re-raises the exception that is being currently handled.
This can be useful when we want to log some information before raising the exception.
This can be useful when we want to log some information before raising the exception.
Ø Exceptions
are part of API of the function: Exceptions form an important aspect of the API of a function.
Callers of a function need to know which exceptions to expect under various
conditions so that they can ensure appropriate exception handlers are in place.
In fact we should also modify the docstring to make it plain which exception
type will be raised and under what
circumstances. The exceptions which are raised are
as much a part of a function's specification as the arguments it accepts,
and as such must be implemented and documented appropriately.
Ø Standard
Python Exceptions: Python
provides us with several standard exception types to signal common errors. If a
function parameter is supplied with an illegal value, it is customary to raise
a ValueError. We can do this by using the raise keyword with a newly created exception object,
which we can create by calling the ValueError
constructor. The ValueError constructor accepts an error message.
In [114]: import sys
In [115]: def cubeme(x):
...: if
x < 0:
...:
raise ValueError("Dont want to work with negative numbers")
...: return
x * x * x
In [116]: try:
...:
cubeme(1)
...:
cubeme(97)
...:
cubeme(-1)
...:
except ValueError as v:
...:
print(v,file=sys.stderr)
Dont
want to work with negative numbers
Ø There are a handful of common exception
types in Python, and usually when we need to raise an exception in our own code
one of the built-in types is a good choice.
o
IndexError is
raised when an integer index is out of range. You can see this when you index
pass the end of a list.
o
ValueError is
raised when the object is of the right type, but contains an inappropriate
value.
o
KeyError is
raised when a look-up in a mapping fails
Ø Do
not guard against Type Errors: doing so runs against
the grain of dynamic typing in Python and limits the reuse potential of the code
that we write.
If a function works with a type, even one you couldn't have known about when you designed your function, then that's all to the good. If not, execution will probably result in a TypeError anyway.
If a function works with a type, even one you couldn't have known about when you designed your function, then that's all to the good. If not, execution will probably result in a TypeError anyway.
Ø EAFP
vs LYBL: Only two approaches to dealing with a
program operation that might fail.
o
The
first approach is to check that all the preconditions
for a failure-prone operation are met in advance of attempting the operation.
o
The
second approach is to perform the operation but be prepared
to deal with the consequences if it doesn't work out.
In Python culture, these two philosophies are known as
o
Look
Before You Leap, LBYL, and
o
It's
Easier to Ask Forgiveness than Permission, EAFP
Python
is strongly in favor of EAFP because it puts primary logic for the happy path
in its most readable form with deviations from the normal flow handled
separately rather than interspersed with the main flow.
Problem with LYBL is that we need to think of all the preemptive
checks before performing the risky operation.Also, there is a chance of a race condition
(atomicity issue). Things might change between the check and the actual risky
operation.
Ex: we may check for file existence with a pre-emptive test, however file may
get deleted by another process between the check and actual use of the file in
our code.
With Pythonic EAFP approach, we simply attempt the operation
without checks in advance, but we have an exception handler in place to deal
with any problems. We don't even need to know in a lot of detail exactly what
might go wrong.
EAFP is standard in Python, and that philosophy is
enabled by exceptions.
Without
exceptions, that is using error codes instead, you are forced to include error
handling directly in the main flow of the logic. Since
exceptions interrupt the main flow, they allow you to handle exceptional cases
non-locally. Exceptions coupled with EAFP are also superior because
unlike error codes exceptions cannot be easily ignored. By default, exceptions
have a big effect whereas error codes are silent by default.
Ø Try-finally: Code in the finally-block is executed
whether execution leaves the try-block normally by reaching the end of the
block or exceptionally by an exception being raised. So finally block can be
used to perform a cleanup action irrespective of whether an operation succeeds.
v Summary:
Ø The raising of an exception
interrupts normal program flow and transfers control to an exception handler.
Ø Exception handlers are
defined using the try…except construct. Try blocks define a context in which
exceptions can be detected. Corresponding except blocks define handlers for
specific types of exceptions.
Ø Except blocks can capture an
exception object, which is often of a standard type such as a ValueError,
KeyError, or IndexError.
Ø Programmer errors such as
indentation error and syntax error should not normally be handled.
Ø Exceptional conditions can be
signaled using the raise keyword, which accepts a single parameter of an
exception object. Raise without an argument with an except block re-raises the
exception which is currently being processed.
Ø We tend to not to routinely
check for TypeErrors. To do so would negate the flexibility afforded to us by
Python's dynamic type system.
Ø Exception objects can be
converted to strings using the str() constructor for the purposes of printing
message payloads.
Ø The exceptions thrown by a
function form part of its API and should be appropriately documented.
Ø When raising exceptions,
prefer to use the most appropriate built-in exception type.
Ø Cleanup and restorative
actions can be performed using the try…finally construct, which may optionally
be used in conjunction with except blocks.
Ø Output of the print()
function can be directed to standard error using the optional file argument