01: Functions and scripts

Why use functions?

functions are a great coding tool! They allow to setup reusable bits of code that you might need over and over. Using functions, you only have the write the code once, and, if you need to change it, only change it once.

We can create a function that writes the Fibonacci series to an arbitrary boundary:

[1]:
def fib(n):
    """Print a Fibonacci series up to n."""
    a, b = 0, 1
    while a < n:
        print(a)
        a, b = b, a + b
[2]:
a, b = 0, 1
[3]:
help(fib)
Help on function fib in module __main__:

fib(n)
    Print a Fibonacci series up to n.

Now call the function we just defined:

[4]:
fib(2000)
0
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
1597

The keyword def introduces a function definition. It must be followed by the function name and the parenthesized list of formal parameters or arguments. The statements that form the body of the function start at the next line, and must be indented.

The first statement of the function body can optionally be a string literal; this string literal is the function’s documentation string, or docstring. (More about docstrings can be found in the section Documentation Strings.) There are tools which use docstrings to automatically produce online or printed documentation, or to let the user interactively browse through code; it’s good practice to include docstrings in code that you write, so make a habit of it.

[ ]:

The execution of a function introduces a new symbol table used for the local variables of the function. More precisely, all variable assignments in a function store the value in the local symbol table; whereas variable references first look in the local symbol table, then in the local symbol tables of enclosing functions, then in the global symbol table, and finally in the table of built-in names. Thus, global variables cannot be directly assigned a value within a function (unless named in a global statement), although they may be referenced. In plain english, that means the varaibles that are formed and used within a function can only be used within the function, unless they are return-ed - they are sent back.

The actual parameters (arguments) to a function call are introduced in the local symbol table of the called function when it is called; thus, arguments are passed using call by value (where the value is always an object reference, not the value of the object). [1] When a function calls another function, a new local symbol table is created for that call.

A function definition introduces the function name in the current symbol table. The value of the function name has a type that is recognized by the interpreter as a user-defined function. This value can be assigned to another name which can then also be used as a function. This serves as a general renaming mechanism:

[5]:
fib
[5]:
<function __main__.fib(n)>
[6]:
?fib
Signature: fib(n)
Docstring: Print a Fibonacci series up to n.
File:      /var/folders/4x/bmhyjcdn3mgfdvkk_jgz6bsrlnfk3t/T/ipykernel_46966/2487284216.py
Type:      function
[7]:
f = fib
[8]:
f(200)
0
1
1
2
3
5
8
13
21
34
55
89
144

Coming from other languages, you might object that fib is not a function but a procedure since it doesn’t return a value. In fact, even functions without a return statement do return a value, albeit a rather boring one. This value is called None (it’s a built-in name equivalent to null or not defined). Writing the value None is normally suppressed by the interpreter if it would be the only value written. You can see it if you really want to using print:

[9]:
fib(0)
print(fib(0))
None

It is simple to write a function that returns a list of the numbers of the Fibonacci series, instead of printing it:

[10]:
def fib2(n):  # return Fibonacci series up to n
    """Return a list containing the Fibonacci series up to n."""
    result = []
    a, b = 0, 1
    while a < n:
        result.append(a)  # see below
        a, b = b, a + b
    return result
[11]:
fib2(100)
[11]:
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
[12]:
f100 = fib2(100)  # call it
f100  # write the result
[12]:
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

This example, as usual, demonstrates some new Python features:

The return statement returns with a value from a function. return without an expression argument returns None. Falling off the end of a function also returns None.

An object oriented preview aside: terminology alert!!!

The statement result.append(a) calls a method of the list type object instance result. A method is simply a function that ‘belongs’ to an object and is named obj.methodname, where obj is some object (this may be an expression), and methodname is the name of a method that is defined by the object’s type. Different types define different methods. Methods of different types may have the same name without causing ambiguity. (It is possible to define your own object types and methods, using classes, see Classes) The method append() shown in the example is defined for list objects; it adds a new element at the end of the list. In this example it is equivalent to result = result + [a], but more efficient.

back to functions

It is also possible to define functions with a variable number of arguments. In python, there are three forms function arguments which can be combined. Each of these forms get used a lot.

The most user-friendly form of a function argument is to specify an argument which has a default value. This creates a function that can be called with fewer arguments, but that also allows greater flexility in controlling the behaviro within the function itself.

For example:

[13]:
import math


def thiem(T, r, Q=1000.0, R=1.0e10, h0=0.0):
    """
    A very simple example function
    with a mixture of argument types.
    Solves the Thiem equation:

    h = (Q/2piT)*(ln(R/r)) + h0

    Parameters
    ----------

    T: transmissivity
    r: distance from pumping to observation
    Q: pumping rate
    R: distance to "zero" influence
    h0: initial head

    Returns
    -------
    h: head
    """
    first_term = Q / (2.0 * 3.14159 * T)
    second_term = math.log(R / r)
    return (first_term * second_term) + h0
[14]:
help(thiem)
Help on function thiem in module __main__:

thiem(T, r, Q=1000.0, R=10000000000.0, h0=0.0)
    A very simple example function
    with a mixture of argument types.
    Solves the Thiem equation:

    h = (Q/2piT)*(ln(R/r)) + h0

    Parameters
    ----------

    T: transmissivity
    r: distance from pumping to observation
    Q: pumping rate
    R: distance to "zero" influence
    h0: initial head

    Returns
    -------
    h: head

This function requires both T and r. All the rest use the defaults if not explicitly passed in. The default values are evaluated at the point of function definition in the defining scope.

Giving only mandatory arguments: T=100.0, r=300.0

[15]:
thiem(100.0, 300.0)
[15]:
27.568951478843925

Giving one of the optional arguments using the implied position of Q: T=100.0, r=300.0, Q=500.0

[16]:
thiem(100.0, 300.0, 10)
[16]:
0.2756895147884393

Giving one of the optional arguments using the explicit argument name: T=100.0, r=300.0, Q=500.0

[17]:
thiem(100.0, 300.0, Q=500.0)
[17]:
13.784475739421962

Your turn

1.) What is the thiem solution for T=1000.0, r=20.0?

[ ]:

2.) What is the Thiem solution for T=1000.0, r=20.0, h0=40.0?

[ ]:

3.) What is the Thiem solution for T=1000.0, r=20.0, Q=2000.0, h0=40.0

[ ]:

4.) What is the Thiem solution for T=1000.0, r=20.0, Q=2000.0, h0=40.0 if there is a lake 2000.0 units away?

[ ]:

5.) using a loop to accumulate Thiem results for T=1000.0, r=3000.0 for Q values ranging from 100.0 to 2000.0 by 100.0

[ ]:

6.) redefine the Thiem function to take a list as the Q argument and return a list of h results. Call the new function thiem_list(). Then call thiem_list() using the same Q as in 5.) above

[18]:
def thiem_list(T, r, Q=[1000.0], R=1.0e10, h0=0.0):
    """
    A very simple example function
    with a mixture of argument types.
    Solves the Thiem equation:

    h = (Q/2piT)*(ln(R/r)) + h0

    Parameters
    ----------

    T: transmissivity
    r: distance from pumping to observation
    Q: pumping rate
    R: distance to "zero" influence
    h0: initial head

    Returns
    -------
    h: list of head s
    """
    h = []
    for q in Q:
        first_term = q / (2.0 * 3.14159 * T)
        second_term = math.log(R / r)
        result = (first_term * second_term) + h0
        h.append(result)
    return h
[19]:
thiem_list(1000, 100, Q=[10, 20, 30])
[19]:
[0.029317448718566664, 0.05863489743713333, 0.0879523461557]

More on functions: lambda functions

lambda functions are a special type of function known as an “in-line” function. They are present in virtually all modern programming langauges (not Fortran, that’s not modern) and are usually high-optimized. They allow you to quickly define a simple-ish function that can only accept a single, required argument. The only reason to introduce them is because they appear frequently when using a python library named pandas that we will cover later.

The variable square is actually a function that squares (or attempts to) the single required argument that it is given

[20]:
square = lambda x: x * x
square
[20]:
<function __main__.<lambda>(x)>
[21]:
square(100)
[21]:
10000
[22]:
square("not gonna work")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[22], line 1
----> 1 square("not gonna work")

Cell In[20], line 1, in <lambda>(x)
----> 1 square = lambda x: x * x
      2 square

TypeError: can't multiply sequence by non-int of type 'str'

Your turn

Define a lambda function that raises a number to the power of 3 then divides by 3

[ ]:
power3 = lambda x: x**3 / 3
[ ]:
power3(3)

In a loop, pass you function all integers from 1 to 100

[ ]:

Rewrite the Thiem function from above as a lambda function that only accepts an argument for one variable (you chose) then “sweep” over that varaible with a range of values in loop

[ ]:

[ ]:

Scripts

If you quit from the Python interpreter and enter it again, the definitions you have made (functions and variables - e.g. ALL YOUR HARD WORK) are lost. Therefore, if you want to write a somewhat longer program, you are better off using a text editor to prepare the input for the interpreter and running it with that file as input instead. This is known as creating a script. As your program gets longer, you may want to split it into several files for easier maintenance. You may also want to use a handy function that you’ve written in several programs without copying its definition into each program.

To support this, Python has a way to put definitions in a file and use them in a script or in an interactive instance of the interpreter. Such a file is called a module; definitions from a module can be imported into other modules or into the main module (the collection of variables that you have access to in a script executed at the top level and in calculator mode).

A module is a file containing Python definitions and statements. The file name is the module name with the suffix .py appended. Within a module, the module’s name (as a string) is available as the value of the global variable name.

Your turn

Using a text editor, put the thiem() and thiem_list() in to a file named thiem_functions.py. Make sure you put the file in the same directory as the notebooks we are using. Test that you have everything working by typing import thiem_functions

[ ]:
import thiem_functions
[ ]:
from thiem_functions import thiem
[ ]:
from thiem_functions import thiem as thm
[ ]:
thiem(100, 100)
[ ]:
thm(100, 100)

call both functions in the thiem_function module using the correct signature (a.k.a arguments)

[ ]:
thiem_functions.thiem(100, 100)

A module can contain executable statements as well as function definitions. These statements are intended to initialize the module. They are executed only the first time the module name is encountered in an import statement. [1] (They are also run if the file is executed as a script.)

Each module has its own private symbol table, which is used as the global symbol table by all functions defined in the module. Thus, the author of a module can use global variables in the module without worrying about accidental clashes with a user’s global variables. On the other hand, if you know what you are doing you can touch a module’s global variables with the same notation used to refer to its functions, modname.itemname.

Modules can import other modules. It is customary but not required to place all import statements at the beginning of a module (or script, for that matter). The imported module names are placed in the importing module’s global symbol table.

There is a variant of the import statement that imports names from a module directly into the importing module’s symbol table. For example:

[ ]:
from thiem_functions import thiem, thiem_list
[ ]:
thiem(500.0, 100.0)

Your turn

Using a text editor, create a new python script that imports the thiem_functions module and uses it to calculate some results. Then run your new script from the command line

[ ]: