$ mkdir ~/cs170/labs/lab10 $ cd ~/cs170/labs/lab10
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 . 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!).
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.
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.
>>> 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 >>>
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.
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.
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.
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 0000000000Which 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.
$ 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, inprocess_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()
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.
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.
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
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.