PROJECT 10

Math Trainer

Do you want a chance to be the teacher? This project tests users on their times tables and displays times tables so a user can practice. It keeps score and records how long it took to take the test.

image

Here’s where you put your class skills to the test and give your brain a workout.

Planning Your Math Trainer

Your math trainer should ask for answers about the times tables (from 1 through 12) and make sure the answer is right.

But does your best friend need help memorizing the tables? The math trainer could print out a times table. Maybe your other BFF knows the smaller numbers already, so the trainer should set a bottom limit (just avoid the 1s, for instance). If you like, you can add a higher upper limit than 12.

Add a score and time how long it takes to take the test. Game on!

Setting Up

Set up your file and put some code in that asks a question and works out whether the answer is correct.

Do the following to get the project up and running:

  1. Create a new file called math_trainer.py.
  2. Create a module docstring for the file.
  3. Use a hash comment to mark these sections: Imports, Constants, Function, and Testing.

    You know from the silly sentences project that if you put a tuple after a formatting operator %, Python unpacks the tuple into formatting specifiers in the formatting string. That means that "What is %sx%s?"%(4,6) becomes "What is 4x6?". You’re going to store each question as a tuple with two numbers.

  4. Make a question to use for testing. Create a TEST_QUESTION constant in the Constants section. Choose two numbers as a test and set TEST_QUESTION equal to the tuple containing them.

    This example is (4,6). You can use it or think up some other numbers.

  5. Create a constant called QUESTION_TEMPLATE.

    It’ll be a formatting template for your test question. Set it to "What is %sx%s" (or another template if you can think of one).

  6. Create a new variable question and make it equal to TEST_QUESTION.

    Use that new variable in your code. Later you’ll list questions and get them one by one from the list. You’ll change the value of question, and the rest of the program will work with the new questions — without any other changes.

  7. In the Testing section, create a prompt to use in a raw_input command by feeding the tuple question into the formatting string QUESTION_TEMPLATE.
  8. Add a line that calculates the correct answer (by multiplying question[0] and question[1] together).
  9. Use the prompt you created in a raw_input statement to get the user’s answer.
  10. Change the user’s answer to a number (use int).
  11. Check the user’s answer against the correct answer and print Correct! or Incorrect.

Here’s my code:

"""

math_trainer.py

Train your times tables.

Initial Features:

* Print out times table for a given number.

* Limit tables to a lower number (default is 1)

and an upper number (default is 12).

* Pose test questions to the user

* Check whether the user is right or wrong

* Track the user's score.

Brendan Scott

February 2015

"""

 

#### Constants Section

TEST_QUESTION = (4, 6)

QUESTION_TEMPLATE = "What is %sx%s? "

 

#### Function Section

 

#### Testing Section

 

question = TEST_QUESTION

prompt = QUESTION_TEMPLATE%question

correct_answer = question[0]*question[1] # indexes start from 0

answer = raw_input(prompt)

 

if int(answer)== correct_answer:

    print("Correct!")

else:

    print("Incorrect")

Run to test it:

>>> ================================ RESTART ================================

>>>

What is 4x6? 24

Correct!

>>> ================================ RESTART ================================

>>>

What is 4x6? 25

Incorrect

Test twice — once for when the answer is right and once for when the answer is wrong.

Create Questions

You could create the questions in this trainer using the random.randint function or even random.choice to create the questions. The problem is that you can’t make sure that the whole times table is tested. Your random number might never give you an 8, for example.

Instead, generate the whole list of possible questions (12x12 = 144 of them) and pick questions from that list. That way you can keep track of what you’ve picked and throw out some other questions. Way back in Project 2 I told you that using the range built-in in Python 2.7 can use up memory. This many entries is manageable, so using range here is okay.

To create your question list, follow along:

  1. In the Function section, define a function called make_question_list.
  2. Write a short docstring for the function.
  3. Specify the upper and lower limits of each number in the questions.

    To do that, add lower and upper as arguments to your function. Add default values to the arguments in the function: lower = 1 and upper = 12:

    def make_question_list(lower=LOWER, upper=UPPER):

    This structure — lower=LOWER — looks odd, but it makes sense. LOWER is a constant with a default value, and lower is the name of the variable that will be used within the function.

  4. Use a double list comprehension to create a list of tuples:

        return [(x+1, y+1) for x in range(lower-1, upper)

                              for y in range(lower-1, upper)]

  5. Return the list as the function’s return value.
  6. Comment out the existing Testing section.

    Don’t delete it; you’ll need it again later.

  7. Add a line in the Testing section to call the function and print the value that it gets back.

