portions of this notebook were adapted from Mark Krumholz's public course notes

Names: [Insert Your Names Here]

Lab 2: Functions and Control Flow

Lab 2 Contents

  1. Python Functions
    • Basic functions
    • Modules and importing
    • Optional and default arguments
  2. Conditional Statements and Control Flow
    • The if statement and comparison operators
    • Iteration
    • Exception handling

1. Python Functions

1.1 Basic Functions

Thus far what we've done in python has been very simple, short tasks. To be useful, though, programs often need to be much longer. Moreover, such programs often involve tasks that must be done repetitively, or there are tasks that we want to perform that are common to many programs. We've already encountered an example of the latter: the sin function. There are many, many reasons why one might want to take the sin of something, so it is useful to define the sin function once and for all so that any program that needs to take the sin can do so, rather than having to come up with their own approximation of the sin using the Taylor expansion or something like that.

The capability that we are seeking is provided by defining new functions. This allows us to make our own functions that are just like the sin function, and can be called in a similar way. Functions are defined by the following syntax:


In [ ]:
def myfunc(arg1, arg2):
    print("I am a function! Here are my arguments:")
    print(arg1)
    print(arg2)
    print("I am returning my first argument now!")
    return(arg1)

This defines a very simple function. Let's walk through this declaration step by step: The first line begins with def, then the name of the function, and then in parentheses a list of arguments for the function, then a colon. Arguments are inputs to the function. For example, the sin function takes an angle as an argument. In this example our function has two arguments. The number of arguments is arbitrary, and can be zero, in which case the parentheses are just left empty. It is also possible to write functions where the number of arguments is variable, and need not be the same every time the function is called, but we won't discuss that capability further in this short course.

After the define line, we begin the body of the function. Note that all the lines in the function body are indented. This indentation is IMPORTANT. In python, indentation is used to indicate that a particular line belongs to a particular function, loop, or other block of code. All the lines of the function are indented four spaces. If you're using entering this manually in ipython, either at the command line or in the notebook, you don't need to type in those four spaces by hand; the ipython shell will automatically enter them for you after seeing the def line. If you're using emacs as a text editor, you can just hit the tab key and the correct number of spaces will be entered for you.

Within the body of the function, we can enter whatever commands we like. We can print things, for example. The arguments in that appeared in parentheses in the definition are accessible within the function, and can be manipulated however we like.

At the end of the function, we have a statement that begins return. A return function causes the function to give back a value, which the calling program can print, assign to a variable, or do something else with. For example the sin function returns the sin of the input angle. Return values are optional: functions don't have to return anything, and can just end.

OK, with that in mind, let's try defining this function


In [ ]:
def myfunc(arg1, arg2):
    """
    This is a function that does nothing in particular
    """   
    print("I am a function! Here are my arguments:")
    print(arg1)
    print(arg2)
    print("I am returning my first argument now!")
    return(arg1)

When we enter that, nothing is printed out, but python now knows about this function. To demonstrate this, let's try calling it:


In [ ]:
myfunc(1,2)

Note two things about the formatting of the function.

  1. I included something called a "docstring" (denoted with the triple quotations at the start and end). This is a description of what the function does and is visible when call that function with a question mark (as below). Many companies (e.g. Google) have extensive rules about what should be included in a docstring. For example, here is a sample Google docstring. Generally speaking, it should include a description of what the function does, some notes about the input and output, and specifics about any optional inputs ("keywords") and what they do. Keep your eye out for these as we proceed as I'll be asking you to include docstrings with all of the functions that you write this semester.
  2. Spacing matters in python, and the code that actually defines the function is all indented by 4 spaces relative to the def line. <div>

In [ ]:
myfunc?

Exercise 1

Write a function called "hellox" that does the following.

  1. Takes an input variable (a name).
  2. Defines a string variable myname that is equal to your name, or some combination of your group members' names.
  3. prints "Hello input, my name is myname.", where input and myname are described in 1 and 2
  4. returns nothing
  5. Has a simple docstring that describes what it does

<div>

1.2 Modules and Importing

