Search code examples
pythonfunctionlimitrepeat

Self Limiting Repition Function


I'm writing a program that is basically a study guide/ practice test for the current section of my A&P class (it keeps me more engaged than just rereading notes over and over). The test works without any problems, but I have an issue where some of my questions use an "enterbox" input, I can have the question loop if the answer is incorrect, but I can't get it to break without a correct answer. I figured out a way to make it work by putting the entire function back into the initial "else" tree, so that right or wrong you advance to the next question but it looks incredibly ugly and I can't believe there isn't a better way. So my "solution" looks like such:

def question82():
   x = "which type of metabolism provides the maximum amount of ATP needed for contraction?"
   ques82 = enterbox(msg = x, title = version)
   #version is a variable defined earlier
   if ques82.lower() in ["aerobic"]:
        add() #a function that is explained in the example further below
        question83()
   else:
        loss() #again its a housecleaning function shown below
        ques82b = enterbox(msg = x, title = version)
        if ques82b.lower() in ["aerobic"]:
            add()
            question83()
        else:
            loss()
            question83()

Okay so it worked, but using a nested if tree for each "enterbox" question looks kinda sloppy. I'm self taught so it may be the only solution but if there is something better I would love to learn about it.

So here is a complete section from my program:

from easygui import * 
import sys

version = 'A&P EXAM 3 REVIEW'
points = 0

def add():
    global points
    msgbox("Correct", title = version)
    points = points + 1

def loss():
    global points
    msgbox("Try Again", title = version)
    points = points - 1  

def question81():
    x = "What chemical is stored by muscle as a source of readily available energy for muscle contractions"
    ques81 = enterbox(msg = x, title = version)
    if ques81.lower() in ["creatine"]:
        add()
        question82()
    else:
        loss()
        question81()  

It works as is so any errors from what's provided are probably my fault from copy and pasting. Also I'm running it in python 2.7rc1 if that helps. Thanks for any help in advance.

I don't know if there is a way to combine "enterbox" that has a button for "skip" as that would also be a solution.


Solution

  • Consider the following approach:

    • We define a list of question and answer pairs. We do this in one place so it's easy to maintain and we don't have to search all over the file to make changes or re-use this code for a different questionset.
    • We create an ask_question function that we can call for all of our questions. This way, if we want to make a change about how we implement our question logic, we only have to make it in one spot (and not in each of the questionXX functions).
    • We compare user input to our answer using == and not in (in will do something else, not what you expect).
    • We create an object to keep track of our answer results. Here, it's an instance of ResultsStore, but it can be anything really, let's just try to get away from global variables.
    • Use a loop when prompting for answers. The loop will repeat if the answer given was incorrect (and if retry_on_fail is False).
    • Allow for the user to enter some "skip" keyword to skip the question.
    • Display the results once the "test" is complete. Here, we do that by defining and calling the store.display_results() method.

    So, what about:

    from easygui import enterbox
    
    question_answer_pairs = [
        ("1 + 1 = ?", "2"),
        ("2 * 3 = ?", "6"),
        ("which type of metabolism provides the maximum amount of ATP needed for contraction?", "aerobic")
    ]
    
    VERSION = 'A&P EXAM 3 REVIEW'
    
    class ResultStore:
        def __init__(self):
            self.num_correct = 0
            self.num_skipped = 0
            self.num_wrong = 0
    
        def show_results(self):
            print("Results:")
            print("  Correct:", self.num_correct)
            print("  Skipped:", self.num_skipped)
            print("  Wrong:  ", self.num_wrong)
    
    
    def ask_question(q, a, rs, retry_on_fail=True):
        while True:
            resp = enterbox(msg=q, title=VERSION)
            # Force resp to be a string if nothing is entered (so .lower() doesn't throw)
            if resp is None: resp = ''
            if resp.lower() == a.lower():
                rs.num_correct += 1
                return True
            if resp.lower() == "skip":
                rs.num_skipped += 1
                return None
    
            # If we get here, we haven't returned (so the answer was neither correct nor
            #   "skip").  Increment num_wrong and check whether we should repeat.
            rs.num_wrong += 1
            if retry_on_fail is False:
                return False
    
    # Create a ResultsStore object to keep track of how we did
    store = ResultStore()
    
    # Ask questions
    for (q,a) in question_answer_pairs:
        ask_question(q, a, store)
    
    # Display results (calling the .show_results() method on the ResultsStore object)
    store.show_results()
    

    Now, the return value currently doesn't do anything, but it could!

    RES_MAP = {
        True: "Correct!",
        None: "(skipped)",
        False: "Incorrect"          # Will only be shown if retry_on_fail is False
    }
    
    for (q,a) in question_answer_pairs:
        res = ask_question(q, a, store)
        print(RES_MAP[res])