I added constants to the Constants section to avoid magic numbers (numbers that appear magically without any explanation in your code):

#### Constants

TEST_QUESTION = (4, 6)

QUESTION_TEMPLATE = "What is %sx%s? "

LOWER = 1

UPPER = 12

The Function section has a new function in it:

#### Function Section

def make_question_list(lower=LOWER, upper=UPPER):

    """ prepare a list of questions in the form (x,y)

    where x and y are in the range from LOWER to UPPER inclusive

    """

 

    return [(x+1, y+1) for x in range(lower-1, upper)

                       for y in range(lower-1, upper)]

This function really only has one line of code — the double list comprehension. The list comprehension was tough because range(lower, upper) goes up to, but doesn’t include upper. That’s not what was specified in the statement of requirements. To account for this the generated tuples add 1 to both the numbers (x+1, y+1). It’s also the reason that lower value has -1 in both list comprehensions.

Minus (math pun) the code commented out, the Testing section now looks like this:

#### Testing Section

 

question_list = make_question_list()

print(question_list)

When you run it, you get a list of 144 tuples ranging from (1,1), (1,2)…(12,11), (12,12). That’s good.

This test shows that the function works for the default values. Test it with a couple other values just in case. Change the Testing section to this:

  1. Choose two or three values to pass as lower and two or three values to pass as upper. Make them pairs of lower and upper.
  2. Call make_question_list. Pass the lower and upper value pairs.
  3. Print the results each time.

This is the new Testing section:

#### Testing Section

 

for lower,upper in [(2, 5), (4, 6), (7, 11)]:

    question_list = make_question_list(lower, upper)

    print(question_list)

Can you see what’s happening? The for loop runs through a list with three elements in it. Each element is a two-tuple. Each element is unpacked into the variables lower and upper, in order. For each of those values, the function is called and the question list it creates is printed.

This is what I got:

[(2, 2), (2, 3), (2, 4), (2, 5), (3, 2), (3, 3), (3, 4), (3, 5), (4, 2), (4, 3), (4, 4), (4, 5), (5, 2), (5, 3), (5, 4), (5, 5)]

[(4, 4), (4, 5), (4, 6), (5, 4), (5, 5), (5, 6), (6, 4), (6, 5), (6, 6)]

[(7, 7), (7, 8), (7, 9), (7, 10), (7, 11), (8, 7), (8, 8), (8, 9), (8, 10), (8, 11), (9, 7), (9, 8), (9, 9), (9, 10), (9, 11), (10, 7), (10, 8), (10, 9), (10, 10), (10, 11), (11, 7), (11, 8), (11, 9), (11, 10), (11, 11)]

Ask Lots of Questions in a Row

You have a list of questions and you know how to ask any one of them. The next step is to bombard your user with questions — boom, boom, boom. If you try to step through the questions one at a time, you run into two problems:

  • The list is in times table order, so it’s not much of a test. It’s better to make it random.
  • The list has 144 items. Unless you’re really, really mean, you probably don’t want to force the user to do all of them at one time. It’s better to ask a certain number of questions at one time.

No worries! You can solve these problems.

Put questions in a random order

You can sort the table into random order:

  1. At the top of the file, add an Imports section and import the random module.
  2. Add an optional argument random_order to the function make_question_list.

    The argument should have a default value of True (or False, if you prefer your questions to be in the right order).

  3. Update the docstring to explain what the random_order argument is going to do.
  4. Store the list of questions generated in a dummy variable.

    It’s a dummy variable, so call it something short. I’m going to call it spam.

  5. Test whether random_order is True.

    if random_order: is good enough, but if that makes you uncomfortable, try if random_order is True:

    If it is True, apply random.shuffle(spam) to the dummy variable. You shouldn’t need an else: block. Use help(random.shuffle) in the IDLE Shell window to confirm what random.shuffle does and what its return value is (warning: trick!).

  6. Return the dummy variable.

This is the new Imports section:

#### Imports Section

import random

The new Function section looks like this:

#### Function Section

