15-112 Lecture 6 (Thursday, May 30, 2013)

Formatting strings

One thing we've struggled with so far this semester is getting our printed output to look pretty. Sometimes, for example, we've had to use multiple print lines to ensure that the "+" operator was correctly understood to be string concatenation, not incorrectly understood to be addition of a string and an integer.

One thing that can help here is Pythons ability to format strings. It inherits this ability from C and UNIX environments. It involves the use of type-specific placeholders to lay out strings. The most common examples we'll see will be %s (string), %d (decimal number), and %f (float-point number). Consider the example below:


heightFt = 5
heightIn = 4
fname = "Gregory"
lname = "Kesden"
line = "%s, %s: %dft %din" % (lname, fname, heightFt, heightIn)
print line

# The output will be:
# Kesden, Gregory: 5ft 4in
  

Notice that we have used the first %s as a placeholder for lname, the second %s as a placeholder for fname, the first %d as a placeholder for heightFt, and the last %d as a placeholder for heightIn.

Notice that the placeholders are ordered and that the values to be plugged into them are provided via a tuple: "(lname, fname, heightFt, heightIn)". This tuple needs to have the values for the placeholders in the correct order. They need to be of the corresponding types. And there needs to be exactly one value per placeholder -- no extra and not too few. Notivce also the %-sign that separates the string from the value tuple.

The formatting language is very rich. You can check the standard Python documentation or tutorials for more information -- and we'll learn more, a bit at a time, as we bump into the need. For now just note that, if you want a literal %-sign within a string, it is represented as %%, two percent-signs in a row. If you want two -- use four. Etc.

Getting More Control over Exceptions

The code below illustates the syntax for handling multiple exceptions within one "except:" block. Note that we are providing a tuple of exceptions.


def sumValuesFromFile(fileName):

  sum = 0

  try:
    f = open(fileName)
    lines = f.readlines()

    for line in lines:
      sum = sum + int(line)
  except (IOError,ValueError):
    print "Either the file couldn't be opened, or a value wasn't a number."

  return sum

fileName = raw_input("filename> ")
print sumValuesFromFile(fileName)

  

But, the example above is somewhat of a mess. We really don't want to handle these two exceptions the same way, we want to handle them differently. Notice how they are separate out and handled differently in the code below. We can have as many "except:" blocks as we'd like:


def sumValuesFromFile(fileName):

  sum = 0
  
  try:
    f = open(fileName)
    lines = f.readlines()

    for line in lines:
      sum = sum + int(line)
  except IOError:
    print "Specified file (" + fileName + ") could not be opened."
    return 0
  except ValueError:
    print "Ignoring non-integer value: " + line + "."

  return sum

fileName = raw_input("filename> ")
print sumValuesFromFile(fileName)
      
  

Note that the example above now behaves more appropriately in the case of each type of problem:


[gkesden@unix13 ln]$ ./example.py 
filename> asddassda
Specified file (asddassda) could not be opened.
0
[gkesden@unix13 ln]$ ./example.py
filename> input.txt
Ignoring non-integer value: a
.
16
  

Raising Exceptions and Exception Control Flow

When an exception is raised, the program's normal control flow is turned off. The program doesn't flow linearly downward, skiping code as directed by if-elif-else, looping as directed by while or for, jumping to functions as they are called, and/or returning with or without a value to calling functions.

Instead, if the exception is unhandled, control will immediately return to the place where the current function was called -- but without returning a value. If the exception is unhandled there, it will return to that functions caller, allowing it the opportunity to handle the exception, and if left unhandled, return to its caller. If an exception isn't handled in the main body of the program, outside of any of the functions, the program ends and prints the familiar error messages. This call "call stack" shows the exception's path from the location where it was initally raised -- through each of the functions in the call chain, all the way out.

To "raise" an exception, we can use "raise". Check out the example below where we "reraise" an excpetion after partially handling it:


#!/usr/bin/python

def sumValuesFromFile(fileName):

  sum = 0
  
  try:
    f = open(fileName)
    lines = f.readlines()

    lineNumber=1
    for line in lines:
      sum = sum + int(line)
  except IOError:
    print "Specified file (" + fileName + ") could not be opened."
    return 0
  except ValueError as ve:
    print "File corrupt at line " + lineNumber + "."
    print "Line contains non-integer as shown:"
    print line,

    raise ve # Notice that we are re-raising the same exception we just caught

  lineNumber = lineNumber + 1

  return sum

fileName = raw_input("filename> ")
print sumValuesFromFile(fileName)
      
  

Note that, in the output below, you see the error message from the sumeValuesFromFile() function's handling of the exception -- and also the error from the exception being unhandled where this function is called.


./example.py
filename> input.txt
Traceback (most recent call last):
  File "./example.py", line 29, in 
    print sumValuesFromFile(fileName)
  File "./example.py", line 18, in sumValuesFromFile
    print "File corrupt at line " + lineNumber + "."
TypeError: cannot concatenate 'str' and 'int' objects
  

Notice that, in the example below, we catch this exception again in the main part of the Python script. Also, notice the use of the type() function to discover the type of an exception.


#!/usr/bin/python

def sumValuesFromFile(fileName):

  sum = 0
  
  try:
    f = open(fileName)
    lines = f.readlines()

    lineNumber=1
    for line in lines:
      sum = sum + int(line)
  except IOError:
    print "Specified file (" + fileName + ") could not be opened."
    return 0
  except ValueError as ve:
    print "File corrupt at line " + lineNumber + "."
    print "Line contains non-integer as shown:"
    print line,

    raise ve # Notice that we are re-raising the same exception we just caught

  lineNumber = lineNumber + 1

  return sum

try:
  fileName = raw_input("filename> ")
  print sumValuesFromFile(fileName)
except Exception as e:
  print type(e) + ": Can't print sum -- fatal error"
  print e
  

Below is an example of the output when run:


./example.py
filename> input.txt
Traceback (most recent call last):
  File "./example.py", line 32, in 
    print type(e) + ": Can't print sum -- fatal error"
TypeError: unsupported operand type(s) for +: 'type' and 'str'
  

Exceptions as Objects Defined By Class Specifications

Exceptions are objects. We'll learn a little bit more about what that means in detail toward the end of the session. But, for now, it is useful to observe that, like strings, they are rich aggregations of data and "methods" that act upon it.

Objects are "instances of a class". In other words, we define the properties of a type of object, e.g. a class of object, and then we create an actual object that has those properties. The idea is much like the idea of an architect specifying a house -- that a builder later builds.

In our case, we specify a program -- that Python interprets. And, we specify the properties objects, that we can ask Python to create for us.

A sedan is a type of car, and a car is a type of vehicle. A wagon is another type of car, which is another type of vehicle. A truck is a type of vehicle, but not a type of car. What is the idea here? We can have classes within classes.

Every exception is a type of Exception. For example, ValueError and IOError are both types of exceptions. And, if we create our own exceptions, they too will be types of Exception. Being a type of Exception enables somethign to be raised and handled.

Defining our Own Types of Exceptions

Defining our own type of exception is very formulaic. We tell Python the we want to create a new class of object, that it is a type of the exisiting Exception class of object, and then include some broiler plate code that desxcribes how our exception should be initalized and represented as a string, for example, to be printed, if needed.

We'll learn to understand the broilerplate later. For now, any time you want to create your own type of exception, just copy the example below, and replce "MyError" with whatever name you'd like to give it. By convention, the name should end with "Error".

Notice the key word "class". This line, "class Error(Exception):" lets Python know that we are defining a class of object called MyError that is a subclass of Exception, which it already understands.


  class MyError(Exception):
    def _init_(self, value):
      self.value=value
    def _str_(self):
      return repr(self.value)
  

In the example below, we create an exeception, raise it, and handle it:


class CorruptInputFileError(Exception):
  def _init_(self, value):
    self.value=value
  def _str_(self):
    return repr(self.value)

def sumValuesFromFile(fileName):

  sum = 0
  
  try:
    f = open(fileName)
    lines = f.readlines()

    lineNumber=1
    for line in lines:
      sum = sum + int(line)
  except IOError:
    print "Specified file (" + fileName + ") could not be opened."
    raise IOError
  except ValueError as ve:
    print "File corrupt at line " + str(lineNumber) + "."
    print "Line contains non-integer as shown:"
    print line,
    raise CorruptInputFileError(fileName)

  lineNumber = lineNumber + 1

  return sum

try:
  fileName = raw_input("filename> ")
  print sumValuesFromFile(fileName)
except IOError as ioe:
  print "Couldn't open input file."
except CorruptInputFileError as cife:
  print  "Non-integer value in input file"
  

Below is one more example from class:


MAX_ATTEMPTS = 3
FACULTY = ["Cortina", "Kaynar", "Kesden", "Kosbie"]

class MenuChoiceError(Exception):
  def __init__(self,value):
    self.value = value
  def __str__(self):
    return repr(self.value)


def getInput(maxInputNumber):

  for attempt in range (MAX_ATTEMPTS):
    input = raw_input ("input> ")

    if (input == "q"):
      return input

    try:
      number = int(input)
    except:
      continue

    if ( (number >= 0) and (number <= maxInputNumber)):
      return number

  raise MenuChoiceError(input)

while (True):
  try:
    index = getInput(len(FACULTY)-1)
    if (index == "q"):
      break
    print FACULTY[index]
  except MenuChoiceError as me:
    print "3 strikes -- and your out!"
    raise me
    break