ENGI E1006: Introduction to Computing for Engineers and Applied Scientists


Lecture 3: From Algorithm to Code; writing your own modules

Reading: Punch and Enbody Chapter 6.1-6.2

Back to coding...

Recall: Last time we learned about algorithms and some of their mathematical properties. So how do we get back to programming in Python? Let's consider a simple algorithm for computing the factorial of a nonnegative integer. First we write it in pseudocode:

factorial

  • Input: A nonnegative integer n
  • Output: n!
  • Method:
answer=1
if n>0
    for i = 1 to n
        answer=answer * 1
return answer

So we just saw how to define a simple value-returning function in Python. Now we are going to see how we can use function definitions to make the design process of a program much easier.

Pig

Pig is a two player game played with a single die. The object of the game is to accumulate 100 points. During each turn a player repeatedly rolls the die earning points equal to the face value of the die on each roll until:

  • A one is rolled, in which case the turn is over and the player forfeits all points earned so far on that turn
  • The player volunteers to end the turn, takes the points earned so far and hands the die over to the opponent.

Let's consider how we would write a program in Python to interactively play this game. We will start by wishing Python had built in functions that allow us to do everything we need. Eventually, we will have to write these functions ourselves but the process of imagining what they would do for us will greatly inform our design of the program.

  1. Display greeting
  2. Play the game
  3. Display goodbye

Okay, so that's a first very coarse iteration. Now we can attack each one of these steps one at a time. What we will do is come up with a function in Python

Display greeting
This one is pretty simple. We'll just write a function that prints a greeting. Something like this:

In [1]:
def greeting():
    print('Welcome to Pig Deathmatch!')
    print('Prepare to be challenged!')

That was easy enough. We can do last step just as easily. Display goodbye

In [2]:
def goodbye():
    print('Thanks for playing!')

Okay, easy. But what about actually playing the game? This one requires a little bit more refinement. Let's work on it.

Play the game

  1. Player 1 takes their turn
  2. Player 2 takes their turn
  3. Repeat steps 1 and 2 until someboday wins
  4. Display the result

Not hard. Let's see if we can refine this a little bit. Notice that we use the word until in step 3. This is often a good indicator that a while loop may be used to capture the reptition.

Let's try again,

Play the game

while (nobody has won the game)  
    Player 1 takes their turn
    Player 2 takes their turn
Display the result

Let's refine a little more. What does nobody has won the game really mean? It means that neither player has accumulated 100 points yet. So let's write it like that.

while (player1_score<100 and player2_score<100)
    Player 1 takes their turn
    Player 2 takes their turn
Display the result

Almost there! This is almost right. What's wrong though? Does player 2 really always play after player 1? What about if player 1 wins the game?

while (player1_score<100 and player2_score<100)
    Player 1 takes their turn
    if player1_score<100
        Player 2 takes their turn
Display the result

Excellent! So now we just need to articulate more precisely what 'Player 1 takes their turn' means and what 'Player 2 takes their turn' means. That is, we need to write more algorithms.

Player 1 takes their turn

  1. roll the die
  2. print the result
  3. if roll == 1 set turn_score to zero and end turn
  4. else add the roll to turn_score and ask if they want to roll again
  5. repeat until they roll a 1 or the player chooses to end the turn

Let's refine this.

turn_total=0
while(player wants to roll again and rolls no 1)
    roll the die
    print the result
    if roll == 1
        turn_total=0
        end the turn
    else
        turn_total = turn_total + 1
        ask the user if they want to roll again or not
return turn_total

That's better. It's not Python, but it's pretty close. Notice that we'll have to write one more function for rolling the die. That's easy though. We already saw how we can do that in the roshambo program.

So what's left? The computer player, Player 2.

Player 2 takes their turn

  1. roll the die
  2. print the result
  3. if roll == 1 set turn_score to zero and end turn
  4. else add the roll to turn_score and decide whether to roll again or not
  5. repeat until they roll a 1 or the player chooses to end the turn

So how does the computer decide? We need to invent a strategy for the computer. One option is to have it roll up to x number of times. Another is to keep rolling until it's accumulated x number of points. There are many other options. We'll go with this simple approach: Keep rolling until 20 points are accumulated or a 1 is rolled. Translating the above into something a little closer to Python and incorporating this strategy give us

turn_total = 0
while (turn_total <20 and rolls no 1)
    roll the die
    print the result
    if roll == 1
        turn_total=0
        end the turn
    else
        turn_total=turn_total+1 
return turn_total

And now to code....

So now that we have done all of this we are ready to write it up in Python. When you're coding on your own, it's really important to go through this process. It will save you time!!

Example

pig.py

In [3]:
#********************************************
#Pig.py
# 
#Plays the interactive dice game Pig
#
#Written by Cannon
#********************************************

import random

score = 0

# The main function for the applicaiton
def main():
    greeting()
    playgame()
    goodbye()


def playgame():
    '''This function plays the game Pig'''
    p1score=0
    p2score=0
    #establish a while loop for the game
    while (p1score <100 and p2score <100):
        print ('your score total is now: {} \n \n'.format(p1score))
        p1score=p1score + playerMove()
        print ('your score total is now: {} \n \n'.format(p1score))
        if(p1score<100):
            p2score=p2score + computerMove()
            print ('the computer score total is now:{}\n \n'.format(p2score))
    if (p1score>p2score):
        print ('you win!')
    else:
        print ('you lose!')

def playerMove():
    '''manages the human players move in Pig. Returns their score'''
    roundScore=0
    again='y'
    #establish a while loop for the player's turn
    while again=='y':
        roll=rollDice()
        if roll==1:
            print ('you rolled a 1')
            roundScore=0
            again='n'
        else:
            print( 'you rolled a {}'.format(roll))
            roundScore=roundScore+roll
            print( 'your round score is {}'.format(roundScore))
            again=input('roll again? (y/n)')
    print ('your turn is over')
    return roundScore


def computerMove():
    '''plays the computer players turn in Pig returns their turn score'''
    roundScore=0
    again='y'
    #establish a while loop for the computer's turn
    while again=='y':
        roll=rollDice()
        if roll==1:
            print ('computer rolled a 1')
            roundScore=0
            again='n'
        else:
            print( 'computer rolled a {}'.format(roll))
            roundScore=roundScore+roll
            if roundScore < 20:
                print( 'computer will roll again')
            else:
                again='n'
                
    print( 'computer turn is over')
    print( 'computer round score is {}'.format(roundScore))
    return roundScore

def rollDice():
    '''Rolls a 6 sided die and returns an int 1-6'''
    face=int(random.random()*6+1)
    return face

def greeting():
    '''Displays a greeting for the game Pig'''
    print( 'welcome to Pig Death Match!')
    
def goodbye():
    '''Displays a goodbye message for the game Pig'''
    print()
    print()
    print('Toodles!')
    

What just happened?

We started with an idea. We then wrote in very simple pseudocode an algorithm version of our idea. We did this imagining that there existed built-in functions to do the stuff we wanted. We then refined our algorithm filling in pseudocode for each one of the functions we imagined existing. Finally, we wrote it all down in Python!

Writing your own modules

So now we know how to use functions to aid in the design of our Pig program. Writing your own functions has many benefits most of which are derived from encapsulation.

Encapsulation: The hiding of the details of an operation which allows programmers to use coarser operations when designing and writing programs. The programmer need not know the details of the finer operations, just how to use them.

The term is most often used in the context of data-types in Object-oriented programming but it is useful here too. Your textbook does a great job of motivating the use of functions in Section 6.1.1. Please read this carefully!

Modules

Previously we have imported the math and random modules so that we may use the functions that they contain. Contrast this to the use of built-in functions, functions that don't live in modules that need to be imported. In the last two examples we saw that we can write our own functions in Python. The fun doesn't stop there, we can create our own modules to store those functions as well. Typically we will store related functions in a sinle module so that we can import that module whenever we want access to that collection of functions. To start, it's okay if you think of a module as a file that serves as a place to keep your functions.

Example

This example consists of the three files rectangle.py, circle.py, and geometry.py listed below.

rectangle.py

In [4]:
# The rectangle module has functions that perform
# calculations related to rectangles.

# The area function accepts a rectangle's width and
# length as arguments and returns the rectangle's area.
def area(width, length):
    return width * length

# The perimeter function accepts a rectangle's width
# and length as arguments and returns the rectangle's
# perimeter.
def perimeter(width, length):
    return 2 * (width + length)
In [5]:
# The circle module has functions that perform
# calculations related to circles.
import math

# The area function accepts a circle's radius as an
# argument and returns the area of the circle.
def area(radius):
    return math.pi * radius**2

# The circumference function accepts a circle's
# radius and returns the circle's circumference.
def circumference(radius):
    return 2 * math.pi * radius
In [6]:
# This program allows the user to choose various
# geometry calculations from a menu. This program
# imports the circle and rectangle modules.

import circle
import rectangle
# The main function.

def main():
    
    # The choice variable controls the loop
    # and holds the user's menu choice.
    choice = 0

    while not(choice == 5):
        # display the menu.
        display_menu()

        # Get the user's choice.
        choice = int(input('Enter your choice: '))

        # Perform the selected action.
        if choice == 0:
            r = float(input("Enter the circle's radius: "))
            print('The area is ', circle.area(r))            
        elif choice == 2:
            radius = float(input("Enter the circle's radius: "))
            print('The circumference is ', \
                  circle.circumference(radius))
        elif choice == 3:
            width = float(input("Enter the rectangle's width: "))
            length = float(input("Enter the rectangle's length: "))
            print('The area is ', rectangle.area(width, length))
        elif choice == 4:
            width = float(input("Enter the rectangle's width: "))
            length = float(input("Enter the rectangle's length: "))
            print('The perimeter is ', \
                  rectangle.perimeter(width, length))
        elif choice == 5:
            print("Exiting the program...")
        else:
            print("Error: invalid selection.")
    
# The display_menu function displays a menu.
def display_menu():
    print("        MENU for ")
    print("1) Area of a circle")
    print("2) Circumference of a circle")
    print("3) Area of a rectangle")
    print("4) Perimeter of a rectangle")
    print("5) Quit")

# Call the main function.
main()
        MENU for 
1) Area of a circle
2) Circumference of a circle
3) Area of a rectangle
4) Perimeter of a rectangle
5) Quit
Enter your choice: 5
Exiting the program...

What just happened?

We created a circle module and a rectangle module that store functions we wrote to compute the areas and perimeters of those shapes. Those modules don't actually do anything, we just use them to store our function definitions. Then we wrote the geometry.py file that contains the function definition for the main function. You can think of the main function as the command and control function. It's running the show, calling all of the other functions as needed. In terms of the Pig example from last lecture, the main function is the implementation coarsest of the algorithms we created at the beginning of the wishful programming process.

Example

The Heron method for determining the square root of a number is an old (ancient even!) algorithm. It's very intuitive. Suppose we want to calculate the square root of x. Let's guess y. If we guessed right then x/y should equal y, right? Unless we made a really lucky guess, it won't. So we'll use the average of y and x/y as our next guess and that should be a little closer to the right answer. We'll continue this way until successive guesses are close enough to suit our needs. More formally:

Suppose we are trying to determine the square root of x. Let our first guess $$x_0 = 1$$ and let our nth guess be $$ x_n = \frac{x_{n-1} + x/x_{n-1}}{2}$$ and we keep guessing until successive guesses are withing some tolerance. Now we can easily turn this into a function in Python.

heron.py

In [7]:
def heron(a,tolerance):
    '''Uses the Heron method to approximate the square root'''
    oldGuess = 1.0
    newGuess = (a / oldGuess + oldGuess) / 2
    while abs(oldGuess - newGuess) > tolerance:
        oldGuess = newGuess
        newGuess = (a / oldGuess + oldGuess) / 2
    return newGuess

# We can break the function up into smaller functions if we want


def next_Guess(a, oldGuess):
    '''part of the Heron method for finding square roots'''
    return (a / oldGuess + oldGuess) / 2

def has_next_guess(a, tolerance, oldGuess):
    '''predicate function for Heron method for finding square roots'''
    newGuess = next_Guess(a, oldGuess)
    return (abs(oldGuess -newGuess) > tolerance)


def root_approx(a, tolerance):
    '''Uses the Heron method to approximate the square root'''
    guess = 1.0
    while has_next_guess(a, tolerance, guess):
        guess = next_Guess(a, guess)
    return next_Guess(a,guess)
In [8]:
import heron 
import math


def main():
    TEST = 2
    EPSILON = .000001
    print('Heron answer: ', heron.root_approx(TEST, EPSILON))
    print('Math answer: ', math.sqrt(TEST))

main()
Heron answer:  1.414213562373095
Math answer:  1.4142135623730951

What just happened?
In the first file, heron.py we created a module to store our root approximating function. We wrote the functin in two different ways, just for fun. The second file, heron_test.py is used to define the main function where we test out our implementation of the heron method and use it to calculate the square root of 2. We compare our answer with the answer the math module gives us.