def make_question_list(lower=LOWER, upper=UPPER, random_order=True):

    """ prepare a list of questions in the form (x,y)

    where x and y are in the range from LOWER to UPPER inclusive

    If random_order is true, rearrange the questions in a random order

    """

    spam = [(x+1, y+1) for x in range(lower-1,upper)

                       for y in range(lower-1,upper)]

    if random_order:

        random.shuffle(spam)

    return spam

 

tip Leave the Testing section the same. It’s easier to review the shorter lists.

This is what you get when you run it:

[(4, 2), (3, 4), (4, 4), (5, 2), (5, 4), (2, 5), (3, 2), (2, 4), (3, 5), (5, 3), (2, 3), (4, 3), (5, 5), (2, 2), (4, 5), (3, 3)]

[(5, 6), (4, 5), (6, 5), (5, 4), (6, 4), (5, 5), (4, 4), (4, 6), (6, 6)]

[(11, 7), (8, 11), (9, 8), (11, 10), (9, 7), (7, 7), (10, 8), (9, 10), (8, 10), (10, 10), (9, 11), (7, 10), (10, 7), (7, 11), (8, 9), (11, 8), (11, 11), (8, 7), (10, 11), (9, 9), (7, 8), (10, 9), (11, 9), (7, 9), (8, 8)]

Your shuffled list will look different because it’s, yanno, random. Compare these to the unshuffled versions. Just as an example, the first unshuffled list ought to start with (2,2).

You’ve added a new variable, random_order, which can be True or False. It defaults to True, so you’ve only tested one option so far. You should also test what happens when random_order is False. (The function should return unshuffled lists like before you added the variable.) To do that, change the code so it passes False into the function (question_list = make_question_list(lower, upper, False)) and check that it returns an unshuffled list.

Ask a certain number of questions at a time

It’s pretty easy to pose multiple questions — put them in a loop. When you ask multiple questions, though, the next logical thing to do is to keep track of the score.

To ask multiple questions and keep track of the score:

  1. Comment out the code in the Testing section that was used to test variations on make_question_list.
  2. Create a constant for the total number of questions to be asked (MAX_QUESTIONS = 3). Add it to the Constants section.

    Use a small number so you don’t have too many numbers to test against. I chose 3.

  3. In the Functions section, create a function called do_testing. (It doesn’t need to take arguments.)
  4. In that function, create a variable to hold the user’s score. Initialize to 0.

    Initializing is when you assign a value to a variable for the first time.

  5. Still in the function, add a line creating a question_list by calling make_question_list().
  6. In that function, create an enumerate loop that runs through the questions in question_list.

    for i, question in enumerate(question_list):

  7. For each iteration of the loop, enumerate(question_list) returns a number (i in this case) and a question (a two-tuple) from the list.

    The number i is how far through the list you are (starting at 0).

    Each iteration tests to see if i is larger than or equal to the MAX_QUESTIONS constant. If it is, you’ve asked enough questions so use break to get out of the loop.

  8. Go back to the code in the Testing section that you commented out in Step 6 of the “Create Questions” section earlier in this project.

    Uncomment it, apart from the line question=TEST_QUESTION — you don’t need that. Move it to the end of the do_testing function and then indent it one level to make it part of the for loop’s code block.

  9. In the code block that tells users they are correct, increase the score by 1.
  10. Print the user’s score after the end of the questions.
  11. In the Testing section, add a call to the new function do_testing.

The new Constants section looks like this:

#### Constants Section

TEST_QUESTION = (4, 6)

QUESTION_TEMPLATE = "What is %sx%s? "

LOWER = 1

UPPER = 12

MAX_QUESTIONS = 3 # for testing, you can increase it later

The new do_testing function is mostly recycled code:

def do_testing():

    """ conduct a round of testing """

    question_list = make_question_list()

    score = 0

    for i, question in enumerate(question_list):

        if i >= MAX_QUESTIONS:

            break

        prompt = QUESTION_TEMPLATE%question

        correct_answer = question[0]*question[1]

        # indexes start from 0

        answer = raw_input(prompt)

 

        if int(answer) == correct_answer:

            print("Correct!")

            score = score+1

        else:

            print("Incorrect, should have been %s"%(correct_answer))

 

    print("You scored %s"%score)

remember You use enumerate in do_testing, rather than for i in range(MAX_QUESTIONS):. If MAX_QUESTIONS is too big, it could be longer than the number of questions in the list, causing an error.

