Friday, March 13, 2020

Python - Exceptions


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:
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

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.

Ø 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.

Ø 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.

Ø 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

No comments:

Post a Comment