1.5. Functions and Scope#

(not the mouthwash)

Reference: Chapter 3 of Computational Nuclear Engineering and Radiological Science Using Python, R. McClarren (2018)

1.5.1. Learning Objectives#

After studying this notebook, completing the activities, and asking questions in class, you should be able to:

  • Know good practices for creating functions and adding a reasonable docstring.

  • Be able to call functions with correct inputs and access the outputs.

  • Understand local versus global scope and basic scoping rules with functions.

1.5.2. Functions#

1.5.3. Motivating Example#

Why use functions? We want to write, debug, and test code once and then reuse as much as possible.

Soon, we’ll formulate mass balances as linear systems and solve them using Python. But for now, let’s just consider a problem you would expect to see in math class:

We want to solve the linear system,

\[\begin{split} \mathrm{Eqn.~1}:\quad 4.5 x + 3 y = 10.5\\\mathrm{Eqn.~2}:\quad 1.5 x + 3 y = 7.5.\end{split}\]

One way to do this is using Python as calculator. In the comments below, we walk through the steps.

"""python code to solve 
4.5 x + 3 y = 10.5
1.5 x + 3 y = 7.5
by solving the second equation for y first,
and then solving for x"""
#step 1 solve for y, multiply equation 2 by -3, 
## and add to first equation
LHS_coefficient = -3*3 + 3 #the coefficient for y
RHS = -3*7.5 + 10.5 #the right-hand side

print('LHS_coefficient:',LHS_coefficient)
print('RHS:',RHS)
LHS_coefficient: -6
RHS: -12.0

Mathematically, we started by multiplying equation 2,

\[1.5 x + 3 y = 7.5\]

by -3,

\[(-3) \times 1.5 x + (-3) \times 3 y = (-3) \times 7.5\]

and then added this scaled equation 2 to equation 1, giving:

\[(4.5 - 3 \times 1.5) x + (3 - 3 \times 3) y = 10.5 - 3 \times 7.5\]

Notice that our choice of scaling equation 2 by -3 means that the coefficient for x becomes zero after addition. The coefficient for \(y\) is LHS_coefficient is our code. RHS is the right hand side of the new equation.

#now divide right-hand side by left-hand side coefficient
y = RHS / LHS_coefficient
#plug y into first equation
x = (10.5 - 3*y)/4.5 
#print the solution, note \n produces a linebreak
print("The solution to:\n4.5 x + 3 y = 10.5\n1.5 x + 3 y = 7.5\n is x =",
      x,"y=",y)
The solution to:
4.5 x + 3 y = 10.5
1.5 x + 3 y = 7.5
 is x = 1.0 y= 2.0

How to extend this code to another linear system?

Let’s define a function that will solve the system for (almost) any coefficients and right-hand side.

I’ll define such a function to solve

\[a_1 x + b_1 y = c_1\]
\[a_2 x + b_2 y = c_2.\]

Home Activity

Write pseudocode to generalize the steps from the motivating example to solve the linear system with coefficients a1, a2, b1, b2, c1, and c2.

Class Activity

Discuss your pseudocode with a partner. Give one compliment and one suggestion to partner’s pseudocode. We’ll then regroup and write it together as a class.

Below is a function that solves (most) 2x2 linear systems. Take a few minutes to study the code below. Specifically:

  • Notice the function has seven inputs. The first six are the coefficients. The seventh, LOUD, is followed by =False. This sets the default value of LOUD to false.

  • The input LOUD toggles on/off a print statement.

  • This function has a long comment string at the top. It includes a brief description, then a list of inputs (arguments) and finally a list of outputs (returns). All of the functions you write in this class must be commented in the same style.

def two_by_two_solver(a1,b1,c1,a2,b2,c2, LOUD=False):
    """Calculate the solution of the system 
    a1 x + b1 y = c1, 
    a2 x + b2 y = c2

    Args:
        a1: x coefficient in first equation (cannot be zero)
        b1: y coefficient in first equation
        c1: right-hand side in first equation
        a2: x coefficient in second equation 
        b2: y coefficient in second equation 
        c2: right-hand side in second equation
        LOUD: boolean that decides whether to print out the answer
        
    Returns:
        list containing the solution in the format [x,y]
    """
    #step one, eliminate x from the second equation by 
    #multiplying first equation by -a2/a1
    #and then adding it to second equation
    new_b2 = b2 - a2/a1*b1
    new_c2 = c2 - a2/a1*c1
    #solve the new equation 2
    y = new_c2/new_b2
    #plug y into original equation 1
    x = (c1-b1*y)/a1
    
    if (LOUD):
        print("The solution to:\n",a1,"x +",b1,"y =",c1,
              "\n",a2,"x +",b2,"y =",c2,"\n is x =",x,"y=",y)
    return [x,y]

We can call this function for the problem above by typing

two_by_two_solver(4.5,3,10.5,1.5,3,7.5,True)
The solution to:
 4.5 x + 3 y = 10.5 
 1.5 x + 3 y = 7.5 
 is x = 1.0 y= 2.0
[1.0, 2.0]

We can also solve other systems, including simple ones

two_by_two_solver(1,0,3,0,1,2,True)
The solution to:
 1 x + 0 y = 3 
 0 x + 1 y = 2 
 is x = 3.0 y= 2.0
[3.0, 2.0]

We can’t solve systems where \(a_1\) is zero because our function divides by \(a_1\):

two_by_two_solver(0,1,2,1,0,3,True)
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
Input In [6], in <cell line: 1>()
----> 1 two_by_two_solver(0,1,2,1,0,3,True)

Input In [3], in two_by_two_solver(a1, b1, c1, a2, b2, c2, LOUD)
      2 """Calculate the solution of the system 
      3 a1 x + b1 y = c1, 
      4 a2 x + b2 y = c2
   (...)
     16     list containing the solution in the format [x,y]
     17 """
     18 #step one, eliminate x from the second equation by 
     19 #multiplying first equation by -a2/a1
     20 #and then adding it to second equation
---> 21 new_b2 = b2 - a2/a1*b1
     22 new_c2 = c2 - a2/a1*c1
     23 #solve the new equation 2

ZeroDivisionError: division by zero

1.5.4. Calling functions#

Approach above: give inputs in order

We called the function two_by_two_solver by listing out the arguments in the order that it expects them a1, b1, c1, a2, b2, c2, LOUD.

Another option: use keywords

Python allows you to call them in any order, as long as you are explicit in what goes where.

two_by_two_solver(a1 = 4.5, b1 = 3, 
                  a2 = 1.5, b2 = 3, 
                  c1 = 10.5, c2 = 7.5, LOUD = True)
The solution to:
 4.5 x + 3 y = 10.5 
 1.5 x + 3 y = 7.5 
 is x = 1.0 y= 2.0
[1.0, 2.0]

It is often a good idea to call a function explicitly (with keywords). That way if you mess up the order of the arguments, it does not matter.

Notice that in the function definition, the argument LOUD has =False after it. This indicates that if the function is called without a value for LOUD, it assumes the caller does not what the function to “be loud”.

In other words, False is the default for argument LOUD.

two_by_two_solver(a1 = 4.5, b1 = 3, a2 = 1.5, 
                  b2 = 3, c1 = 10.5, c2 = 7.5)
[1.0, 2.0]

Notice that it didn’t print out it’s spiel about the system.

two_by_two_solver(1,1,2,a2 = 1, c2 = 0, b2 = 3)
[3.0, -1.0]

1.5.5. Return Values#

At the end of the function we have a return statement. This tells python what the function is returning to the caller. In this case we return a list that has the solution for \(x\) and \(y\). We can store this in a new variable, or do whatever we like with it.

answer = two_by_two_solver(a1 = 4.5, b1 = 3, a2 = 1.5, 
                           b2 = 3, c1 = 10.5, c2 = 7.5)
x = answer[0] #store in the variable x the first value in the list answer
y = answer[1] #store in the variable y the second value in the list answer
print("The list",answer,"contains",x,"and",y)
The list [1.0, 2.0] contains 1.0 and 2.0

Home Activity

Solve the linear system given below and store the answers in my_x and my_y.

\[ 2 x -1 y = 3\]
\[-4 x + 3 y = 0.\]
# Add your solution here
# Removed autograder test. You may delete this cell.

We can do even fancier things, if we are so bold

#just get x
x = two_by_two_solver(a1 = 4.5, b1 = 3, a2 = 1.5, 
                      b2 = 3, c1 = 10.5, c2 = 7.5)[0]
print("x =",x)

#assign variables to the output on the fly
x,y = two_by_two_solver(a1 = 4.5, b1 = 3, a2 = 1.5, 
                        b2 = 3, c1 = 10.5, c2 = 7.5)
print("x =",x,"y =",y)
x = 1.0
x = 1.0 y = 2.0

These examples are more advanced and they are designed to show you some of the neat tricks you can do in python.

1.5.6. Docstrings and Help#

Our 2x2 solver code had a long, and pretty detailed comment at the beginning of it. This is called a docstring and it is printed by a user by typing

help(two_by_two_solver)
Help on function two_by_two_solver in module __main__:

two_by_two_solver(a1, b1, c1, a2, b2, c2, LOUD=False)
    Calculate the solution of the system 
    a1 x + b1 y = c1, 
    a2 x + b2 y = c2
    
    Args:
        a1: x coefficient in first equation (cannot be zero)
        b1: y coefficient in first equation
        c1: right-hand side in first equation
        a2: x coefficient in second equation 
        b2: y coefficient in second equation 
        c2: right-hand side in second equation
        LOUD: boolean that decides whether to print out the answer
        
    Returns:
        list containing the solution in the format [x,y]