We just entered a function at the command line. This is helpful in that it lets us use the function any time we want without having to type in every single command more than once. When you define a function within a notebook, you can use it throughout that notebook without having to redefine it. If you wanted to use the function in a different notebook though, you'd have to redefine the function by typing in all the commands again, however (or copy pasting them into the new notebook).

When you've written a useful function that you will want to use again, a nice python feature is the ability to write a function once, in a file, and then never have to type it again. Python provides this capability through what are called modules. A module is just a file that defines functions, and perhaps other things (for example variables). Modules files must end with the extension .py. Let's write our function into a module.

To do so, navigate one tab over from this document in your web browser (should be labeled "Home" and show all of the files in the directory where you launched Jupyter). Click on "New" in the upper right and select Text File, then copy and paste the text below into that text file (ignore the ```python at the beginning and the ``` at the end - this is a way to tell the markdown cell that you want the text to be formatted like a block of python code, but not executable like python code). Save it as mymodule.py and close it.

# I am a module!
def myfunc(arg1, arg2):
    print("I am a function in a module! Here are my arguments:")
    print(arg1)
    print(arg2)
    print("I am returning my first argument now!")
    return(arg1)

Note the comment at the start, denoted with the # symbol. This tells python that the text that follows it is just a comment, and not something that it should execute. We don't need to use comments as much with Jupyter notebooks as we do in normal programming because we have markdown cells, but it is ALWAYS a good idea to use comments in functions whenever you are doing something complex. In particular, whenever you write a complex line of code (something that it would be hard for you to come back tomorrow, look at and know instantly what it does), you should write a comment above it explaining what it does. It is also always good to add a comment about your module, what it does, when it was created, etc.

Once we've saved that file, how do we get it into python? We use a command we've already encountered: import. The import command tells python to read a module file and learn all the definitions in it. Let's try this in our interactive session:


In [ ]:
import mymodule

In [ ]:
mymodule.myfunc(1, 2)

Here the import command told python to read the module and learn all the functions in it. Then we can call a function. The only difference between this and when we defined the function by hand in the session is that we needed to precede the function name by the name of the module it came from, followed by a period. This is to avoid conflicts where two modules happen to have a function of the same name. If this is too much typing, yu can use a short version of the module name by assigning a new name when you import it:


In [ ]:
import mymodule as mm

In [ ]:
mm.myfunc(1,2)

One advantage of modules is that they can define more than one function, and can also define variables. Edit the text in your mymodule.py file to contain the text below.

# I am a module!

# Here's a variable
about_pi = 3.14

# Here's the definition of myfunc
def myfunc(arg1, arg2):
    print("I am a function in a module! Here are my arguments:")
    print(arg1)
    print(arg2)
    print("I am returning my first argument now!")
    return(arg1)

# Here's another function
def myfunc1(arg1):
    print("I only take one argument. It is:")
    print(arg1)
    print("I also don't return anything. Bye!")

Now let's import the modified module. The import statement by default doesn't do anything if a module has already been imported, for reasons of efficiency: different parts of a program may all import the same module, and it would be inefficient to import it again if it has already been imported once. However, we can explicitly tell python to override this default behavior using the command reload. This command was built into Python 2, but in Python 3 (which we are using here) needs to be imported from a module called imp, as below, before use. Since we only need the one function, we tell python to import that function alone from the module imp.


In [ ]:
from imp import reload

In [ ]:
reload(mm)

In [ ]:
mm.about_pi

In [ ]:
mm.myfunc1(3)

The reload command brought in the new function myfunc1, and also the variable about_pi. Note that, when we called myfunc1, nothing was printed as output beyond the print statements in the function itself. This is because it didn't have a return value.

1.3 Optional and default arguments

The functions we have defined have to be called with the right number of arguments, where "right" means "however many arguments the function says it wants". Let's see what happens if we fail to do this:


In [ ]:
mm.myfunc()

We get an error message. Sometimes, however, it is convenient to have arguments that are optional. They can be set if a user wants, but they don't have to be. We define optional arguments (sometimes also called keyword arguments) by giving them names and assigning default values in the argument list. Here's an example. Add the following to your mymodule.py file:

# Yet another function
def myfunc2(arg1, arg2='cheese', arg3='sandwich'):
    print("I take one mandatory argument, and two optional ones.")
    print("The mandatory argument is:")
    print(arg1)
    print("The optional arguments are:")
    print(arg2)
    print(arg3)

In this definition, the argument arg1 is required, and is not given a default value. In contrast, the arguments arg2 and arg3 are optional, and are given defaults. If this function is called without arg2 or arg3 being set, they will be assigned to the default value indicated.

Let's reload the module again so that we import this new definition, then experiment with this new capability:


In [ ]:
reload(mm)

In [ ]:
mm.myfunc2(1)

In [ ]:
mm.myfunc2(1, arg3='burger')

In [ ]:
mm.myfunc2(1, arg2='ham')

In [ ]:
mm.myfunc2(1, 'peanut butter')

Here we see that optional arguments can be handled several ways when calling a function. First, we can just give the mandatory argument and skip the optional ones, in which case they get their default values. Second, we can call specify one or more of the optional arguments by name, and use the equal sign to specify the value we want it to have. Third, we can give more than just the mandatory number of arguments, but not specify names. In this case, the first extra argument (beyond the mandatory ones) is assumed to correspond to the the first optional argument, the second extra argument to the second optional one, etc.

In some cases we want an argument to be option and NOT to have a default value. If it is not set, we want nothing to be assigned. Fortunately, python provides a way to do this. Variables can be set to the special value None, which indicates that the variable has not been assigned to anything. We can always make the default value be None, for example

def mfunc2(arg1, arg2='cheese', arg3=None):

Exercise 2

Create a python module file hellox.py that contains your original hellox function from Exercise 1.

  1. Modify it to include an optional argument called mood, with a default value "grumpy".
  2. Change the print statement to "Hello input, my name is myname and today I'm feeling mood"
  3. Modify your docstring as necessary

<div>

2. Conditional Statements and Control Flow

2.1 The if statement and comparison operators

Now that we know how to define new functions and import them, we're in a position to do some useful programming. However, we need a few more tools. One tool we need the ability to decide what to do based on the input we get. To demonstrate this capability, let's define new module. We'll put it in a file called plottrig.py. Let's enter the following:

# This module plots user-specified trig functions

# Import all the definitions from the numpy and plotting libraries
from numpy import *
from matplotlib.pyplot import *

# Define the plotting function
def makeplot(funcname):

    # Define the x array
    x=arange(0, 2*pi, 0.01)

    # Check if the user entered the sin, cos, or tan function
    if funcname=='sin':
        plot(x, sin(x))
    elif funcname=='cos':
        plot(x, cos(x))
    elif funcname=='tan':
        plot(x, tan(x))
    else:
        print("Unrecognized function "+str(funcname))

Let's examine this module to understand what it does. First, we import the numpy and matplotlib.pyplot libraries so that we can use arrays, make plots, etc. Note that we have to import these libraries within the module EVEN IF they have already been imported in our main ipython session. Definitions and import statements are specific to the module where they are called, and functions that are defined or imported into the main session (or the main program, if we're running non-interactively) do not affect other modules, and vice-versa: something that is imported into a given module will not also be imported into the main session.

Next, we define a function that takes one argument. This argument is the name of the trig function we are going to plot.

Third, we define an array that goes from 0 to 2 pi with a spacing of 0.01. This is exactly as we did in the last class.

Finally, we decide what to do based on the user input, using an if statement. The structure of if is fairly simple. We begin a line with if and then we follow that with a test that will be either true or false. In this case, we compare the input function name to 'sin' and see if it matches. The comparison is done with the == operator -- more on that in a moment. If the input funcname matches 'sin', we go ahead and plot a sin function. The line to do this is indented another 4 spaces, to indicate that this command belongs within the block to which the if applies.

If the name doesn't match 'sin', we next compare it to 'cos'. The statement that does this is 'elif', which is short for 'else if'. The meaning of 'elif' is that we first check the if statement. If it is true, we execute the block of code within that if statement. If it is false, we then go on to the 'elif' line and do that test. If that test turns out to be true, we execute that block of code. In this case, if the input value of funcname is 'cos', then we will plot a cos function. If the elif turns out to be false, we go on to the next one, that compares the input to 'tan'. If that is true, we plot tan. If not, we encounter an 'else'. The 'else' statement is the catchall: it's what we do if all the if's and elif's turn out to be false. In this case, we just print out a message complaining that we don't recognize the input function.

