Week 6

Today's agenda:

  • Returning values from functions
  • stack frames
  • variable scope

Functions and return values

We have been writing functions for a while now. However, all of our functions are like a black box. Stuff goes into it, but nothing ever comes back out of it...

In [1]:
# Find the smaller of two numbers

def findSmaller(x, y):
    if x < y:
        print x
    else:
        print y
        
def main():
    a = int(raw_input("Give me a number: "))
    b = int(raw_input("Give me another number: "))
    findSmaller(a, b)
    
main()
    
Give me a number: 7
Give me another number: 12
7

The findSmaller() function prints out which of the two numbers are smaller, but what if we want to use the result in another computation? We can't, 'cos it's not giving us back a value.

We can use the return command to make functions give us back a result:

Syntax

return <expr>

When the function sees a "return" command, it terminates immediately, and the value of <expr> is returned to the calling function as the "result" of the function.

In [2]:
# Find the smaller of two numbers
# Function only does the task itself (find the smaller number)
# Everything else (including input/output) is done by main()

def findSmaller(x, y):
    if x < y:
        return x
    else:
        return y
        
def main():
    a = int(raw_input("Give me a number: "))
    b = int(raw_input("Give me another number: "))
    result = findSmaller(a, b)
    print "The smaller of the two numbers is", result
    
main()
    
Give me a number: 8
Give me another number: 16
The smaller of the two numbers is 8

Some function terminology:

def name(formal parameters):
  body

def functionName(param1, param2, ...):
  do this
  and this
  return this
  1. invoking or calling: starting the function's execution
  2. arguments: actual values that are inputs into a function
  3. formal parameters (or just parameters): variables that stand for inputs into a function
  4. return values: outputs or results from a function
  5. scope: the places in a program where a given variable may be referenced

Function Stack Diagrams

Run update21 and try the understandingFunctions.py program.

Function Stack

When the program starts running, Python allocates a chunk of memory to the program. The memory can be divided into two areas, the "stack", and the "heap".

The stack contains "frames", which represent functions that have been called, but have not yet returned (i.e. have not yet terminated).

Global Frame

At the beginning of the program, Python creates a "frame", which is a chunk of memory, and puts it onto the stack. This first frame is called the "global frame". It represents the program itself.

Python reads in the program code from top to bottom. When it sees a function, it puts the name into the global frame.

Things that live inside a frame are names (e.g. names of variables, names of functions, etc). The values that the names refer to are stored on the "heap". Pointers connect the names with the values.

When Python sees an unindented statement (i.e. a statement that's not inside any function), it executes it. Obviously, whatever the statement calls must already be in the frame! (Otherwise Python won't be able to access it.)

Things that are in the global frame can be accessed anywhere inside the program.

In our case:

  1. Python sees the function definition for findSmaller, so it places that onto the global frame, with a pointer to the programming statements.

  2. Then Python sees the function definition for main(), so it places that onto the global frame.

  3. Then Python sees the call to main(), and it calls main(). Since the main() function's name is already on the global stack, Python can follow the pointer to the programming code and run the function.

Individual function frames

As functions are called, their frames get added to the function stack. The stack grows "upwards", like a stack of dishes.

Variables that are defined inside the function are added to the function frame. Again, the names of the variables "live" inside the function frame, but they point to values that are on the heap.

The function only has access to variables that live "inside" its frame. It cannot access variables outside its frame.

The function whose frame is at the top is the currently-executed function.

In our case:

  1. The call to main() creates a new frame (call that the main frame) and places it on top of the global frame. The variables n1 and n2 are then created inside the frame, with pointers pointing to their values on the heap.

  2. The call to findSmaller() inside main() creates a new frame for findSmaller() and places it on top of the main frame. x and y are created inside the frame, with pointers pointing to their values on the heap.

When functions return

When a function terminates, their return value (if any) is "sent back" to the calling function. The frame for the terminated function is then destroyed.

Variable Scope

The scope of a variable is where it may be accessed. The scope of all variables is the frame in which they were created.

Therefore, n1 and n2 cannot be accessed in findSmaller().

x and y cannot be accessed in main()

Note

We can use www.pythontutor.com to visualize the call stack. However, please be careful about two things:

  1. They draw their stack growing downwards. That's actually contrary to convention. When you put a new dish on a stack, you put it at the top! Therefore, we will draw our stack growing upwards.
  2. They draw their variables and values inside the function frame. That's wrong. The values for the functions do not live inside the frame. They live on the heap.

Exercise:

In pairs, work on the tryFunctions.py program. Draw out the stack.

Functions and Variables

Try the understandingFunctionVariables1.py program. Does the output match with your intuition?

For simple data types (int, float, string, boolean), the "thing" that lives on the heap is a value. When a new value is created (e.g. when we do x+1), this creates a new "thing" on the heap. When we do the assignment statement, this makes the variable point to the new "thing".

Since the x variable in myFunction() and the x variable in main() are two separate variables, reassigning the x variable in myFunction() to point to a new value does not affect the x variable in main()!

Compare this with understandingFunctionVariables2.py. In the latter case, the assignment to x in main() changes the pointer of the x variable in main().

Remember: Python won't change the value of a variable (or change the pointer of a variable) unless there is an explicit assignment operator!

Functions and Lists

Try sumUpList.py and draw out the stack.

Note that the list "lives" on the heap, and the function variables in both sumUpList() and main() point to the list. This implies that if sumUpList() changes the list in any way, then main() will see it when the function returns!