The point of this long comment is to communicate to other people:

  1. The main idea behind the function.

  2. What the function expects from you: Inputs.

  3. What the function gives you: Outputs.

In this example we can see that we need to provide at least 6 numbers, and possibly an optional boolean (true/false).

It is good programming practice to include good docstrings. It will be mandatory for any code you turn in this class.

Let’s look at the docstring for some members of the math module and the random module.

import math
help(math.fabs)
Help on built-in function fabs in module math:

fabs(x, /)
    Return the absolute value of the float x.
import random
help(random.uniform)
Help on method uniform in module random:

uniform(a, b) method of random.Random instance
    Get a random number in the range [a, b) or [a, b] depending on rounding.

We don’t have the source code for these functions in front of us, but if we want to know how to call them (and we didn’t want to Google® them), the docstrings tell us what to do.

You may wonder why those docstrings are a bit different that the one I used. The format for mine is derived from the Google coding standards for python docstrings.

# another example
def nothing():
    #this function does nothing
    #docstring?
    return 0
help(nothing)
Help on function nothing in module __main__:

nothing()
    # another example

1.5.7. Scope#

When we call a function, it carves out in the computer’s memory its own space. Variables that live in the special space, known as local scope are completely different than those in other parts of the program. Here’s a simple, but illustrative example:

Home Activity

Before you run the code below, predict the output. This is good practice for exam questions, where you’ll be asked to predict the output of Python code without access to a computer.

Home Activity - Your Predictions

x =

new_x =

y =

new_y =

def scope_demonstration(input_variable):
    ''' A simple demonstration of scoping rules
    
    Args:
        input_variable: a string or number
        
    Returns:
        x: --REDACTED TO NOT GIVE AWAY THE ACTIVITY ANSWER --
    '''
    x = input_variable*3
    return x

#now call the function after defining some variables
x = "oui "
y = "no "

new_x = scope_demonstration(x)
new_y = scope_demonstration(y)
print("x =",x,"\nnew_x =",new_x)
print("y =",y,"\nnew_y =",new_y)

When I call scope_demonstration it creates its own memory space and any variable I create in there is different than in the rest of the program, even if the variables have the same name.

There are many subleties in scoping rules, but this example outlines just about everything that you’ll need to know as a neophyte programmer.

A variable declared at the top level Python file, i.e., outside a function, is in the global scope for that file. This is easiest to see in an example:

my_var = 1

def print_my_var():
    ''' Another scope demonstration
    
    Args:
        Nothing
        
    Returns:
        Nothing
        
    Other:
        prints `my_var`
    
    '''
    print(my_var)
    return None

print_my_var()
1

We were able to access my_var inside the function even though it was not an input to a function.

The next natural guess is can we modify my_var with a function? Let’s see.

def change_my_var():
    ''' A third scope demonstration
    
    Args:
        Nothing
        
    Returns:
        Nothing
        
    Other:
        Attempts to change `my_var', prints to screen
    '''
    
    my_var = 3*my_var + 2
    print("my_var = ",my_var," inside the function")
    
    return None

change_my_var()
print("my_var =",my_var," outside the function")
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
Input In [20], in <cell line: 19>()
     15     print("my_var = ",my_var," inside the function")
     17     return None
---> 19 change_my_var()
     20 print("my_var =",my_var," outside the function")

Input In [20], in change_my_var()
      1 def change_my_var():
      2     ''' A third scope demonstration
      3     
      4     Args:
   (...)
     11         Attempts to change `my_var', prints to screen
     12     '''
---> 14     my_var = 3*my_var + 2
     15     print("my_var = ",my_var," inside the function")
     17     return None

UnboundLocalError: local variable 'my_var' referenced before assignment

We got an error message! In Python, we can only access my_var inside functions. We cannot change it inside functions. If we wanted to change my_var, we would need to do the following:

def change_my_var2():
    ''' A third scope demonstration
    
    Args:
        Nothing
        
    Returns:
        New value of my_var
        
    '''
    
    return 3*my_var + 2

my_var = change_my_var2()
print("Now my_var =",my_var)
Now my_var = 5

Class Activity

Take 30 seconds to predict the output of the code below without running it. Below are multiple choice answers.

def my_func1(x,y):
    ''' A simple function to demonstrate the nuances of scope

    Arguments:
        x: scalar real number
        y: scalar real number
        
    Returns:
        z: scalar real number

    '''
    z = x + y
    x = 3
    y = z - x + 1
    return z

# Run the function
x = 1
y = 1
z = my_func1(x,y)
# Print values of x, y, and z to screen
print("x =",x,"  y =",y,"  z =",z)

Class Activity multiple choice answers:

  1. x=1, y=1, z=2

  2. x=1, y=1, z=3

  3. x=3, y=0, z=2

  4. x=3, y=0, x=3

  5. None of these

### Create your answer here.