Functions

0. Learning objectives


1. Functions

Creating variables is useful for abstraction, because it means we can re-use a variable over and over again in different calculations or keep track and change a value. But what if you want to do the same calculation multiple time using different values for the variables? Think about our first example when we were discussing algorithms:

Sally has 12 apples. She gives 3 apples to Manuel and buys 6 apples from Jiao. How many apples does Sally now have?

We asked ourself: what if every day we have different number of apples to start with and different number of apples for each transaction? Can we come up with an algorithm that always works?

This is a very common question: can we generalize a specific problem and come up with a more general solution? It is so common that pretty much every programming language has a construct that allows us to do just that: abstract and generalize.

This is where functions are useful: they allow us to abstract some calculation in terms of the variables that go into the calculation.

If we assign to the variable apples_start the number of apples Sally has when she starts, to apples_given the apples Sally gives away, and to apples_bought the number of apples Sally buys, can you come up with an expression that describes how many apples are left at the end of the day?

What about apples_start - apples_given + apples_bought?

We can even assign the result to a new variable: apples_left = apples_start - apples_given + apples_bought

Now that we think about it, does it even matter if it is Sally or somebody else who is dealing in apples? No! abstraction at work!

Even better, if we use more general variables names, we don't even care if what is exchanges is apples or oranges as long as we have a starting number, some are taken away, and some are added (even more abstraction!):

left = start - given + added

Despite being a trivial example, this gives you a feeling of the power of abstraction: you can remove the details and write code to solve more general problems that are somehow similar.

A similar approach can be applied to the cereal example we discussed before. Our brains were pretty good at realizing what steps are the same and what should be allowed to change. So we could just write an algorithm that gives instructions for pouring cereal and eating it, allowing the user to specify the type of cereal we want. Whenever we want some cereal, we just run the algorithm and specify the type of cereal.

Great! We get the idea that there we can generalize ideas and we saw that we can write instructions in Python that implement those ideas and allow us to perform a calculations between variables that represent "things" that we might want to change between different "runs" of our algorithm. What we are missing is a mechanism to tell Python that this bunch of instructions, that uses this bunch of variables to implement an algorithm, should be consider a "block of instructions" that we might want to run with different values for the variables...

Fear not! As I mentioned before, pretty much every language allows you to do this using functions.

Before we look at the syntax to define your own function, can you think of some things that we might have seen before that looked like function?

type(), int(), float(), str(), and bool() are all examples of functions that come wih the Python Standard Library. They are referred to as built-in function since they come included with Python. (Don't worry too much about all the other functions that you might see listed and the pages and pages of documentation about the standard library. We will look at many of the key components of Python a step at a time!)

2. Defining your own functions

On the right, you should see an example of a function definition in Python. Notice that the syntax highlighter (that feature in IDEs that recognizes different parts of the code, depending on the language, and colors them) colors the def word is in blue because it is a reserved Python keyword. Other IDEs might color it in different colors (in many you can even specify your favorite coloring theme): it should be purple in the example below. (Also notice that, if you are editing your code in a text editor that does not have a syntax coloring extension, it might just look like plain text, but it will still be recognized by the interpreter).

There are a few key ingredients we need in order to define a function.

Declare your function. The declaration must be started with the def keyword.

After the def keyword, you need to tell Python the name of your function. Naming your functions follows the same rules as naming variables. Remember the name of your function! You will need to use this name to call your function later.

Then you list all variables that your function needs to do the calculation you want surrounded by parentheses. These are the arguments (inputs variables) to your function. You can have as many inputs to your function as you want, and you must separate them using commas. In the example on the right, we have two arguments x and y. The naming of the arguments follows the same rules as variables and functions. You should try to give meaningful names to your arguments that reflect the values they will store. After defining your arguments in parentheses, you must add a colon :. The colon defines the beginning of a block of lines that represents a sequence of instructions. The first line of a function definition is also called the function header.

After the colon, anything you want to write inside your function must be indented by the same amount relative to the function declaration. According to the Python specifications, the indentation can be anything, as long as it is consistent within a single block (yes, it can be different from block to block). My recommendation is to always use 2 or 4 spaces (4 is the de facto standard for Python). Most IDEs will automatically enter those spaces for you as soon as you go to a new line within a block or as soon as you press the tab key on the new line. Python will let you know if it finds an inconsistency in your indentation with a nice IndentationError: unexpected indent.

The first indented thing you should place in the body of your function is a docstring. The docstring is written by you for humans and it describes what your function does and some additional information about arguments and returned value (more about return below). Note that docstrings are strings surrounded by """triple double quotes""".

In Python you can use either 'single' or "double" quotes to define strings as long as you match the opening and closing quotes (more about it later), so even docstring can use either """triple double""" or '''triple single''' quotes.

In the example, the docstring describes what the function does: it adds two numbers together. It also defines what we are expecting the type of the input variables to be, and what the type of the value returned by the function is going to be. Docstrings are optional, Python does not require them, but it's good practice to use one. It really helps making your code more readable and maintainable (imagine opening large program you didn't write - or one you wrote months earlier - and just find code, no comment whatsoever... I promise you it is going to be a nightmare trying to figure out what is going on or what you were thinking). In this course, you will always be required to write docstrings for your functions.

After you described what you function does, it is time to actually make your function do something! This might involve computing intermediate expressions, showing stuff to humans (printing), anything you need to achieve the purpose of the function.

Once you are done with the computation, your function might or might not need to return one or more values so that it can be used for additional computation. This is accomplished by the return keyword in Python. In the example, we are returning the value stored in the variable z since we want the result of x + y to be available to whoever called our add() function.

Python function always return a value. If you don't specify a particular value using return, the function will automatically return the value None (which is of NoneType introduced earlier).

Check out the function no_return_value() defined in the example. This function takes a variable (x), shows the content of x to humans using the print() function, and then it is done. No return! Nonetheless, we assign whatever no_return_values() returns to the variable z as we did for the add() function, and we show the value of z to humans using print(z).


3. Calling (executing) functions

If you look at the two examples, we first have the definition of our function, then the indentation disappears, indicating the end of the block of code that is part of the function, and we see one or more function calls (e.g., z1 = add(2, 3)).

In order to call a function, you have to provide all the arguments required in the function header. Our function add(), for example, requires two arguments (x and y) whereas our function no_return_value() only requires one argument (x).

In the first example, you can see that in line 14 we are calling the add() function passing the two required values (add(2,3)). The function does its job, adds the two values and stores the result of the addition in the variable z. After that, the function returns the value of the variable z (5) and the value is assigned to a different variable (z1). We then show the content of the variable z1 to humans using the print() function.

As you can see in line 17, we can also nest function calls to pass the output of one function into another one. This is an example of composition that, as we mentioned, is a very powerful approach to build solution to complex problem by composing solution to simpler problems. In this case, Python evaluated the innermost function and uses the returned result from that call as value for the argument of the next call. In the example, we call add(add(2,3), 7 ) which first evaluates add(2, 3) to give 5 which is then used as the first argument to the outermost function, which evaluates add(5, 7) to produce the final 12.

The second example is designed to show two things:

To execute the code in an example, you can press the Run button. This will show the output generated by the script below the code.

Warning: To reiterate, the print() function prints the value of its argument, typically a variables or a string, or a combination of the two, to the console for the human who is running the code. return is a Python instruction that returns one or more values back to whomever called the function. A very common source of confusion is to think that print() "returns" a value to whomever called the function. THIS IS WRONG! Look one more time at the no_return_value() example. We print the value of x but we do not use return to return the value of x to the caller. The result is that the variable z gets assigned the value None, that we then print.

The print() function itself does not return the value that it prints, it returns None.

What do you think the following is going to display? print(print('hello'))

Python will first evaluate print('hello') since this is the innermost function call. This will display hello in the console. Then print will return None. Therefore, the outermost print() will receive a None as argument and it is going to show it on the console.


4. Variable scope

Remember when we said that variables are like little boxes that hold values? Some questions might have had are:

  • who has access to the box?
  • how long does the box stick around?

Consider the code in the example below. It is pretty confusing, isn't it? Why do you find it confusing? The main issue is that x is everywhere! As you will see, it is not at all uncommon to reuse the same variable name over and over again, especially it it represents the same thing in different functions. Imagine a program that has to convert temperatures. You can imagine there might be the need for multiple function that deal with a temperature and it would make sense to use a variable called temperature in each of them.

So, how does Python keeps all these variables straight?

Each time Python executes a function it creates the equivalent of a larger box (associated with the function) in which it stores all the boxes associated with the variables we define within that function. This is called the scope. (We also call this scope stack frame or frame). This happens each time a function is called! If you call a function from within a function, as soon as the new function is called, a new nested scope is created.

In the example below there are two scopes:

The scope (and all the variable defined within that scope) exist as long as the scope is valid. The scope of the function, for example, is valid as long as the current function call is executing. After the function is done, the box is thrown away with all the variable boxes inside. If you call the same function twice (as in the example below), the second time you call the function, Python is going to create a new scope.

So, all the variables that are define within a function, only exist as long as a function call is executing. They stop to exist once the function is done and, if you call the function again, it is going to be a new set of variable which have nothing to do with those that existed during the previous call. The only way for a function to return the value of one of its variable to some code that is executing outside of the function, is to use the return statement.

So, now that you know about scope, pick a piece of paper and a pencil, and see if you can identify the different scopes in the code below. You can use boxes/frames to identify scopes and boxes inside those boxes to identify variables.

When you run the code, Python:

  1. creates a the main scope
  2. associates the definition of the function times_two() in that main scope
  3. executes the assignment x = 1 and associates the definition of the variable x with the main scope
  4. evaluate the expression x + 1 and calls the function times_two() using the resulting value (2)
  5. create a scope for the times_two() function. The variable x defined in the argument, that will have initial value 2, exists only as long as this particular call to the function is running.
  6. executes the statements inside the function and finishes the function returning the calculated value (4)
  7. destroys the scope created for the times_two() function call (and, with it, all the variables that we defined during the function call)
  8. continues evaluating line 14. Here we are calling again times_two() this time with x + 2. Since we are back in the main scope, the value of x is still 1. This x is not the same x as the one that existed in the scope of previous function call. So, we execute again steps 5 to 7 starting with a different argument value
  9. once we are back from the second call to times_two(), it can finally add the two returning values (4 + 6) and assign the result to the variable x in the main scope
  10. prints (for humans) the content of the variable x in the main scope: 10

Suggestion: Use the Download option under the Share You can copy the code and paste it in Thonny, then run it using the debugger. This will allow you to see exactly how Python goes through the code. Thonny will open a new window for each function call to show the execution of the function and a window listing the local variables. These are the variables that exists only as long as the function is executing (variables within the scope of the function).

Here are another couple of examples for you to examine.

The first one is quite similar to the one above. We just use the print() function and additional comments to highlight the scope of the various functions.

The second example it is a little more complex and you can see that the same function is called twice, each time with a different parameter. Use the debugger in Thonny to look at how Python handles the different calls.

For this last example, here is how the stack diagram (sequence of stack frames/scopes as the script gets executed) looks. This is an animation of the execution. You can get the same effect using the debugger in Thonny. In that case, each stack frame (or scope) is going to be the little window that Thonny pops up every time you step into a function.

Now, there may be cases in which the input variable is indeed modified by the function, but this only happens with special variable types, and we'll see that in a few weeks.


5. Accessing functions in modules

Imagine you wrote a bunch of functions that help someone eat cereal in a file called cereal.py. Maybe this file has the following function headers:

To be able to access these functions that we wrote, we have to tell Python that it should grab them from our file by importing them. This can be done using the Python keyword import. This should be done once at the top of your Python script. In Python jargon, the file from which we import stuff is called a module. Modules can be much bigger than a single file, but the way we import them does not change. After we imported them, the functions available in the module can be accessed using the dot notation. This means that we need to use a period (.) after the name of the module to call the particular function. See the example on the right.

First we call import cereal. Then the functions defined in the cereal.py module (file) are accessible by calling cereal.grab_bowl(500), cereal.pour_cereal('froot loops', 1) and cereal.eat_cereal().

We will use a few modules that are built into Python but there are gazillions of modules out there that have been developed to solve all sorts of problem and are ready to be downloaded, installed, imported, and used!

One that comes with the Python standard library is the math module, which contains a lot of useful functions for doing mathematical operations, like square-roots, powers, and trigonometric functions. The example below shows you how you can import and use the math module in a Python script. The great thing is that, thanks to composition, you can use any function (the one you develop or the one you import from modules) inside other function to build tools capable of solving the most complex problems!

Finally, if you want to know what functions are available in a module, you can use the dir() function passing as argument the module you have just imported. This will give you the list of all functions (and variables) in that module.

Try it in Brython! type importmath and then type dir(math). You should see a list of all the functions you can call! You will also see a lot of other strangely named objects (starting with a single or a double underscore (_). You can safely ignore those for now.

If you want to know how to call a specific function and what that function does, you can then use the help() function and pass as argument, the function you are interested. For example, calling help(math.pow) (after having imported the math module) will give you more info on how to use the pow() function. Note that this documentation is the docstring! And that's why it is important that you add docstrings to your functions!

Unfortunately, Brython has a very limited support for the help() function - a lot of the docstrings were stripped away to minimize the size of the code embedded in the browser! You can definitely use the help() function in Thonny or pretty much most of the other IDEs.


Exercises

Write a function that converts from Fahrenheit to Celsius. It should: