Machine Learning - Rock Paper Scissors, how does this persistent variable work?

Tell us what’s happening:
The function we’re supposed to change has this bit of code in it:

opponent_history.append(prev_play)

It adds the opponent’s previous play to the list, of course. But how does it work, and why?
We do not pass an argument for opponent_history to the player function, so it should be initialized as an empty array, but for some reason the contents just stick around over multiple independent calls.
Moreover, if I put the same line:

opponent_history.append(prev_play)

AFTER some other piece of code rather than as the first line or so, the function DOES get initialized with an empty array, and no information is carried over. Why? Is there a name for this behaviour I can study?

Finally, if I try to clear this variable, for example when we get a new opponent, I’d like to say something like:

if prev_play == '': opponent_history = []

In order to reset the memory when I get a new opponent, but the entire history still carries over. Same if I delete the opponent_history variable entirely.

I’d put more code here but I don’t have so much a code problem as I have a problem understanding what this behaviour is which I’ve never seen before.

Could anyone enlighten me?

User Agent is: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0

Challenge: Machine Learning with Python Projects - Rock Paper Scissors

Link to the challenge:

You may notice that in the line of codes just before the one you quote, opponent_history is already set to be an empty list as a default argument value:

def player(prev_play, opponent_history=[]):

Python’s Documentation says the following about it:

Important warning: The default value is evaluated only once. This makes a difference when the default is a mutable object such as a list, dictionary, or instances of most classes. For example, the following function accumulates the arguments passed to it on subsequent calls:

def f(a, L=[]):
    L.append(a)
    return L

print(f(1))
print(f(2))
print(f(3))

This will print

[1]
[1, 2]
[1, 2, 3]

So the author of the code makes use of this particular behaviour to keep the value of opponent_history saved between consecutive calls of the player function.

So if you want to clear the memory of opponent_history, you may pass an empty list in that second argument during call of player function:

player(prev_play, opponent_history=[])

That’s actually not allowed in the context of this challenge, since that call is in RPS_game.py, which we’re not supposed to alter.

I did manage to clear it after all, it seems it is possible to change this memory only on the first line of the function… So I managed with nested if statements. Now I just need to actually solve the problem…

Thank you for your insights! I will leave my question unanswered though, since I still don’t quite understand, and perhaps someone else might further enhance my understanding of this principle. I wonder if it’s even a feature, or rather a bug.

I may say it’s more a bug than a feature, because this behaviour is quite counter intuitive. Many people, including authors of Python’s documentation, warn against using a mutable default value as an argument. Here is from a not-to-do guide called The Little Book of Python Anti-Patterns:

Using a mutable default value as an argument

Passing mutable lists or dictionaries as default arguments to a function can have unforeseen consequences. Usually when a programmer uses a list or dictionary as the default argument to a function, the programmer wants the program to create a new list or dictionary every time that the function is called. However, this is not what Python does. The first time that the function is called, Python creates a persistent object for the list or dictionary. Every subsequent time the function is called, Python uses that same persistent object that was created from the first call to the function.

Anti-pattern

A programmer wrote the append function below under the assumption that the append function would return a new list every time that the function is called without the second argument. In reality this is not what happens. The first time that the function is called, Python creates a persistent list. Every subsequent call to append appends the value to that original list.

def append(number, number_list=[]): 
    number_list.append(number) 
    print(number_list) 
    return number_list 

append(5) # expecting: [5], actual: [5] 
append(7) # expecting: [7], actual: [5, 7] 
append(2) # expecting: [2], actual: [5, 7, 2]

Best practice

Use a sentinel value to denote an empty list or dictionary

If, like the programmer who implemented the append function above, you want the function to return a new, empty list every time that the function is called, then you can use a sentinel value to represent this use case, and then modify the body of the function to support this scenario. When the function receives the sentinel value, it knows that it is supposed to return a new list.

# the keyword None is the sentinel value representing empty list 
def append(number, number_list=None): 
    if number_list is None: 
        number_list = [] 
    number_list.append(number) 
    print(number_list) 
    return number_list 

append(5) # expecting: [5], actual: [5] 
append(7) # expecting: [7], actual: [7] 
append(2) # expecting: [2], actual: [2]

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.