Let's consider the following program the contains a function
def lie_about_age(age, be_older, be_younger): ''' (int, bool) -> None If only be_older is True, make age a year older. If only be_younger is True, make age a year younger. Otherwise, age stays the same Print the resulting age. ''' if be_older: age += 1 if be_younger: age -= 1 print("Age is", age) return lie_about_age(18, True, False)
Age is 19
We see that if we call our function with 18, True and True, it prints
Age is 19.
Let's trace that in the visualizer in PCRS.
[If you are reviewing these notes or following them on your own, go to PCRS and pick Code Editor from the top and then enter this program and step through it carefully line by line.]
def lie_about_age(age, be_older, be_younger, age_gap): ''' (int, bool) -> None If only be_older is True, make age age_gap year older. If only be_younger is True, make age age_gap years younger. Otherwise, age stays the same Print the resulting age. ''' if be_older: age += age_gap if be_younger: age -= age_gap print("Age is", age) return Michelle_age = 50 lie_about_age(Michelle_age, False, True, 3) print(Michelle_age)
Age is 47 50
When the function
lie_about_age begins to execute, a new frame is created on the stack. The new frame initially holds one variable is for each parameter in the function header (with the name from the header). The initial values of these variables are copies of the values of the expressions in the corresponding places of the function call.
In our example,
age gets its initial value from the variable
age_gap gets the value 3.
Let's do a little exercise to make sure you understand the model. Suppose I have this code,
draw a picture of the memory model as function
fun is just called but before it does
def fun(name, drinks_per_day): print(name) # ... rest of function not shown return patient = "fred" normal = 1 fun(patient.capitalize(), normal+2)
Again draw a picture of the memory model (the stack and the objects), just after this function is called and before it does anything. The program is a bit silly, but it illustrates a point.
def make_snow(snow_level, temperature): # draw the picture right now before this first if statement is evaluated if temperature < 0: snow_level = snow_level + 5 elif temperature == 0: snow_level = snow_level + 2 else: snow_level = snow_level/2 snow_level = 30 temperature = 0 make_snow(snow_level, temperature - 2) print('snow_level is', snow_level)
snow_level is 30
Using the visualizer, we see that there is a
snow_level variable in the global frame of the stack and another one in the frame for the function
make_snow. The same rules apply as before. The variables inside the frame for
make_snow are called local to that frame.
If we continue stepping through the program with the visualizer, we see that the local variable
snow_level changes but the global one does not. Then when the function returns, the stack frame for the function disappears. We can't access those local variables any more.
How would we change our function if we wanted to make use of the resulting snow_level? We would need to return it and then assign it to a variable.
def make_snow(snow_level, temperature): ''' (number, number) -> number Return the new snow level. ''' # draw the picture right now before this first if statement is evaluated if temperature < 0: snow_level = snow_level + 5 elif temperature == 0: snow_level = snow_level + 2 else: snow_level = snow_level/2 snow_level = 30 temperature = 0 snow_level = make_snow(snow_level, temperature - 2) print('snow_level is', snow_level)
snow_level is None
When we call a function, it always works the same way: A new frame appears on the stack, it has a local variable for each parameter (with the name of that parameter). The initial value of the parameter is a copy of the expression from the corresponding position in the function call.
This is true always, but it might not work as you expect at first for lists because of how lists are stored in the first place. Let's trace a function that changes a list.
def double_entries(L): ''' (list of int) -> None Change list L so that every element is doubled. ''' for i in range(len(L)): L[i] = L[i] * 2 my_list = [2, 5, 1] print(my_list) double_entries(my_list) print(my_list)
[2, 5, 1] [4, 10, 2]
Remember that lists are objects. So the value of a variable that is of type list, is actually a reference to that list. And when we copy a list we just get a copy of that reference. There is only one list and two variables that refer to it. Try tracing this code fragment in the visualizer.
my_list = ['alpha', 'bravo', 'charlie'] copy = my_list
One last thought, why is it called the stack? It is because the frames for functions stack up on top of each other (or possibly under each other.) So far we've mostly seen programs with at most one function. But suppose one function calls another. The same rules apply each time a function is called. And each time a function returns, the frame for that function disappears.
def remove_vowels(s): ''' (str) -> str Return a version of s with all the vowels removed. Y is not a vowel. ''' result = "" for ch in s: if not ch in "aeiouAEIOU": result = result + ch return result def obfuscate_entries(L): ''' (list of str) -> None Change list L so that every element has the vowels removed. ''' for i in range(len(L)): L[i] = remove_vowels(L[i]) my_list = ["heh", "IOU", "77"] print(my_list) obfuscate_entries(my_list) print(my_list)
['heh', 'IOU', '77'] ['hh', '', '77']