15-112 Lecture 5 (Wednesday, July 11, 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

fraction = 5.0/4.2
print fraction # The output will have a bunch of decimal places
print "%.2f has two decimal places." % fraction # The output has 2 decimal places
  

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. And, we were also able to format a number with a fractional component to a fixed number of decimals. within a %f placeholder.

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.

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.

The short story here is that, whereas the %-operator upon integers is the modulus operator, the %-operator upon strings is the formatting operator.

Exceptions

Let's consider the following code. I know it doesn't work as written. But, what I'd like to do is to consider how it breaks:


def addNumbers():
  sum = 0

  while (True): 
    number = raw_input("number> ")
 
    sum = sum+int(number)

  return sum

print addNumbers()
  

It adds numbers, so long as it is fed integer numbers -- but then it dies when it is fed anything else:


Traceback (most recent call last):
  File "./example.py", line 13, in 
    print addNumbers()
  File "./example.py", line 9, in addNumbers
    sum = sum+int(number)
ValueError: invalid literal for int() with base 10: 'q'
  

Now, let's considered the revised version below. Any idea what it is doing?


def addNumbers():
  sum = 0

  while (True): 
    number = raw_input("number> ")
 
    try:
      sum = sum+int(number)
    except:
      break

  return sum

print addNumbers()
  

What do you think the try and except do? In effect, try tries to execute the associated block of code as normal, but if there is an error, looks to one or more associated except blocks for how to handle the error. In this case, we handle the error by breaking out of the loop.

So, now our function adds numbers provided by the usr, and lets the user stop by entering a non-number. Pretty cool, huh?

The Exception, as an Object

Exceptions are actually special types of complex entities called, "Objects". They contain data and associated functions, known as "methods".

When the user enters a non-number, and we try to use it as a number, Python generates an exception object to describe this situation. In effect, this object is realized as part of the "except:" block. In the code above, we used a short-hand "except:" syntax that let us know about the existence of the exception, but didn't actually allow us to get our hands on the exception object, itself.

Notice that, in the "except block" below, we use "as" to associate this object with the variable "e", and then print it. This is obviously not helpful to our user -- but it is a good example for us.


def addNumbers():
  sum = 0

  while (True): 
    number = raw_input("number> ")
 
    try:
      sum = sum+int(number)
    except:
      break

  return sum

print addNumbers()
  

The output of the "print e" above, is shown below:


invalid literal for int() with base 10: 'a'
  

Multiple Exception Types

The code below illustates the syntax for handling multiple exceptions within one "except:" block. Note that we are able to look for more than one exception in an except clause by listing them within a tuple. We'll learn more about tuples soon, but, in short, they are unchangable, ordered, ,-comma separated lists defiend within parentheses, e.g. "(4, 5, 6)", or, in the case below, "(ZeroDivisionError,TypeError)".


#!/usr/bin/python

def divideNumbersAndPrint(fmtString, x,y):
  try:
    result = x / y
    newString = fmtString % (result)
  except (ZeroDivisionError,TypeError):
    print "Invalid format string or divide by zero"

divideNumbersAndPrint("Result is %d", 5, 3)
divideNumbersAndPrint("Result is %f", 5.0, 3.0)

divideNumbersAndPrint("Result is %f %f", 5.0, 1.0)

divideNumbersAndPrint("Result is %f", 5.0, 0.0)
  

The example below is somewhat similar, except that we use multiple except blocks, which enables us to handle each exception differently:


#!/usr/bin/python

def divideNumbersAndPrint(fmtString, x,y):
  try:
    result = x / y
    newString = fmtString % (result)
  except ZeroDivisionError:
    print "Undefined" # As we called the result fo dividing by zero in high school
  except TypeError: 
    print result # Without broken formatting

divideNumbersAndPrint("Result is %d", 5, 3)
divideNumbersAndPrint("Result is %f", 5.0, 3.0)

divideNumbersAndPrint("Result is %f %f", 5.0, 1.0)

divideNumbersAndPrint("Result is %f", 5.0, 0.0)
  

Using the Exception Object

As mentioned earlier, it is possible to associate the raised exception with a variable, so we can do things with it, such as print it or inquire about its type. This is done with an as clause:


#!/usr/bin/python

def divideNumbersAndPrint(fmtString, x,y):
  try:
    result = x / y
    newString = fmtString % (result)
  except ZeroDivisionError as zde:
    print zde
    print type(zde)
  except TypeError as te:
    print te
    print type(te)

divideNumbersAndPrint("Result is %d", 5, 3)
divideNumbersAndPrint("Result is %f", 5.0, 3.0)

divideNumbersAndPrint("Result is %f %f", 5.0, 1.0)

divideNumbersAndPrint("Result is %f", 5.0, 0.0)
  

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.

This discussion should remind you of yesterday's discussion about the call stack. The same call stack that is used to allow functions to maintain their state as they are called and return normally -- is used to allow them to fall back for exception handling.

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


#!/usr/bin/python

def divideNumbersAndPrint(fmtString, x,y):
  try:
    result = x / y
    newString = fmtString % (result)
  except ZeroDivisionError as zde:
    print "Undefined"
    throw zde    # This causes the exception to be "rethrown" and go "live" again
  except TypeError:
    print result # Without formatting, as before

divideNumbersAndPrint("Result is %d", 5, 3)
divideNumbersAndPrint("Result is %f", 5.0, 3.0)

divideNumbersAndPrint("Result is %f %f", 5.0, 1.0)

divideNumbersAndPrint("Result is %f", 5.0, 0.0)

  

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

The functions defined within class specifications are known as the "methods" of the class. The automatically get passed an argument that is hidden by the caller, but visible in the method definition. This argument, "self", is a reference to the instance of the object upon which the method is being called. For example, "self.value" references the "value" variable within the specific object upon which the method was invoked.

The __init(...)__ method is present in a "constructor" and is called automatically to initialize a newly created object. The __str__(...) method is called to get a string representation fo the object.

Notice that the __str__() method calls the repr() function. repr() generates a string representation of an object, in a particular, perhaps peculiar, but formulaic way.


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


#!/usr/bin/python

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


  
def pow2(x):

  if (x < 0):
    raise NegativeNumberError("Can't handle exponents < 0")
  
  if (x == 0):
    return 1

  value = 1

  while (x > 0):
    value *= 2
    x -=1

  return value

print pow2(3)
print pow2(-1) # Check the output -- see our exception?

  
  

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
  

Assert

You've already seen assertions and herd about them a bit from the TAs. But, to give them 30 seconds of treatment in class, their syntax is basically like this:

assert boolean expression message

A "boolean expression" is one that evaluates to exactly True or False. As you've no doubt noticed, if the expression given in the assert is False, the assert rases an AssertionError. With our discussion of exceptions, you now have a deeper understanding of what this means.

The "message" gets passed in to the __init__() method of the exception object and is typically rendered by the str() method.

Essentially, there is no magic here. Assertions are implemented via exceptions.