Lecture 10 - Exceptions


As usual, create a directory to hold today's activities:

$ mkdir ~/cs170/labs/lab10
$ cd ~/cs170/labs/lab10

Catching and Raising Exceptions

You have likely experienced plenty of exceptions when writing code. Exceptions are the way that the Python interpreter handles everything from invalid input, syntax errors, and even dividing by zero. However, sometimes we don't want our programs to crash when such an error occurs. Or, maybe we want to create our own exceptions that allow us to signal that something has not gone as intended. Thankfully, Python provides mechanisms for both of these (and more!).


Lab Activity 1
Factorials

I know we've used factorial as an example time and time again. However, it is a really good, simple, and understandable mathematical structure that happens to fit in well with handling exceptions.

You are going to write a simple program that allows for a user to compute the factorial of a number that they input. Your program should not crash if they provide invalid inputs.

Details

Create a file called factorial.py, and add a function called factorial(n). This function takes an integer and returns the factorial of that input. You should not use any built in functions to accomplish this.

Write another function called compute_factorials(), which simply continually calls factorial with values read from the user, until the user types 'quit' to exit the program.

Notice that your functions were probably written assuming the input was going to be a positive integer. However, there is nothing restricting the user from typing in negative values or words. Add appropriate try/except statements, as well as raise statements, to make sure that the program continues running until the user types 'quit,' irrelevant of what else they type in.

Example

>>> compute_factorials()
What number do you want the factorial of? 7
5040
What number do you want the factorial of? asdf
That wasn't a number!
What number do you want the factorial of? -1000
Factorial is only defined on positive integers!
What number do you want the factorial of? quit
>>> 

Hint

  • At some point in your code, you are using the int function on some input you are reading from the user. You need to wrap at least this expression in a try/except statement. However, you may need to include other statements to make sure you don't inadvertently raise additional exceptions.

  • Your factorial function will produce invalid input if it is given a negative value to process. You should check to make sure the input value is positive, and raise a ValueError exception if the parameter is negative. Your call to factorial from compute_factorials needs to handle this exception by using a try/except statement.

 

Challenge

Having exception handlers are great, as they make sure your program doesn't crash. However, they can be frustrating because they can also hide errors in your code. This is why we typically don't handle the default Exception in our code. We only handle the specific exceptions we are actually expecting. If some other exception happens, we want the program to quit so we can debug our errors.

Create your own exception called NegativeInput. Instead of raising a ValueError in factorial, raise your created exception. Your exception handler should now explicitly state that it is handling this new exception.


Lab Assignment 10
Battleship

Battleship is a classic board game that consists of two 2-dimensional grids containing some number of battleships. Players take turns firing "missiles" at locations in their opponents 2-dimensional grid. This game has a reputation for being a very easy game to cheat at.

Let's see if we can use Exceptions to help make it a little harder to cheat at the game. You are going to make a very simple, single player version of battleship. You will read in a battleship specification from a file, and allow a player to take turns choosing grid locations to destroy. Your program should handle invalid files and invalid player input gracefully.

Details

Create a file called battleship.py to house your battleship program. Your program should have at least two functions: read_configuration_file(file_name) and process_user_input(game_board).

read_configuration_file(file_name) should read a battleship configuration file specified in the parameter. This file will be of the following format:

1000000000
1000000000
0011110000
0000000000
0111000010
0000000010
0000000010
0000000010
0011100010
0000000000
Which represents this battleship board.

Notice that a valid configuration is a 10 × 10 board, with 17 1's on the board. Any other board is invalid. You should define a InvalidBattleshipBoard exception, and your read function should raise this exception if the board is invalid.

Once you have read a valid board, you should call process_user_input(game_board). This should read input from the user via the command line. The user should be able to specify coordinates like a traditional battleship game: Rows specified with letters from A-J, and columns specified with an integer in the range [1-10]. If the user doesn't specify a valid coordinate, you should raise an InvalidCoordinate exception, which of course you should write.

If the users input is valid, you should process it as normal. Marks hits with x's and misses with o's.

Example

$ cat default.in
1000000000
1000000000
0011110000
0000000000
0111000010
0000000010
0000000010
0000000010
0011100010
0000000000
$ cat error.in
1111111111
1111111110
1111111100
1111111000
1111110000
1111100000
1111000000
1110000000
1100000000
1000000000
$ python3
>>> import battleship
>>> board = battleship.read_configuration_file("default.in")
>>> battleship.process_user_input(board):
Current Board:
0000000000
0000000000
0000000000
0000000000
0000000000
0000000000
0000000000
0000000000
0000000000
0000000000
Your move: A1
HIT!
x000000000
0000000000
0000000000
0000000000
0000000000
0000000000
0000000000
0000000000
0000000000
0000000000
Your move: A2
MISS
xo00000000
0000000000
0000000000
0000000000
0000000000
0000000000
0000000000
0000000000
0000000000
0000000000
0000000000
Your move: Z11
...snip...
During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "battleship.py", line 77, in 
    process_user_input(board)
  File "battleship.py", line 70, in process_user_input
    " is not a valid coordinate!")
__main__.InvalidCoordinate: Z11 is not a valid coordinate!
>>> board = battleship.read_configuration_file("error.in")
Traceback (most recent call last):
  File "battleship.py", line 76, in 
    board = read_configuration_file(fname)
  File "battleship.py", line 41, in read_configuration_file
    raise InvalidBattleshipBoard("Invalid board size, or invalid number of pieces")
__main__.InvalidBattleshipBoard: Invalid board size, or invalid number of pieces
>>> quit()

Hint

  • You can open a file in python using the open function. This function takes two parameters: a string that is the name of the file you want to open, and a string representing your permissions. For this, you only need 'r' (read) permissions.

    Don't forget to close your files when you are done!

  • Your exception classes are going to be very similar. As a matter of fact, if you are sneaky you can define the classes with very little code.

  • You are going to want to catch various exceptions when reading files, so that you can raise your InvalidBattleshipBoard exception. You also need to keep a running sum of the number of 1's on the board. If it does not equal 17 after reading the entire board, you also have an InvalidBattleshipBoard.

  • Remember, you can use ord to convert from strings to ASCII values. You can actually decompose every ASCII character to an integer this way.

    Instead of having an if statement for handling invalid coordinates, you can simply try/except an IndexError when attempting to check the value of the game board. If an IndexError is raised, you just re-raise (by raising an exception in the exception handler) and InvalidCoordinate exception.

 

Challenge

An easy way to make the program more "idiot-proof" is to make it incredibly easy to interact with. Instead of relying on the user to type coordinates to input, simply have them click on locations they want to "fire" missiles at.

That's right, it's tkinter time! Create a grid that the user can click on, and allow them to click to reveal locations. Note that this does not mean you can completely get rid of exceptions. You should raise an InvalidMove exception if they click on a location that is already uncovered.

 

Challenge

It seems that raising an exception in the graphical program should have a different connotation than doing so via the command line. especially since it is a little difficult to notice that an exception was actually raised when using the graphical program.

Look at the documentation for tkMessageBox dialogs. You should create a showerror pop-up window upon raising the exception. You will need the following import statement in your code:

  from tkinter import messagebox


Submission

When you have finished, create a tar file of your lab10 directory. To create a tar file, execute the following commands:

cd ~/cs170/labs
tar czvf lab10.tgz lab10/

To submit your activity, go to cseval.roanoke.edu. You should see an available assignment called Lab Assignment 10. Make sure you include a header listing the authors of the file.


In-class Notes