Hello, and welcome to the eighth episode of the Software Carpentry lecture on Python. In this episode, we'll show you how functions work, and how to define new functions of your own.
First, a bit of design philosophy. A programming language should not try to include everything that anyone might ever want, because (a) it's impossible, and (b) the resulting language would be so large that it would be impossible to learn.
Instead, languages should make it easy for people to create what they need to solve their specific problems.
Every language does this by allowing programmers to define functions that carry out new higher-level operations.
Which leads some people to regard programming as the act of creating a mini-language in which the solution to the original problem is trivial.
In Python, we define new functions using the keyword def
.
For example, here's a function that does nothing but return a particular string to its caller.
To call it, we just use the function's name, followed in this case by empty parentheses. If we print the result, it's the string the function returned.
To make functions more useful, we can give them parameters.
Here's another greeting function that takes the name of the person being greeted as a parameter. What it returns depends on that parameter's value.
Let's have a closer look at what happens when we call it. First, we assign the string 'doctor'
to the variable temp
.
When we call greet
with temp
as a parameter, Python creates a variable called name
and copies the reference in temp
into that variable. temp
and name
are now aliases for the same string.
Inside the function, another new variable called answer
is created to hold the result of concatenating 'Hello, '
and the value of name
.
When the function returns, its variables name
and answer
are discarded, but the value that answer
was pointing at is returned and assigned to result
.
As this example shows, each function call creates a new set of variables called a stack frame. Each function call's frame is stacked on top of those that are currently active, just like the plates in my sink.
Let's have a look at another example. Here we have two functions: add
and double
.
In our main program, we give val
the value 10.
We then call the function double
, which creates a new stack frame. Initially, that stack frame only contains the variable c
, which is the function's sole parameter, and that variable points at the value that was passed into the function.
Since double
immediately calls add
, Python immediately pushes another frame on top of the stack. Since add
also only has one parameter, this frame initially only contains the variable a
. Our 10 is now pointed at by three aliases, one in each of three different frames.
Now we finally do some computation: b
is assigned the value of a
plus 1. Python creates the new value in memory, then creates a new variable b
in the current stack frame to point to it.
Next, the function add
returns that value to its caller. add
's stack frame is popped off the stack and thrown away; the 11 it returned is multiplied by 2 and the result assigned to a new variable d
. This variable is in double
's stack frame, as shown here.
Now it's double
's turn to return. Its result, 22, is assigned to result
, and double
's stack frame is discarded.
The program can finally print the result of this sequence of calls. Every modern programming language uses some variation on this basic model: each function call creates a new stack frame, which has its own set of variables, and which is discarded when the function returns.
Stack frames also determine what variables are visible, or in scope, at any time. In Python, the program can only "see" variables in the current frame and the base "global" frame (which isn't part of any function call).
If the current and global frames have variables with the same name, the one in the current frame takes precedence.
Here's an example: we define a function that, when called, will create a local variable temp
, and then create a global variable that's also called temp
before calling the function.
This is what's in memory just before the function returns. As you can see, the function's temp
is a separate variable from the global temp
, and when the function refers to temp
, it means its temp
, not the global one.
The final output of this contrived little program is therefore what's shown here.
In our examples so far, we have always assigned the values returned by functions to variables before using them so that our diagrams will be easier to draw. We don't have to do this: Python will create hidden temporary variables for us when and as needed.
For example, here's our greeting program rewritten so that the only explicit variable is the parameter name
to the function greet
.
Just before the function returns, Python has created two such variables: one in the global frame to refer to the string 'doctor'
before it's passed into the function, and one inside the function's stack frame to temporarily store a reference to the result that is to be returned. Their actual names are a lot longer, and less readable, than _x1_
and _x2_
, but that doesn't matter: they're never visible to programmers.
Python has an explicit return
keyword because a function can return a value at any time.
Here, for example, is a function that returns the sign of a number.
If we call it with the parameter 3, the first branch of the if
executes and returns 1.
If we call it with -9, the return
in the else
is executed.
Returning as soon as a value is known is handy, but over-use can make functions hard to understand, since people have to read the whole thing to figure out how it might behave.
There are no hard and fast rules for what's good practice, and what's abuse, but most programmers would agree that…
it's OK to have a small number of "early returns" at the very start of the function to handle special cases, and then…
…one at the end to return the "general" result. We'll see examples of this style later on when we start to write larger programs.
One important thing to note about Python is that every function returns something, even if it doesn't have an explicit return
statement.
To see how this works, let's comment out the last two lines of our sign function.
The sign of 3 is still 1…
…but now, the sign of -9 is None
.
The rule in Python is that if a function doesn't explicitly return something else, it returns None
. Other languages do this differently: in C, for example, trying to assign the "result" of a function that doesn't return one is a compilation error—the program can't even be run.
This kind of behavior is one more reason why commenting out blocks of code is a bad idea: it's all too easy to accidentally get rid of a return
statement, after which your function will silently be telling its caller "no data".
Another important feature of functions in Python is that, like variables, they don't have specific types: their parameters can be anything at all, and so can their return values.
For example, here's a function called double
that multiplies its argument by 2.
double
of 2 is 4…
…and double
of the string 'two'
is the string 'twotwo'
. In the first case, the function took an integer and returned an integer; in the second, it took a string and returned a string. Python's quite happy to do this…
…since there's nothing in the function that depends on its parameter having either specific type. For obvious reasons, you should only rely on this behavior when the function only does things that will work on all possible types of input.
It's possible to write code like this, that does different things depending on parameters' actual types…
…but it's almost always a sign of bad design, since it will have to be rewritten every time you want to generalize the function.
If you really want to do this, there's a much better way, which we will explore in detail in the lecture on object-oriented programming.
For now, let's go back to functions. As we said earlier, references to values are copied into parameters when the function is called.
This creates aliases: in particular, it means that when a list is passed into a function, what's actually passed in is an alias for the list.
To explore what this means in practice, here's a function that takes a string and a list as parameters, and appends something to both.
Here's some code to set up a pair of variables and call that function.
Just before the call, the global frame has two variables, as shown here.
The call places a new frame with aliases for those variables on top of the stack.
The statement a_string += 'turing'
creates a new string and overwrites the value of a_string
with a reference to it.
The statement a_list.append('turing')
, however, actually modifies the list that a_list
is pointing at, which is the same thing that list_val
in the caller is pointing it.
Sure enough, when the function returns and the call frame is thrown away, the new string 'alanturing'
is lost, because the only reference to it was in the stack frame. The change to the list, on the other hand, is kept, because the function actually modified the list in place. We'll explore this idea of passing references around in more detail in a later lecture: it turns out to be fundamental to a lot of computational thinking.
First, though, let's finish our look at Python functions. To avoid writing redundant code, we can define default values for parameters.
Here, for example, we've defined an adjust
function that has two parameters, but knows to use the value 2.0 for the second one if the caller doesn't pass a value in.
If we call this function with one parameter, it is assigned to value
, and 2.0 is used for amount
.
If we call it with two parameters, the second overrides the default for amount
.
One function with default values is usually easier to read than several functions, each taking a different number of parameters.
For example, if Python didn't support default values, we would probably write two functions: one to handle the general case, and another with a slightly different name to handle the common case that did nothing but call its more general cousin.
One restriction, though, is that all of the parameters that have default values must come after all of the parameters that don't.
To see why, imagine we were allowed to mix defaulting and non-defaulting parameters as shown here.
If we call the function with just one parameter, it's pretty clear that its value has to be assigned to middle
…
…but what should the program do if the function is called with two parameters?
Should it use the provided values for the first and second parameters, and the default for the third…
…or use the first parameter's default, and assign the given values to the second and third? We could define a rule, but it's simpler and safer to disallow the problem in the first place.
To close off this episode, let's try to answer a frequently-asked question: when should we write functions? And what should we put in them?
The answer depends on the fact that human short-term memory can only hold about seven items at a time. If we try to remember more unrelated bits of information than that for more than a few seconds, they become jumbled and we start making mistakes.
In particular, if someone has to keep a dozen things straight in their mind in order to understand a piece of code, that code is too long.
Functions are a way to divide code up into more comprehensible pieces: essentially, to replace several pieces of information with one to make the whole easier to understand.
Functions are therefore not just about eliminating redundancy: they're worth writing even if they're only called once.
Here's an example. This code uses meaningful variable names, and is well structured, but it's still too much to digest in one go.
Let's start by replacing the loop bounds with function calls that give us a bit more context. grid_interior
of arg
might just return range(1, arg-1)
, but try reading the first two lines of this code aloud, and then the first two lines of what it replaced. This version is easier to understand.
Now let's replace those two conditionals with function calls as well. Again, we've reduced the number of things our eyes have to scan, and provided more information about what we're actually doing.
Finally, let's call a function that handles updates to our data structure. Our original nine lines have become five, and those five are all at the same level. It's hard to pin down exactly what that means, but most programmers would agree that the first version mixed high-level ideas about boundaries and update conditions with low-level details of grid access and cell value comparisons. In contrast, this version only has the high-level stuff; the low-level implementation details are all hidden in those functions.
A good programmer would probably actually write the code shown here right off the bat…
…then go back and write the functions that it assumes…
…and finally refactor. Essentially, the best way to program is to pretend that the special-purpose computer you wish you had already existed, then bring it into existence piece by piece.
We'll see more of this top-down style of program design in future lectures.