Now enumerate will stop of its own accord at the end of the list, regardless of how big MAX_QUESTIONS is. Added bonus: enumerate is the more Pythonic way to do it.

And in the new Testing section, I deleted everything that wasn’t reused:

#### Testing Section

 

do_testing()

Running this code gives you this:

What is 7x6? 42

Correct!

What is 11x12? 132

Correct!

What is 6x7? 24

Incorrect, should have been 42

You scored 2

Everything is working as it should be.

Print Out a Times Table

Printing out a times table for a given number isn’t too hard. Given a value like (4,6), you already know how to calculate the answer and print it in a formatted way. You shouldn’t have any trouble putting together a format string to print each entry of the times table.

What is a little tricky is how you want it to look. Should it be one column with each problem on a separate line? That wastes a lot of screen space. Should you have multiple columns? You don’t know how wide the screen is, and how many columns you can use, until you get to the end of the line. You have to decide all these interface design questions.

Start printing out the whole times table:

  1. Create a constant to be used as a template for each times table entry. The constant needs to print out three numbers as a multiplication problem.

    TIMES_TABLE_ENTRY = "%s x %s = %s"

    You’re going to accumulate these entries and then print them all at once.

  2. Create a function called display_times_tables, in the Functions section, that takes one argument upper.

    Make upper default to UPPER, the constant you defined earlier. UPPER equals 12:

    def display_times_tables(upper=UPPER):

    The argument upper is the largest times in the table. Change UPPER to get larger tables if you want them, but know this — they’ll get big quickly.

  3. Write a docstring for the function.
  4. In the function, create two for loops, one inside the other.

    Each loop should range up to the local variable upper. (Note the lowercase upper.) Call the dummy variables x and y. They’re going to be the two numbers being multiplied together.

        for x in range(upper):

            for y in range(upper):

  5. Inside the for y loop, use the template string to create a string to print out the number, the index, and the product of the two of them. Then print it.

    This code will prepare the output. Then you need to print entry:

    entry = TIMES_TABLE_ENTRY%(x+1, y+1, (x+1)*(y+1))

  6. Comment out the contents of the existing Testing section.
  7. Add a line calling display_times_tables.

This is the template string in the Constants section:

TIMES_TABLE_ENTRY = "%s x %s = %s"

Here’s how the display function looks:

def display_times_tables(upper=UPPER):

    """

    Display the times tables up to UPPER

    """

 

    for x in range(upper):

        for y in range(upper):

            entry = TIMES_TABLE_ENTRY%(x+1, y+1, (x+1)*(y+1))

            print(entry)

When I first did this function, I forgot to add 1 to y, so the times table started at 1x0 and ended at 1x11.

The Testing section looks like this:

#### Testing Section

 

#do_testing()

display_times_tables()

Run it to get this:

1 x 1 = 1

1 x 2 = 2

[140 lines omitted]

12 x 11 = 132

12 x 12 = 144

It’s working right, but the alignment is a little wonky. Double digits throw things off; triple-digit numbers will, too.

remember You used the %s specifier in the silly sentences project, but %i is specifically for numbers. It lets you set a minimum width for the numbers you print. For example, %2i sets a minimum width of 2 and %3i is a minimum width of 3.

On the left, make a width of 2 (since the largest number is 12, which has two digits) and on the right side of 3 (since the largest number will be 144). This is called left padding.

Here’s a tidy format string:

TIMES_TABLE_ENTRY = "%2i x %2i = %3i"

If you make this change and rerun the program, you get this:

 1 x 1 = 1

 1 x 2 = 2

[140 lines omitted]

12 x 11 = 132

12 x 12 = 144

Hey, that’s much neater! All the numbers are lined up on the right. This code won’t fail if you specify a large number (like 1000) for the times table, but the output won’t be neatly aligned.

tip It’s possible to write code that can handle any size number and nicely line things up, but part of programming is knowing when to compromise. Most times tables run from 1 through 12, so I think this is a good compromise.

Print Multiple Tables Across the Screen

The times tables are too far down the screen. A lot of the horizontal space on the screen isn’t being used. This’ll be better if tables are horizontal. To do that, you need to know how wide each entry is.

The len() built-in gives you the length of an object like a string or a list.

In the IDLE Shell window, make a sample entry and get its length. Like this:

>>> TIMES_TABLE_ENTRY = "%2i x %2i = %3i"

>>> entry = TIMES_TABLE_ENTRY%(12,12,144)

>>> len(entry)

13

This means that there are 13 characters in 12 x 12 = 144. Adding a space to separate them horizontally makes 14. Programs tend to assume that a screen is about 70 characters wide. You’ll use this as your benchmark. 70 divided by 14 is 5, so you can fit five times tables across the width of the screen.

To print a more compact series of times tables, you have to replace the display_times_tables function with code that does the following:

  1. In the Constants section, add a blank space at the end of the constant TIMES_TABLE_ENTRY:

    TIMES_TABLE_ENTRY = "%2i x %2i = %3i ".

  2. Create a local variable tables_per_line set it to 5.
  3. Make a list of all the tables to be printed:

        tables_to_print = range(1, upper+1)

  4. Slice off the first tables_per_line from tables_to_print and save the remainder of the list back in tables_to_print.

        batch = tables_to_print[:5]

        tables_to_print = tables_to_print[5:]

  5. Create a while loop that executes while batch != []:.
  6. In that loop:, create a for loop that ranges from 1 to upper+1 (to get the numbers from 1 to upper, including upper).

    for x in range(1, upper+1):

  7. In this for x loop, create an empty list to hold one line of times tables entries: accumulator = [].

    You make this assignment here so that it resets each time you go through the for x loop.

  8. Create another for loop. This one should be for y in batch:.
  9. Within this for y loop — watch the indents — add a times table entry to the accumulator:

    accumulator.append(TIMES_TABLE_ENTRY%(y, x, x*y))

  10. At the indent level of the for x loop, join up the accumulator with an empty string "" and print it: print("".join(accumulator)).
  11. Slice off another batch and shrink tables_to_print.

    This is the same code from Step 4 (but indented).

The final code has one space added at the end of TIMES_TABLE_ENTRY in the Constants section:

TIMES_TABLE_ENTRY = "%2i x %2i = %3i "

And a revised display_times_tables function:

def display_times_tables(upper=UPPER):

    """

    Display the times tables up to UPPER

    """

    tables_per_line = 5

    tables_to_print = range(1, upper+1)

    # get a batch of 5 to print

    batch = tables_to_print[:tables_per_line]

    # remove them from the list

    tables_to_print = tables_to_print[tables_per_line:]

    while batch != []: # stop when there's no more to print

        for x in range(1, upper+1):

            # this goes from 1 to 12 and is the rows

            accumulator = []

            for y in batch:

                # this covers only the tables in the batch

                # it builds the columns

                accumulator.append(TIMES_TABLE_ENTRY%(y, x, x*y))

            print("".join(accumulator)) # print one row

        print(" ") # vertical separation between blocks of tables.

        # now get another batch and repeat.

        batch = tables_to_print[:tables_per_line]

        tables_to_print = tables_to_print[tables_per_line:]

The while loop splits the tables (1 through 12) into batches of five at a time. The y loop puts together each row for any given x. For the first row, x+1 is 1, and y goes from 1 to 5, so the first row is 1x1, 2x1, up to 5x1. For the second row, x+1 will be 2. The y still goes from 1 to 5, so the second row will be 2x1, 2x2, and on up to 5x2. This goes until all tables are printed.

Start on the User Interface

So far in this project, you’ve been working on back-end functionality — things that go on behind the user interface. While users have a little bit of interaction, they can’t control what the program does. For example, they have no instructions and can’t choose testing over training.

tip Yeah, really it’s truly a user interface, even though it’s just text on the command line. Think carefully about the interface so your program is usable. Fantastic functionality is no good if the interface is too hard to use. Many apps seem to spend far more time on graphics than they do on what’s behind them.

Because you only have text to work with, your user interface will be pretty simple. That said, not having to worry about beauty helps you focus on how the program will go from the time the user runs it.

Your math trainer’s user interface should:

  • Introduce the program to the user and explain the options —training, testing, or quit. The explanation could say, “Hey, I’m a times table trainer that prints out the times tables and I test your multiplication chops with questions.” Choose numbers that the user will type to run each option and list them in the instructions.
  • Ask the user which option to run.
  • Pass control to the part of the program that handles that choice.
  • Allow each part of the program to return control to the main part of the program. For example, users should be able to train on one of their times tables, then, when they’re finished, run the Testing section.
  • Either pass control back to the main part of the program, or let the user run that section again.

The good news is most of this is already done! Now start on these by laying down a skeleton of the code:

  1. In the Constants section, create a constant called INSTRUCTIONS. Assign to this constant some introductory text and some instructions.

    When putting together this string, imagine that it’s a docstring. Use triple double quotes to start it, and let it go over multiple lines.

    tip Don’t worry if you come up blank. If you’re having trouble, leave INSTRUCTIONS empty, do the other steps, and then come back to it. Maybe something like:

    INSTRUCTIONS = """Welcome to Math Trainer

    This application will train you on your times tables.

    It can either print one or more of the tables for you

    so that you can revise (training) or it can test

    your times tables.

    """

  2. Comment out the Testing section and add a Main section at the end of the program.
  3. In the Main section, add a if __name__ == "__main__": block.
  4. In that block, create a while True: loop.
  5. In that block, print the instructions.

    Then use "Press 1 for training. Press 2 for testing. Press 3 to quit" as a prompt for a raw_input. Store the value you get from raw_input in a variable called selection.

    warning The quit function is in the next section. To end this program you’ll need to use Ctrl+C.

  6. Test the value that the user gives you. Use the strip method on selection: selection = selection.strip().

    This is a tiny bit of data cleaning. It removes blank spaces from the start and end of a string.

  7. If selection isn’t "1" or "2" or "3", (they’re strings, remember?), then create a message asking the user to choose again.

    This process should loop until the user chooses one of those three options.

  8. Create one function stub for do_quit().

    You already have two functions that you can use for these options: do_testing and display_times_tables for testing and training.

  9. Create a docstring for it.

    Include a print statement identifying the function. It’s useful for testing.

  10. In the Main block, use an if/elif/else to call these functions based on the user’s choice.

The Constants section has changed by adding this constant:

INSTRUCTIONS = """Welcome to Math Trainer

This application will train you on your times tables.

It can either print one or more of the tables for you

so that you can revise (training) or it can test

your times tables.

"""

This is the stub added to the Functions section:

def do_quit():

    """ quit the application"""

    print("In quit")

The Testing section is now entirely commented out. The following was added as a new Main section:

#### Main Section

 

if __name__ == "__main__":

    while True:

        print(INSTRUCTIONS)

        raw_input_prompt = "Press: 1 for training,"+

                           " 2 for testing, 3 to quit. "

        selection = raw_input(raw_input_prompt)

        selection = selection.strip()

        while selection not in ["1", "2", "3"]:

            selection = raw_input("Please type either 1, 2 or 3: ")

            selection = selection.strip()

 

        if selection == "1":

            display_times_tables()

        elif selection == "2":

            do_testing()

        else: # has to be 1, 2 or 3 so must be 3 (quit)

            do_quit()

Run through this four times to make sure things are working properly. Why four? Once to check that each of the three functions is called when the option is chosen and once to test the behavior when you make an invalid choice.

Add Quit Functionality

Now you need to get the quit feature working. Shouldn’t be hard though, because you’ve dealt with that before in Project 5, where you improved your guessing game.

  1. In the Imports section, add import sys.

    You’ll use this for its sys.exit() function, which will cause the program to end.

  2. Copy the confirm_quit() function that you covered in Project 5 into the Functions section.
  3. Copy the constants that confirm_quit() relies on.
  4. In the do_quit function, call confirm_quit().

    If the quit is confirmed, call sys.exit(). Otherwise, do nothing. The function will return and continue from where it was called.

This is the new Imports section:

#### Imports Section

import random

import sys

Awesome, huh? This constant had to be added to the Constants section. It came across from Project 5 with confirm_quit:

CONFIRM_QUIT_MESSAGE = 'Are you sure you want to quit (Y/n)? '

Just add this to the end of the constants that are already there.

tip I always have the urge to put constants in alphabetical order. I’m not sure that’s a good idea. It’s probably better to group constants by what they do. (For example, put LOWER and UPPER together because they’re both used in the questioning functionality.)

The confirm_quit function was copied from Project 5:

def do_quit():

    """ quit the application"""

    if confirm_quit():

        sys.exit()

    print("In quit (not quitting, returning)")

 

