15-112 Lecture 4 (Monday, June 8, 2013)

Functions and Code Reuse

Functions are a way of taking a chunk of code, giving it a name, and clearly designating what inputs the code requires and what the resulting value is.

By creating functions from complex or reused code, we can make our programs more maintainable and more readable. If we have long, complex code, it can distract from the "Big picture" logic. To combat this, we can give it a meaningful name and "factor it out" into a function. Then, the detail is tucked away in the function, and the algorithmic, big-picture part of the code is clearer. It is like putting all of the tables and reference data at the back of a book, in an appendix, rather than in-line each and every time it is used.

And, if we have code that is used in mmore than one place, using functions enables us to write it only once and to "call" it from each use. this makes our program smaller. It means we have to type less, and it means that, if we ever need to revise the code, we only need to change it in one place -- rather than go hunting.

Let's see an example:


#!/usr/bin/python

def boxvolume(height,width,depth):
  volume = (height * width * depth)
  return volume

print "The volume of a 2x6x9 inch box is ",
print boxvolume(2,6,9),
print "cubic inches"

  

Notice that, in the example above, the arguments are positionals. In other words, the parameters or arguments passed into the function are matched up with the arguments of the formal argument list given as part of the functions definition by the order in which they are presented, rather than based upon the name of the variable or an explicit label.

Variables defined outside of any function are global variables and can be used by any function -- or outside of any function. If we define a variable within a function, by assigning it a value (rather than using its value), it will create a new local variable -- hiding the global variable. If we don't want this to happen, before assigning the variable a value, we can explicitly label the variable so that Python knows we want to keep the global one. We do this with the keyword "global", as below:


#!/usr/bin/python

value = 3

def fun():
  value = 2
  print value

fun()        # Prints 2
print value  # Prints 3
  

As compared to the example below:


#!/usr/bin/python

value = 3

def fun():
  global value       # Notice the keyword "global"
  value = 2   
  print value

fun()                # Prints 2
print value          # Prints 2
  

What are the take-aways?

Variable Argument Functions

Most of the time our parameter passing will be based upon positional arguments. This is because it is quick, easy, and familiar to programmers who have worked with many languages. But, sometimes we want to have the ability to define functions that take arguments in different orders, or leaving some arguments out.

For this to work, we have can't use the position of an argument to identify it. The position could change depending upon the presentation order and/or which arguments are presented (vs "left out"). Python solves this problem by allowing the programmer to specify defualt values for "left out" arguments and allowing us to label arguments, so Python can sort them out, even if they are presented out of order. Python even allows us to combine the two systems by leaving the first arguments, those before any are presented out of order or missing, without labels -- and labelling them only after the point where somethihng is out of order or missing. We do this labelling by passign arguments in the form "argumentname-value", where value is the same as it would be for a positional -- what is being passed. And, the argument name is the name assigned to the argument -- in the formal argument list, where the function is defined. An example is below:


def printRecord(name, role="Student", status="Active"):
  print name + ": " + role + "(" + status + ")"

printRecord("Jane Blogs")
printRecord("John Blogs", status="Graduated")
printRecord("Thomas O'Connor", role="TA")
printRecord("Gregory Kesden", role="Instructor")
printRecord(role="Instructor", name="Gregory Kesden")
  

The Stack Data Structure

In computer science, a Stack is a data structure that works very much like a stack of papers on your desk. You can access only the top of the stack. So, when you add something to the stack, you do it by piling it on top. And, when you take something off, you take it off of the top of the stack. You can only see the top of the stack. And, although the stack is ordered, it isn't "indexed". You can't grab the 2nd or 5th item, or whatever, unless you get there by working your way from the top down.

We generally call the operation of adding something to a stack the push operation, becuase it pushes the top down, hiding what is underneath. We call the operation of removing something from the stack pop, because it pops off the stack, revealing what was underneath. We can also peek and look down at the stack and see the top item -- which is the equivalent of popping it, looking at it, and pushing it right back.

A stack is a Last-In/First-Out (LIFO) data structure. In other words, the most recently pushed item is at the top and able to be popped, (last-in/first-out), whereas the first item pushed is burried at the bottom an not accesible.

Stacks turn out to be a very useful data structure for solving problems by computer for the same reason they do in our everday worlds -- a lot of times more recently used items are more likely to be needed than less recently used items --and stacks organize things to make this type of use easy.

We're going to learn how to implement our own stacks and other data structures soon. But, for now, we're going to learn how Python uses a stack to solve an important problem.

The Runtime Stack

Consider the following code:


 
#!/usr/bin/python

PI = 3.14

def multiply (x,y):
  return x * y

def areaCircle(radius):
  return multiply(PI, radius**2)
  
def volumeCylinder(radius, height):
  return multiply(height, areaCircle(radius))

print volumeCylinder(3,2) # Prints 56.52
  

Notice that, at one point in its execution, volumeCylinder() has executed up until the point where it calls areaCircle(), which has, in turn, executed until it calls multiple(). This represents a call chain as might be represented as follows:


  volumeCylinder(3,2)-->areaCircle(3)-->multiply(3.14,3)
  

Notice also that these functions will return in the opposite order and then volumeCyclinder() calls multiply(). multiply() then returns, followed by volumeCylinder()


  volumeCylinder(3,2)-->areaCircle(3)-->multiply(3.14,9)
  volumeCylinder(3,2)-->areaCircle(3) [28.26]
  volumeCylinder(3,2) [28.26]
  volumeCylinder(3,2)-->multiply(2,28.26)
  volumeCylinder(3,2) [56.52]

  print 56.52
  

The Python interpreter is able to keep track of a function's state using a stack, known as the runtime stack. The information it keeps on the stack about any particular call to a function is known as the stack frame. In 15-213 you'll learn the details, but for our purposes, we'll consider the stack frame to include the arguments, local variables, and return location. Every time a function is called, this information is pushed onto the stack. Every time it returns, it is popped from the stack. The example below is another way of looking at the example above -- this time organizing it around the stack and stack frames. To make it easier to understand, the lines of the program are numbered and one stack frame is shown per line in the stack trce that follows it.


 
1. #!/usr/bin/python
2.
3. PI = 3.14
4.
5. def multiply (x,y):
6.   return x * y
7.
8. def areaCircle(radius):
9.   return multiply(PI, radius**2)
10.
11. def volumeCylinder(radius, height):
12.   return multiply(height, areaCircle(radius))
13.
14. print volumeCylinder(3,2) # Prints 56.52

PUSH: volumeCylinder(3,2) [(3,2), line 14]
----------------------------------------
volumeCylinder(3,2) [(3,2), line 14]


PUSH: areaCircle(3) [(3), line 12]
----------------------------------------
volumeCylinder(3,2) [(3,2), line 14]
areaCircle(3) [(3), line 12]
----------------------------------------


PUSH: multiply(3.14,9) [(3.14,9), line 9]
----------------------------------------
volumeCylinder(3,2) [(3,2), line 14]
areaCircle(3) [(3), line 12]
multiply(3.14,9) [(3.14,9), line 9]
----------------------------------------

POP: {multiply() returns}
----------------------------------------
volumeCylinder(3,2) [(3,2), line 14]
areaCircle(3) [(3), line 12]
[Return value = 28.26]
----------------------------------------

POP: {areaCircle() returns}
----------------------------------------
volumeCylinder(3,2) [(3,2), line 14]
[Return value = 28.26]
----------------------------------------

PUSH: multiply(2,28.26) [(2,28.26), line 12]
----------------------------------------
volumeCylinder(3,2) [(3,2), line 14]
multiply(2,28.26) [(2,28.26), line 12]
----------------------------------------

POP: multiply() returns
----------------------------------------
volumeCylinder(3,2) [(3,2), line 14]
[Return value = 56.52]
----------------------------------------

POP: volumeCylinder() returns
----------------------------------------
[Return value = 56.52]
----------------------------------------
    

A Preview of Recursion

In class, we also notes that this system enables any function to be called more than once within a particular call chain. For example, a function might directly call itself, a situation known as recursion. This isn't a problem, becuase each call to the function results in its own stack frame and its own state. It doesn't matter that then controlling code happens to be the same, the exeuction is independent.

We'll talk more about this soon, but below is a quick example of a recursive function:


  #!/usr/bin/python

  def pow2(x):        # This function only works for (x>=0)
    if (x == 0):
      return 1        # x^0 is always 1
    
    return 2*pow2(x-1)