Python Programming Made Easy (2016)

Chapter 12: Exception Handling & Generators

An exception is an event, which occurs during the execution of a program, that disrupts the normal flow of the program. In general, when a Python script encounters a situation that it can't cope with, it raises an exception. An exception is a Python object that represents an error.

Some unexpected condition(s) that are not part of normal operations planned during coding are:

·   Out of Memory

·   Invalid filename

·   Attempting to write into read only file

·   Getting an incorrect input from user

·   Division by zero

·   Accessing an out of bound list element

·   Trying to read beyond end of file

·   Sending illegal arguments to a method

If any of such situations is encountered, a good program will either have the code check for them and perform some suitable action to remedy them, or at least stop processing in a well defined way after giving appropriate message(s). So what we are saying is if an error happened, there must be code written in program, to recover from the error. In case if it is not possible to handle the error then it must be reported in user friendly way to the user.

Exception handling

Exceptions handling is the process of responding in such situations. Most of the modern programming languages provide support with handling exceptions. They offer a dedicated exception handling mechanism, which simplifies the way in which an exception situation is reported and handled.

For handling exceptional situations python provides

1. raise statement to raise exception in program

2. try..... except statement for catching and handling the errors.

When a Python script raises an exception, it must either handle the exception immediately otherwise it would terminate and come out. If we have some suspicious code that may raise an exception, we can defend our program by placing the suspicious code in a try: block. After the try: block, include an except: statement, followed by a block of code which handles the problem as elegantly as possible.

Syntax:

try:

    ......................

except ExceptionA:

except ExceptionB:

     ......................

else:

  No exception

A single try statement can have multiple except statements. We can also provide a generic except clause, which handles any exception. After the except clause(s), we can include an else-clause. The code in the else-block executes if the code in the try: block does not raise an exception.

try:

  n = input("Enter year")

  if n<1000 or n>2015:

      raise "Invalid year",n

  else:

      month = input("Enter month")

      if month<or month>12:

          raise "Invalid month",month

      else:

          day = input("Enter day")

          if day<or day>31:

              raise "Invalid day",day

          else:

               print day,'/',month,'/',n

except:

      print "Wrong Values"

We can also use the same except statement to handle multiple exceptions as follows.

try:

      ......................

except(Exception1[, Exception2[,...ExceptionN]]]):

   ......................

else:

    no exception

The finally block is a place to put any code that must execute, whether the try-block raised an exception or not. The syntax of the try-finally statement is:

try:

   ......................

finally:

     ......................

When an exception is thrown in the try block, the execution immediately passes to the finally block. After all the statements in the finally block are executed, the exception is raised again and is handled in the except statements if present in the next higher layer of the try-except statement.

An exception can have an argument, which is a value that gives additional information about the problem. The contents of the argument vary by exception.

try:

   ......................

except ExceptionType, Argument:

   ...

Raise

We can raise exceptions in several ways by using the raise statement. The general syntax for the raise statement.

raise [Exception [, args [, traceback]]]

Here, Exception is the type of exception (for example, NameError) and argument is a value for the exception argument. The argument is optional; if not supplied, the exception argument is None.

The final argument, traceback, is also optional (and rarely used in practice), and if present, is the traceback object used for the exception.

User-Defined Exceptions

Python also allows us to create our own exceptions by deriving classes from the standard built-in exceptions. For example, let’s take a class that is subclassed from RuntimeError. This is useful when you need to display more specific information when an exception is caught.

A raise statement that does not include an exception name will simply raise the current exception. Here exception name is the type of error, which can be string, class or object and argument is the value. As argument is optional its default value None is used in case the argument is not provided.

S.No

ErrorType

Description

1

IOError

I/O operation fails

2

EOFError

A file method tries to read beyond the file.

3

ZeroDivisionError

Denominator in division is zero.

4

NameError

Variable/function name is not found

5

IndexError

Index/Subscript is out of range

6

ImportError

Import statement fails to find the module definition or file name

7

IndentationError

Incorrect indentation

8

TypeError

Perform an operation on incorrect type of value

9

ValueError

Built-in function receives an argument of correct type but incorrect value