def confirm_quit():

    """Ask user to confirm that they want to quit

    default to yes

    Return True (yes, quit) or False (no, don't quit) """

    spam = raw_input(CONFIRM_QUIT_MESSAGE)

    if spam == 'n':

        return False

    else:

        return True    

Keep the print statement in do_quit because it’s useful for testing. (It only prints if you choose to quit, then change your mind.) The call to sys.exit() makes the application exit.

If you run the code from IDLE (like you do with all of the other code in the book), you might get a message like this:

Traceback (most recent call last):

  File "/data-current/dummies book/code folder/math_trainer_6.py", line 122, in <module>

    do_quit()

  File "/data-current/dummies book/code folder/math_trainer_6.py", line 68, in do_quit

    sys.exit()

SystemExit

It happens because of the way IDLE integrates running scripts while making the shell available. If you run this from outside IDLE, Python won’t complain.

Polishing It Off

Tie up a couple of loose ends:

  • Add a timing feature to the Testing section so you can report how much time the user took.
  • Reset the maximum number of questions that will be asked in a round. Currently it’s three.
  • Tidy up and remove unused code.

It’s downhill all the way from here.

Timing the questions

To time a round of questions, use the time module. Here’s your 20-second introduction to it:

>>> import time

>>> time.time() # current time

1433075973.088198

Don’t believe that’s the current time? Check the docs: It’s the current time in seconds since the Epoch. It’s the exact time, down to one-millionth of a second. Don’t ask why. It’s just another one of those historical accidents that infest everything that has to do with time calculations.

You can use the time module to get the difference in the times when two things happen, like this:

>>> t1 = time.time() # current time

>>> t2 = time.time()# current time again (I waited a smidge)

>>> t2-t1 # number of seconds between first and second calls

5.041269063949585

Have a look at time.ctime and time.gmtime sometime.

Now work on the timing stuff:

  1. In the Imports section, import the time module
  2. Set the score to 0 in the do_testing method, and get and store the current time in start_time:

    start_time = time.time()

  3. Just before you print the score, grab the time again and store it in a separate variable. Figure the difference between these two variables.
  4. Print the time taken, along with the score.

    Also print out what percentage of the questions was correct (score divided by total questions multiplied by 100). You can use the following as your template (add it to the Constants section). The doubled %% sign will print as a single %. The escape code %.1f says it’s a floating point number with one decimal place:

    SCORE_TEMPLATE = "You scored %s (%i%%) in %.1f seconds"

The new Imports section looks like this:

#### Imports Section

import random

import sys

import time

There’s a new constant in the Constants section:

SCORE_TEMPLATE = "You scored %s (%i%%) in %.1f seconds"

The reworked do_testing function looks like this now:

def do_testing():

    """ conduct a round of testing """

    question_list = make_question_list()

    score = 0

    start_time = time.time()

    for i, question in enumerate(question_list):

        if i >= MAX_QUESTIONS:

            break

        prompt = QUESTION_TEMPLATE%question

        correct_answer = question[0]*question[1]

             # indexes start from 0

        answer = raw_input(prompt)

 

        if int(answer) == correct_answer:

            print("Correct!")

            score = score+1

        else:

            print("Incorrect, should have been %s"%(correct_answer))

 

    end_time = time.time()

    time_taken = end_time-start_time

    percent_correct = int(score/float(MAX_QUESTIONS)*100)

    print(SCORE_TEMPLATE%(score, percent_correct, time_taken))

Tidying up the main application loop and the rest

Time to loop in the main application, the number of questions per test, and so on. You can do this! This stuff hardly even deserves its own section.

  1. In the Constants section, amp up MAX_QUESTIONS to something like 10 or 20 (like MAX_QUESTIONS = 10).

    This will change the number of questions that will be asked in each round of do_testing. Don’t make it too many, though.

  2. Remove the print statements you included for debugging. Delete commented-out code.

The print(INSTRUCTIONS) code is within the loop. You could leave it out of the loop and add another option “4” to print the instructions again if you like.

Summary

While you were making your math trainer, you did this, too:

  • Randomized a list using the random.shuffle function.
  • Created a user interface that gives instructions, then allowed users to choose from different options.
  • Added quit functionality and confirmed that users do actually want to quit (which, admittedly, you’ve done in the past).
  • Used a while loop to slice up tables.
  • Refactored to split your code.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.135.202