Python loop iterates after expected termination

98 views Asked by At

The below Python code has an odd behavior, which I can't figure out.

The program calls testQuery, which asks the used to reply 'yes' to call scoreAverager, or 'no' to exit the program. If scoreAverager is called, it then requests the user enter a series of scores, or 'x' to finish, which returns the user to testQuery, where they can chose to average another test or exit.

The weirdness happens if a user averages the results for several tests (e.g. 2 or more). At that point giving 'no' to testQuery will fail to terminated the program. It will iterate an additional cycle for every test that was averaged. I can't figure out why this would be the case. It looks and behaves like an off-by-one erros, but the conditions to close the loop appear satisfied. It can be solved with a 'break', but I'd rather know what the problem is to solve it more organically.

Can anyone let me know why this odd behavior is happening?

CODE:

def scoreAverager():
    done=0
    scoreTot=0
    numScores=0
    average=0
    while done == 0:
        score=input("Enter the numerical score, or enter 'x' to finish entering scores:")
        acceptedXs={"X","x"}
        if score in acceptedXs:
            print ("The average of the scores is: ",average)
            #break #this break is necessary for proper function.
            done=1
            testQuery()
        else:
            try:
                score=float(score)
                scoreTot=scoreTot+score
                numScores=numScores+1
                average=scoreTot/numScores
            except ValueError:
                print("EXCEPTION: The entry was invalid, please try again.")

def testQuery():
    done=0
    while done == 0:
        moreTests=input("Do you have a set of score to average? Enter 'Yes' or 'No':")
        acceptedNos=("No","NO","no") 
        acceptedYess=("Yes","YES","yes")
        if moreTests in acceptedNos:
            print("Program Complete.")
            done=1
        elif moreTests in acceptedYess:
            scoreAverager()
        else:
            print ("ERROR: The entry was invalid. Please try again.") 

def main():
    testQuery()

main()    

EXAMPLE INPUT/OUTPUT:

Do you have a set of score to average? Enter 'Yes' or 'No':Yes
Enter the numerical score, or enter 'x' to finish entering scores:1
Enter the numerical score, or enter 'x' to finish entering scores:2
Enter the numerical score, or enter 'x' to finish entering scores:x
The average of the scores is:  1.5
Do you have a set of score to average? Enter 'Yes' or 'No':Yes
Enter the numerical score, or enter 'x' to finish entering scores:1
Enter the numerical score, or enter 'x' to finish entering scores:2
Enter the numerical score, or enter 'x' to finish entering scores:x
The average of the scores is:  1.5
Do you have a set of score to average? Enter 'Yes' or 'No':No
Program Complete.
Do you have a set of score to average? Enter 'Yes' or 'No':No
Program Complete.
Do you have a set of score to average? Enter 'Yes' or 'No':No
Program Complete.
2

There are 2 answers

1
Seif El-Din Sweilam On

The problem is pretty complex, where the solution is pretty simple. By debugging the code tens of times, I recognised that the problem is in your testQuery() line in the scoreAverager function definition. You are entering the loop inside testQuery function while it's still running, and that makes the program stop when the value of done becomes 1 in the two running loops.

To solve this problem, simply delete the testQuery line in the scoreAverager function definition and the function will work with the same efficiency as when the loop in scoreAverager is ended, it will return back to the first launched loop in testQuery.

0
neutrino_logic On

I trimmed down your code a bit to isolate the problem, nested the ``scoreAvenger()``` and made the scope issue a little more explicit. What you've actually done here, it seems, is indirect recursion: you call a function that doesn't directly call itself, but which calls another function which calls it. You thus create a complicated recursive call stack which has to unwind itself.

This can be fixed using nonlocal but only if we nest the functions appropriately; then the recursive calls are terminated [Edit: NO! they continue, but the value of done doesn't get set to 0] (because the nested function captures the value of done).

def testQuery():
    def scoreAverager():
        nonlocal done       #here is the key: make done nonlocal 
        done = 0            #comment out above line to see the problem return
        while done == 0:
            score=input("Enter x")
            acceptedXs={"X","x"}
            if score in acceptedXs:
                print ("Returning to enclosing scope")
                done = True
                testQuery()
                print("stack unwinding in testQuery")


    done = 0
    while done == 0:
        moreTests=input("Want to enter nested scope/make another recursive call?")
        acceptedNos=("No","NO","no") 
        if moreTests in acceptedNos:
            print("Program Complete.")
            done = 1
        else:
            scoreAverager()
            print("stack unwinding in scoreAvenger")
def main():
    testQuery()

main()

This is pretty tricky, I think that's what's going on anyway. Edit: added the print functions which execute after as the recursive calls come off the stack.