In [5]:
def square(list):
    for i in range(len(list)):
        list[i] = list[i]*list[i]

def main():
    myList = [1, 2, 3, 4, 5]
    print "Before calling square():", myList
    square(myList)
    print "After returning from square():", myList

main()
Before calling square(): [1, 2, 3, 4, 5]
After returning from square(): [1, 4, 9, 16, 25]

However, note that only works if we are indexing into the list. What is the difference between this code and what we wrote above?

In [6]:
def square(list):
    for item in list:
        item = item * item 

def main():
    myList = [1, 2, 3, 4, 5]
    print "Before calling square():", myList
    square(myList)
    print "After returning from square():", myList

main()
Before calling square(): [1, 2, 3, 4, 5]
After returning from square(): [1, 2, 3, 4, 5]

Lists, Strings and Objects

In Python, lists and strings are implement as objects. That means that they:

  • Can store multiple pieces of data
  • Can "do things"

There are tons of things that we can do to lists and strings (or that they can do to themselves).

Strings

In [14]:
s = "Hello, World!!!! How are you today!!!"
print "s.upper():", s.upper()
print "After s.upper(), s = ", s
print "s.lower():", s.lower()
print "After s.lower():", s
print "s.index(\"w\"):", s.index("w")
print "s.count(\"o\"):", s.count("o")
print "s.count(\"ello\"):", s.count("ello")
print "s.split():", s.split()
words = s.split()
print words
print "Does s contain the string \"ow\"?", "ow" in s
print "Does s contain the string \"now\"?", "now" in s
s.upper(): HELLO, WORLD!!!! HOW ARE YOU TODAY!!!
After s.upper(), s =  Hello, World!!!! How are you today!!!
s.lower(): hello, world!!!! how are you today!!!
After s.lower(): Hello, World!!!! How are you today!!!
s.index("w"): 19
s.count("o"): 5
s.count("ello"): 1
s.split(): ['Hello,', 'World!!!!', 'How', 'are', 'you', 'today!!!']
['Hello,', 'World!!!!', 'How', 'are', 'you', 'today!!!']
Does s contain the string "ow"? True
Does s contain the string "now"? False

In [9]:
# One more:
s1 = "   hello    "
print "s1.strip():", s1.strip()
print "s1 is now", s1
s1.strip(): hello
s1 is now    hello    

Strings in Python are immutable. That is, you cannot change the contents of a string by calling its methods. Any method that messes around with the contents of a string returns a pointer to a new string with the new contents.

String operations can be found here: https://docs.python.org/2/library/stdtypes.html (Section 5.6.1)

Lists

Python's strings are a version of lists. Therefore, lists can do most of the stuff that strings can (obviously, not stuff like upper(), lower(), etc!)

Unlike strings, lists are mutable. This means that methods that mess around with a list's contents actually change the list.

In [10]:
list1 = [1, 3, 5, "sheep", "goat", 3.56, " "]  # a list in python can contain any kind of data
list2 = [5, 2, -1, 99, 103, 64]

list2.sort()  # this returns nothing
print "After sort, list2 =", list2  # contents of list2 actually get changed!
list1.sort()  # try sorting all sorts of different stuff
print "After sort, list1 =", list1 # numbers first, then strings, by alphabetical order
list2.reverse() # again, returns nothing
print "After reverse, list2 =", list2
print "Looking for a sheep in list1: it's at position:", list1.index("sheep")
print "Adding a camel to list1:", list1.append("camel")
print "list1 is now:", list1
After sort, list2 = [-1, 2, 5, 64, 99, 103]
After sort, list1 = [1, 3, 3.56, 5, ' ', 'goat', 'sheep']
After reverse, list2 = [103, 99, 64, 5, 2, -1]
Looking for a sheep in list1: it's at position: 6
Adding a camel to list1: None
list1 is now: [1, 3, 3.56, 5, ' ', 'goat', 'sheep', 'camel']

In [12]:
# We can combine string and list methods to do a lot of interesting stuff:

s1 = "four score and seven years ago our fathers brought forth on this continent"
list1 = s1.split()  # split the string into words
list1.sort()  # sort by alphabetical order
print "All the words in the first phrase:", list1

# Now let's extract all the words that begin with a vowel:
list2 = [] # empty list
for word in list1:
    if (word[0] in "aeiou"):
        list2.append(word)
print "Vowel-beginning words:", list2
All the words in the first phrase: ['ago', 'and', 'brought', 'continent', 'fathers', 'forth', 'four', 'on', 'our', 'score', 'seven', 'this', 'years']
Vowel-beginning words: ['ago', 'and', 'on', 'our']

In [18]:
# and of course, we could pull out the code into a function and make the function return the list

def extractVowelStartingWords(string):
    words = string.split()
    vowelWords = [] # empty list
    for word in words:
        if (word[0] in "aeiou"):
            vowelWords.append(word)
    return vowelWords

def main():
    s = "four score and seven years ago our fathers brought forth on this continent"
    vowelStarters = extractVowelStartingWords(s)
    for word in vowelStarters:
        print "This word starts with a vowel:", word
        
main()
This word starts with a vowel: and
This word starts with a vowel: ago
This word starts with a vowel: our
This word starts with a vowel: on

Python documentation on lists: https://docs.python.org/2/tutorial/datastructures.html (Section 5.1)