For this code to work, we also have to tell Jupyter to put any plots created by python into the notebook directly. You will use this command in most of the notebooks that we use in this class


In [ ]:
%pylab inline

OK, let's test this code out:


In [ ]:
import plottrig as pt

In [ ]:
pt.makeplot('sin')

Hopefully you see something like this:

Try experimenting with the other trig functions to verify that they work. This is a good chance to practice adding cells with your Jupyter shortcut sheet as well

The thing that follows an if statement can be any statement or function that results in True or False, but it is obviously useful to be able to compare things. Python provides a number of comparison operators, but the most common ones that you will use are ==, <, >, <=, >=, and !=. Most of these have their usual mathematical meanings (for example < and >), but there are a few subtleties to be aware of.
== tests if two things are equal; we use == rather than = because we need to distinguish comparison from assignment != means "not equal"
<= and >= mean "less than or equal to" and "greater than or equal to", respectively
when used on strings instead of numbers, < is interpreted to mean "comes before alphabetically", and similarly for >, <=, and >=

Exercise 3


Add a function to your module file called hellox2. Copy your hellox function from Exercise 2 into this new function, then modify it to meet the following criteria.

  1. If input is specified as "Follette", print "Hello Professor Follette! I LOVE Astronomy!!"
  2. If input has 10 or more characters, print nothing (the python len function will be useful here)
  3. If input is specified as "Sue", set mood to "elated" and print the usual statement.
  4. If input is anything else, use the old behavior.

2.2 Iteration

The next tool we want to add to our arsenal of programming tools is iteration. Iteration means performing an operation repeatedly. We can execute a very simple example at the command line. Let's make a list of objects (more on lists in a moment) as follows:


In [ ]:
names = ['Henrietta', 'Annie', 'Jocelyn', 'Vera']

In [ ]:
for n in names:
    print('There are '+str(len(n))+' letters in '+n)

This is an example of a for loop. The way a for loop works is a follows. We start with a list of objects -- in this example a list of strings, but it could be anything -- and then we say for variable in list:, followed by a block of code. The code inside the block will be executed once for every item in the list, and when it is executed the variable will be set equal to the appropriate list item. In this example, the list names had four objects in it, each a string. Thus the print statement inside the loop was executed four times. The first time it was executed, the variable n was set equal to alice. The second time n was set equal to bob, then charlie, then dana.

One of the most common types of loop is where you want to loop over numbers: 0, 1, 2, 3, .... To handle loops of this sort, python provides a simple command to construct a list of numbers to iterate over, called range. The command range(n) produces a list of numbers from 0 to n-1. For example:


In [ ]:
for i in range(5):
    print(i)

Note that the range command has exactly the same syntax as the arange command we encountered when we constructed arrays. The difference is that arange returns an array, and range returns a list; we'll clarify the difference between those two in a few minutes.

That's really all there is to for loops. To demonstrate their utility, let's say we want to extend the capability of our trigonometric function plotting tool so that a user has the option to plot multiple trig functions with different wavelengths. To implement this, open up your plottrig.py file and edit it as follows:

# This module plots user-specified trig functions

# Import all the definitions from the numpy and plotting libraries
from numpy import *
from matplotlib.pyplot import *

# Define the plotting function
def makeplot(funcname, wavelength=[2*pi]):

    # Define the x array
    x=arange(0, 2*pi, 0.01)

    # Loop over wavelengths
    for w in wavelength:

        # Check if the user entered the sin, cos, or tan function
        if funcname=='sin':
            plot(x, sin(x*2*pi/w))
        elif funcname=='cos':
            plot(x, cos(x*2*pi/w))
        elif funcname=='tan':
            plot(x, tan(x*2*pi/w))
        else:
            print("Unrecognized function "+str(f))
            return

Notice first that we've made use of our optional argument capability. The user can enter a wavelength value, but doesn't have to, because the wavelength will default to 2 pi. If the user does enter a set of wavelengths, we've used a loop to iterate over them. Let's try running this code (remembering to reload the module first, and, if you want, to clear the plot window with clf()):


In [ ]:
reload(pt)

In [ ]:
pt.makeplot('sin',[pi,2*pi,3*pi])

Hopefully the result looks like this:

There are also other ways of iterating, which may be more convenient depending on what you're trying to do. A very common one is the while loop, which does exactly what it sounds like it should: it loops until some condition is met. For example:


In [ ]:
i=0
while i < 11:
    print(i)
    i=i+3

The syntax is while condition:, where the condition is anything that evaluates to True or False. When you make a while statement, the condition is checked, then if it is True, the code inside the while block is executed. Then the condition is checked again. If it is still True, the code inside the block is executed again, and so forth, until the condition evaluates to False. In this example, the condition is true when i is 0, 3, 6, and 9, but then i is set to 12 and the condition is False, so the loop halts. Exception handling

The final control flow tool we'll play with today is exception handling. To see why we might want this capability, let's do what a user might well do when using the wavelength capability, which is to specify a single wavelength rather than a list:


In [ ]:
pt.makeplot('sin',wavelength=2*pi)

We get an error message! Why? Because the code we have written wants to iterate over wavelength, but if the user enters a single number for the wavelength, as opposed to a list, that's not a list we can iterate over.

How can we handle this? We can catch this sort of error and handle it. The capability to detect and recover from errors is called exception handling. There are many ways we could use this capability to handle this situation. Let's implement one. Modify your plottrig.py file as follows:

# This module plots user-specified trig functions

# Import all the definitions from the numpy and plotting libraries
from numpy import *
from matplotlib.pyplot import *

# Define the plotting function
def makeplot(funcname, wavelength=[2*pi]):

    # Define the x array
    x=arange(0, 2*pi, 0.01)

    # Try to loop over wavelengths to make sure it's possible. If not,
    # turn wavelengths into a list.
    try:
        for w in wavelength:
            pass
    except TypeError:
        wavelength=[wavelength]

    # Loop over wavelengths
    for w in wavelength:

        # Check if the user entered the sin, cos, or tan function
        if funcname=='sin':
            plot(x, sin(x*2*pi/w))
        elif funcname=='cos':
            plot(x, cos(x*2*pi/w))
        elif funcname=='tan':
            plot(x, tan(x*2*pi/w))
        else:
            print("Unrecognized function "+str(f))
            return

The difference between this and the last version is the bit of code

# Try to loop over wavelengths to make sure it's possible. If not,
    # turn wavelengths into a list.
    try:
        for w in wavelength:
            pass
    except TypeError:
        wavelength=[wavelength]

Let's break this down into pieces. First, we have a line that says try:. The try statement is a declaration that we're about to try something that may generate an error, and, if it does, we want to decide what to do based on that error. The try statement is followed by the block of code that might generate the error. In this case, we're going to try to loop over wavelength. We won't actually do anything -- we just want to see if it's possible to perform the loop. Thepass statement is a statement that does nothing. It's just a placeholder, inserted because there has to be something inside the for loop. Finally, we get to the line except TypeError:. This line ends the try block, and its purpose is to specify what we do if we get an error. The syntax is that we say except, and then we list one or more types of errors we want to handle. In this case, we want to handle TypeErrors, which arise (among other things) when we try to iterate over something that is not iterable. The final line specified what we do if we generate an error of this sort. The answer is that we turn wavelength into a list of one element, so that we can iterate over it. Then execution continues as before.

Now let's verify that this works:


In [ ]:
reload(pt)

In [ ]:
pt.makeplot('sin',wavelength=2*pi)

With luck this now runs without error, and produces a sin wave with a period of 2*pi, just as it did before we added the multi-wavelength capability.

Exercise 4


Add another function to your python module - hellox3. This function should be broadly similar to hellox2, but should take a list of names and say something to each name in the list according to how many letters are in the name. You can get creative (within the bounds of politeness) about what exactly it says.


In [1]:
from IPython.core.display import HTML
def css_styling():
    styles = open("../custom.css", "r").read()
    return HTML(styles)
css_styling()


Out[1]: