Memory Model -- Stack Frames and Local Variables

Let's consider the following program the contains a function lie_about_age.

In [16]:
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.]

In [17]:
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 Michelle_age and age_gap gets the value 3.

Exercise

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 anything.

In [18]:
def fun(name, drinks_per_day):
    print(name)
    # ... rest of function not shown
    return

patient = "fred"
normal = 1
fun(patient.capitalize(), normal+2)
Fred

Exercise 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.

In [19]:
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.

In [20]:
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

Lists as Parameters

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.

In [21]:
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.

In [22]:
my_list = ['alpha', 'bravo', 'charlie']
copy = my_list

Stacks of Frames

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.

In [23]:
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']