2

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.
Jongware
  • 21,058
  • 8
  • 43
  • 86
  • 1
    Have you noticed that the number of times the loop re-asks "do you have a set of score to average" after entering 'no' depends on how many times you previously said 'yes' and averaged numbers? – boog Feb 19 '20 at 22:02
  • 3
    You are calling `testQuery()` again from inside `scoreAveranger()`. Your program should just pass back to the calling testQuery() and not start another "nested" query again. When you type "no" in your example above you are only exiting one of these nested queries – Florian Braun Feb 19 '20 at 22:04
  • No solution for you, but you may be able to prevent issues with someone typing "yES" and generating an error message by changing to "if moretests.lower() == 'yes'" etc. Good luck! – Bryce Draper Feb 19 '20 at 22:04
  • You've created indirect recursion! See below. – neutrino_logic Feb 19 '20 at 22:52
  • You should really avoid printing everywhere in your functions, and focus on taking parameters and returning values. Also, variable and function names should follow the `lower_case_with_underscores` style. – AMC Feb 20 '20 at 02:15

2 Answers2

0

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.

  • Good catch and thanks for looking at it. My re-calling of testQuery seemed reasonable, but I see how and why that causes further issues. Since scoreAverager is running within testQuery it's better and easier to just let the loop close rather then re-call testQuery. That also solves the need for the break. – Bowerman9001 Feb 19 '20 at 22:48
0

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.

neutrino_logic
  • 1,239
  • 1
  • 4
  • 11