Table 12.1: Types of exceptions

Example:

>>>= [11,22,33]

>>>= input(“Enter index”)

>>> if i > len(l):

       raise IndexError

       else:

            print l[i]

If index value is 5, the code will raise an error, during execution. The error will be index error as mentioned in raise statement.

>>>def menu(choice):

if choice < 1:

    raise "invalid choice", choice

In order to handle the errors raised by the statement, we can use try except statement.

try.....except is used when we think that the code might fail. If this happens then we are able to catch it and handle same in proper fashion. Let's re-consider the menu() function defined above to handle the error also:

while True:

try:

       x = int(raw_input("Enter a number"))

       break

except ValueError:

             print " This was not a valid number. Enter a valid number"

This code will ask user to input a value until a valid integer value is entered.

Once while loop is entered execution of try clause begins, if no error is encountered, i.e. the value entered is integer, except clause will be skipped and execution of try finishes. If an exception occurs i.e. an integer value is not entered then rest of try clause is skipped and except cause is executed.

Let’s take an example of exception handling in data file.

try:

= open("testfile", "w")

f.write("This is my test file for exception handling!!")

except IOError:

print "Error: can\'t find file or read data"

else:

  print "Written successfully"

This will produce the following result, if you are not allowed to write in the file :

Error: can't find file or read data

Generator functions

When a normal python function is called, in a program, the control from calling function is shifted to called function. Execution of called function starts from first line and continues until a return statement or exception or end of function is encountered. In all these situations, the control is returned back to caller function. That means any work done by the called function for providing the result is lost. To use it again we will again call the function and its execution will start from scratch.

Sometimes in programming we need to have functions which are able to save its work, so that instead of starting from scratch the function starts working from the point where it was left last. In other words, a function should be able to yield a series of values instead of just returning a single value. Here returning a value also implies returning of control.

Such functions are Generator functions. These functions can send back a value and later resume processing from the place, where they left off. This allows the function to produce a series of values - over time, rather than computing them all at once and returning a list of values.

Generator functions are not much different from normal functions, they also use def to define a function. The primary difference between generator and normal function is that generator will yield a value instead of returning a value.

Theyield suspends the function and sends a value back to the caller while retains enough state to enable the function immediately after the last yield run. This allows the generator function to produce a series of values over time rather than computing them all at once and sending them back in a list.

Let’s generate the cubes of numbers.

Example 12.1 : Generating cube of numbers

>>> def cube_generator(n):

              for i in range(n):

                            yield i ** 3

it's used in a for loop, control returns to the function after its yield statement each time through the loop:

>>> for i in cube_generator(5):

                  print i,             # Python 2.x

Output:

0 : 1 : 8 : 27 : 64 :

If we use return instead of yield, the result is:

>>> def cube_generator(n):

              for i in range(n):

                            return i ** 3

Traceback (most recent call last):

  File "", line 1, in

    for i in cubic_generator(5):

TypeError: 'int' object is not iterable

The function yields a value and so returns to its caller each time through the loop. When it is resumed, its prior state is restored and control picks up again after the yield statement. When

Example 12.2: Fibonacci series

def fibonacci(max):

               a, b = 0, 1            # 1

              while a < max:

                         yield a            # 2

                        a, b = b, a + b    # 3

Let’s run through this code.

1.                It starts with 0 and 1, goes up slowly at first, then more and more rapidly. To start the sequence, we need two variables: a starts at 0, and b starts at 1.

2.                a is the current number in the sequence, so yield it.

3.                b is the next number in the sequence, so assign that to a, but also calculate the next value a + b and assign that to b for later use. Note that this happens in parallel; if a is 3 and b is 5, then a, b = b, a + b will set a to 5 (the previous value of b) and b to 8 (the sum of the previous values of a and b).

We can use a generator like fibonacci() in a for loop directly. The for loop will automatically call the next() function to get values from the fibonacci() generator and assign them to the for loop index variable (n). Each time through the for loop, n gets a new value from the yield statement in fibonacci(), and all we have to do is print it out. Once fibonacci() runs out of numbers (a becomes bigger than max, which in this case is 500), then the for loop exits gracefully.

Generators can be better in terms of memory usage and the performance. They allow functions to avoid doing all the work up front. This is especially useful when the resulting lists are huge or when it consumes a lot of computation to produce each value. Generator distributes the time required to produce the series of values among loop iterations. Generators can provide simpler alternatives to manually saving the state between iterations in class objects.

Solved Examples

1.       What is exception handling?

Ans. Errors are exceptional, unusual and unexpected situations and they are never part of the normal flow of a program. If an error happened, there must be code written in program, to recover from the error. In case if it is not possible to handle the error then it must be reported in user friendly way to the user.

2.                   When do we get type error?

Ans. When we do not give a number for index,

try:

   num = ['Red','Blue','Green']

   print num['Red']

except TypeError:

    print 'Enter only number for index'

3.                   What are generator functions?

Ans. Generator functions can send back a value and later resume processing from the place, where they left off. This allows the function to produce a series of values - over time, rather than computing them all at once and returning a list of values.

Generator functions are not much different from normal functions, they also use def to define a function. The primary difference between generator and normal function is that generator will yield a value instead of returning a value.

It is the yield statement which allows the generator function to suspend the processing and send a value, simultaneously retaining the existing state of generator to resume processing over time.

4.                   Write a code snippet to generate squares using generator functions.

Ans.

# Squares using generator

def Square (n):

    for i in range (n):

        yield i**2

for k in Square (6):

    print k,

5.                   How is yield different from return?

Ans. Yield is a keyword that is used like return, except the function will return a generator.

6.                   What is the use of raise statement? Mention its syntax.

Ans. Raise statement allows the programmer to force a specified exception to occur. Once an exception is raised, it's up to caller function to either handle it using try/except statement or let it propagate further.

Syntax of raise is:

              raise [exception name [, argument]]

7.       Find out the situation(s) in which following code may crash.

while loop == 1:

       try:

               a = input('Enter a number to subtract from > ')

               b = input ('Enter the number to subtract > ')

       except NameError:

               print "\nYou cannot subtract a letter"

               continue

       except SyntaxError:

               print "\nPlease enter a number only."

               continue

       print a - b

       try:

               loop = input('Press 1 to try again > ')

       except (NameError,SyntaxError):

               loop = 0

Ans.

The code might not crash as all exceptions have been taken care of.

8. Write a function called oops that explicitly raises a Index Error exception when called. Then write another function that calls oops inside a try/except statement to catch the error. What happens if you change oops to raise Key Error instead of Index Error? 

Ans.

def oops():

    l = [1,2,3]

    i = 5

    if i > len(l):

        raise IndexError

    else:

        print l[i]

def handled():

    try:

        oops()

    except(IndexError):

        print "You are trying to access the list with an invalid index"

handled()

Output:

You are trying to access the list with an invalid index

When we raise KeyError, an exception is thrown on the screen.

9. Write a function to find average of a list of numbers. Your function should be able to handle an empty list and also list containing string.                                          

Ans.

l= []

try:

  n = input("Enter number of numbers")

  for i in range(n):

       l.append(input("Enter number"))

  sum =0

  for i in range(len(l)):

      sum = sum+l[i]

  avg = sum/len(l)

  print "Average =",avg

except NameError:

      print "Enter only number"

except TypeError:

      print "Wrong Type"

Practice Questions

1. Find out the situation(s) in which following code may crash

     while loop == 1:

     try:

         a = input('Enter a number to square> ')

     except NameError:

         print "\nYou cannot square a character"

         continue

     except SyntaxError:

         print "\nPlease enter a number only."

         continue

     print a*a

     try:

       loop = input('Press 1 to try again > ')

     except (NameError,SyntaxError):

       loop = 0

2. What is the purpose of yield statement?

3. What are the applications of Generator functions?

4. Write a function to search a list of numbers. Your function should be able to handle a string input.

5. Write a program, to accept a date as day, month & year from user and raise appropriate error(s), if legal value(s) is not supplied. Display appropriate message till user inputs correct value(s).             

6. Create a class student to store student data. Ensure that while accepting the data incorrect entry is properly handled.