Today we’ll be talking about state retention across function calls.
If you prefer to watch a video version, here it is:
Normally, when a function returns a value, the value is all we have. All the other information held in the variables inside the function is lost.
But there are multiple scenarios where data must be remembered across function calls. There are a couple of ways this can be done. Let’s have a look at what options we have:
1) STATE RETENTION WITH GLOBAL VARIABLES
A simple although not optimal way of ensuring state retention across function calls are global variables, so variables defined directly in the module, outside any function or class.
If you want to learn more about global variables, I have an article about them. Feel free to read it.
Have a look at the following example. Here the enclosing function manageScore defines the score variable in global scope and sets it to 0. It also defines the hit function as a nested function and returns it. The hit function also declares the score variable global, which means it uses the score variable defined in the module. The nested function increments the score variable by 1 and prints the current value of score.
>>> def manageScore():
... global score
... score = 0
... def hit():
... global score
... score += 1
... print(f"Just hit an enemy! Your score is {score} points.")
... return hit
...
With the function defined, let’s use it in interactive mode. Let’s assign the hit function returned by the manageScore function to shoot1:
>>> shoot1 = manageScore()
Now shoot1 is the hit function, so we can call it like a function:
>>> shoot1()
Just hit an enemy! Your score is 1 points.
The state is now retained in global scope. As you can see, the value is incremented and printed:
>>> shoot1()
Just hit an enemy! Your score is 2 points.
If we keep assigning and calling the hit function, the score will be incremented and printed each time:
>>> shoot1()
Just hit an enemy! Your score is 3 points.
Let’s create another hit function object:
>>> shoot2 = manageScore()
>>> shoot2()
Just hit an enemy! Your score is 1 points.
>>> shoot2()
Just hit an enemy! Your score is 2 points.
If we use global variables to retain state, we can only retain one copy of the state. Let’s use the shoot1 function again. It left off at 3, so now it should be 4, but is it?
>>> shoot1()
Just hit an enemy! Your score is 3 points.
No, it isn’t. Both shoot1 and shoot2 share the same state of the global variable. If you want independent copies, nonlocal variables with closures could be a solution. Let’s look at them next.
2) NONLOCAL VARIABLES AND CLOSURES
Global variables are not the only way state retention may be achieved. The second option are nonlocal variables and closures. If we declare a variable nonlocal in the nested function, the program then works on the variable with that name which it finds in the enclosing function.
If the enclosing function returns another function and if that returned function remembers state from the enclosing scope, we call it closures.
I have an article about closures, so if you want to learn more about them, feel free read it.
Let’s modify our previous example. Here the enclosing function manageScore defines the score variable in its local scope and sets it to 0. It also defines the hit function as a nested function and returns it. The hit function declares the score variable nonlocal, which means it uses the score variable defined in the enclosing function. The nested function increments the score variable by 1 and prints the current value of score:
>>> def manageScore():
... score = 0
... def hit():
... nonlocal score
... score += 1
... print(f"Just hit an enemy! Your score is {score} points.")
... return hit
...
With the function defined, let’s use it in interactive mode. Let’s assign the hit function returned by the manageScore function to shoot1:
>>> shoot1 = manageScore()
Now shoot1 is the hit function, so we can call it like a function:
>>> shoot1()
Just hit an enemy! Your score is 1 points.
Although the manageScore function in which the score variable was created already returned, its state, which is the value of the score variable, is still remembered by the returned hit function. As you can see, the value is incremented and printed:
>>> shoot1()
Just hit an enemy! Your score is 2 points.
If we keep assigning and calling the hit function, the score will be incremented and printed each time:
>>> shoot1()
Just hit an enemy! Your score is 3 points.
If we create another hit function object, it will remember its own independent copy of the score variable:
>>> shoot2 = manageScore()
>>> shoot2()
Just hit an enemy! Your score is 1 points.
And if we call the first hit object again, it will remember where it left off and continue:
>>> shoot1()
Just hit an enemy! Your score is 4 points.
If you want to learn more about nonlocal variables, I have an article about them. Feel free to have a look at it.
I also an article about scopes, which is related to the topic of nonlocal variables, so you can read it too.
3) FUNCTION ATTRIBUTES
Another way of retaining state across function calls are function attributes. These are names that we attach to functions. Function attributes use the same dot notation as class or instance attributes. They work pretty much the same as nonlocal variables. One important difference is that they are accessible outside the nested function.
Let’s modify our previous example again. This time we’ll be using function attributes and closures. Let’s get rid of the nonlocal declaration. Instead we’ll define a function attribute. This is an attribute on the nested function, so it may be only initialized after the nested function is defined:
>>> def manageScore():
... def hit():
... hit.score += 1
... print(f"Just hit an enemy! Your score is {hit.score} points.")
... hit.score = 0
... return hit
...
Let’s again create the shoot1 and shoot2 functions and call them a couple of times to confirm that each returned function has its own copy of the attribute:
>>> shoot1 = manageScore()
>>> shoot1()
Just hit an enemy! Your score is 1 points.
>>> shoot1()
Just hit an enemy! Your score is 2 points.
>>> shoot2 = manageScore()
>>> shoot2()
Just hit an enemy! Your score is 1 points.
>>> shoot2()
Just hit an enemy! Your score is 2 points.
>>> shoot2()
Just hit an enemy! Your score is 3 points.
>>> shoot2()
Just hit an enemy! Your score is 4 points.
If we now call shoot1 again, it’ll use the value it left off at:
>>> shoot1()
Just hit an enemy! Your score is 3 points.
Function attributes are also accessible outside the nested function. So, let’s see what values they have for the two functions:
>>> shoot1.score, shoot2.score
(3, 4)