8  Functions

Jupyter Notebook

We have already been using functions here and there but in this chapter we will introduce them formally and get into the details. A function encapsulates a block of code designed to perform a specific task or set of tasks. To perform the task correctly, most functions require that you provide some information (called arguments) when you call them. To call a function you type the name of the function followed by the needed arguments enclosed in parenthesis (()).

functionName(argument1, argument2, argument3...)

The number and type of arguments allowed is different for every function. As a first example, let’s consider the print function, which is the simplest (and most familiar) function that we have used so far.

print("print is a function")  #Single string argument
print("print", "is", "a", "function") # Multiple string arguments
print(5) # Single integer argument
print(5,4) # Multiple integer argument
print([5,4,2,5])  # Single list argument
print() # No arguments
print is a function
print is a function
5
5 4
[5, 4, 2, 5]

The print function is pretty flexible in what it allows for arguments; the arguments can be any type of data (strings, ints, floats, booleans, and even lists) and the number of arguments can be as large as you want. Most functions are a little more strict on what they allow their arguments to be. For example, the factorial function only allows one argument and that argument must be an integer.1 If you attempt to call the factorial function with a float argument or with more than one argument, the result will be an error.

1 The factorial of a float can be calculated, but the math library is not equipped to handle it.

from math import factorial
a = factorial(5.54)
b = factorial(5,3)

To Do:

  1. Run the code above and take note of the error message that results.
  2. Comment out the definition of a so you can also see the error message for the definition of b.

Python functions generally fall into three groups: functions that come standard with Python (called native functions), functions that you can import into Python, and functions that you write yourself.

8.1 Native Functions

There are a few functions that are always ready to go whenever you run Python. They are included with the programming language. We call these functions native functions. You have already been using some of them, like these

myList = [5,6,2,1]
a = len(myList)  # 'len' function is native.

b = float(5) # 'float' function is native.

c = str(67.3)  # 'str' function is native.

The len, float and str functions are all native and they all take a single argument. Other native function have been mentioned in previous chapters and others will be mentioned in the future.

8.2 Imported Functions

Many times, you will need to go beyond what Python can do by itself2. However, that doesn’t mean you have to create everything you need to do from scratch. Most likely, the function that you need has already been coded. Somebody else created the function and made it available to anyone who wants it. Groups of functions that perform similar tasks are typically bundled together into libraries ready to be imported so that the functions that they contain can be used.

2 For example, Python does not include \(\sin()\) or \(\cos()\) as Native functions.

In order to use use a function correctly, you’ll need to know what information(arguments) the function expects you to give it and what information the function intends to return to you as a result. This information can be found in the library’s documentation. Most libraries have great documentation with lists of the included functions, what the functions do, the expected arguments, and examples on how to use the most common ones. You can usually find the library documentation by searching the internet for the library’s name plus “Python documentation”.

Providing a complete list of all available libraries and function is not really the purpose of this book. Instead, we’ll illustrate how to import functions and use them. As you use Python more and more you should get in the habit of searching out the appropriate library to accomplish the task at hand. When faced with a task to accomplish, your first thought should be, “ I’ll bet somebody has already done that. I’m going to try to find that library.”

Functions are imported using the import statement. You’ve already seen how to perform very simple mathematical calculations (\(5/6\),\(84\), etc..), but for more complex mathematical calculations like \(\sin( {\pi \over 2} )\) or \(e^{2.5}\) , you’ll need to import these functions from a library.

import math

a = math.sqrt(5.2)
b = math.sin(math.pi)
c = math.e**2.5

The math. before each function is equivalent to telling Python “Use the sqrt() function that you will find in the math book I told you to grab.” If you just type

sqrt(5.2)

Python won’t know where to find the sqrt function and an error message will result. Sometimes the name of the module can be long and typing module. every time you want to use one of it’s functions can be cumbersome. One way around this is to rename the module to a shorter name using the as statement.

import math as mt

a = mt.sqrt(5.2)
b = mt.sin(mt.pi)
c = mt.e**2.5

Instead of importing an entire module, you can import only a selection of functions from that module using the from statement. This can make your code even more succinct by eliminating the module. prefix altogether. The trade-off is that it won’t be as clear which function belongs to which module.

from math  import sqrt, sin, pi, e

a = sqrt(5.2)
b = sin(pi)
c = e**2.5

All of the functions belonging to a module can be imported at once using *.

from math import *

a = sqrt(5.2)
b = sin(pi)
c = e**2.5

8.3 User-defined Functions

After having programmed for a while, you will notice that certain tasks get repeated frequently. For example, maybe in your research project you need to calculate the force exerted on an atom due to many other nearby atoms. You could copy and paste your force-calculation code every time it was needed, but that would likely result in lots of extra code and become very cumbersome to work with. You can avoid this by creating your own function to calculate the force between any two atoms. Then, every time you need another force calculation, you simple call the function again. You only write the force-calculation part of the code once and then you execute it as many times as you need to.

To create your own function, you first need to name the function. The name should be descriptive of what it does and makes sense to you and anyone else who might use it. The first line of a function definition starts with the def statement (short for definition) followed by the name of the function with whatever information, called arguments, that needs to be fed into the function enclosed in parenthesis. The last character in this line must be a colon. Everything inside the function is indented four spaces and placed directly below the first line.

def functionName(arg1,arg2,arg3):
    # Body of Function
    # Body of Function
    # Body of Function

As an example, let’s construct a function that calculates the distance between two atoms. The function will need to know the location of each atom, which means that there should be two arguments: the xyz coordinates of both atoms passed as a pair of lists or tuples.

import math

def distance(coords1,coords2):

    dx = coords1[0] - coords2[0]
    dy = coords1[1] - coords2[1]
    dz = coords1[2] - coords2[2]
    d = math.sqrt(dx**2 + dy**2 + dz**2)
    print(f"The distance is {d:5.4f}.")

distance([1,2,3],[4,5,6])

distance([4.2,9.6,4.8],[2.9,2.6,3.4])
The distance is 5.1962.
The distance is 7.2560.

This function works just fine with integers or floats for the coordinates.

8.3.1 The return statement

The distance function prints out the value for the distance, but what if we want to use this distance in a subsequent calculation? Maybe we want to calculate the average distance between several pairs of atoms. We can instruct the function to return the final distance using the return statement. If the arguments to the function are the inputs, the return statement specifies what the output is. Let’s modify the function above to include a return statement.

import math
def distance(coords1,coords2):
    dx = coords1[0] - coords2[0]
    dy = coords1[1] - coords2[1]
    dz = coords1[2] - coords2[2]
    d = math.sqrt(dx**2 + dy**2 + dz**2)
    return d

distOne = distance([1,2,3],[4,5,6])

distTwo = distance([4.2,9.6,4.8],[2.9,2.6,3.4])

averageDistance = (distOne + distTwo)/2

print(f"The average distance is {averageDistance:5.4f}.")
The average distance is 6.2261.

8.3.2 Local vs Global Variable Scope

Variables created inside of a function have local scope. This means that they are not accessible outside of the function. In our distance function the variables dx,dy,dz, and d were all local variables that are used inside the function but have no value outside of it. This is convenient because we don’t have to worry about overwriting a variable or using it twice. If someone sends you a function and you want to use it in your code, you don’t have to worry about what variable he/she chose to use inside his function; they won’t affect your code at all.

def distance(coords1,coords2):
    dx = coords1[0] - coords2[0]
    dy = coords1[1] - coords2[1]
    dz = coords1[2] - coords2[2]
    d = math.sqrt(dx**2 + dy**2 + dz**2)
    return d

distOne = distance([1,2,3],[4,5,6])

print(dx)  # There is no value associated with this variable outside of the function.

The down side to all of this is that you don’t have access to function variables unless you pass them out of the function using the return statement.

Any variables defined outside of a function is called a global variable, which means that Python remembers these assignments from anywhere in your code including inside of functions. Using global variables with the intention to use them inside of functions is usually considered bad form and confusing and is discouraged. One notable exception to this rule are physical constants like \(g = 9.8\) m/s\(^2\) (acceleration due to gravity on Earth) or \(k_B = 1.38 \times 10^{-23}\) (Boltzmann’s constant which is used heavily in thermodynamics) because these values will never change and may be used repeatedly. Generally speaking every variable that is used in a function ought to be either i) passed in as an argument or ii) defined inside of the function. Below is an example of an appropriate use of a global variable.

def myFunction(a,b):
    c=a+g # <--- Notice the reference to 'g' here 
    d = 3.0 * c
    f = 5.0 * d**4
    return f

#The variable below are global variables. 
r = 10
t = 15
g = 9.8         #<--- g defined to be a global variable
result = myFunction(r,t)

8.3.3 Positional vs. Keyword Arguments

The function arguments we have been using so far are called positional arguments because they are required to be in a specific position inside the parenthesis. To see what I mean consider the example below.

def example(a,b):
    return a**b


resultOne = example(5,2)
resultTwo = example(2,5)

print(resultOne, resultTwo)
25 32

In the first call to example the local variable a gets assigned to be 5 and the local variable b gets assigned 2. In the second call the order of the arguments is switched and the subsequent assignments to a and b switch with it. This produces a different result from the function. Positional arguments are very common but the user must know what information goes where when calling the function.3

3 This is another reason why you want to choose meaningful variable names for your arguments.

The other type of argument is the keyword argument. These arguments are attached to a keyword inside of the parenthesis. The advantage of a keyword argument is that the user does not need to be concerned about the location of the argument as long as it has the proper label.

def example(a=1,b=2):
    return a**b


resultOne = example(a=5,b=2)
resultTwo = example(b=2,a=5)

print(resultOne, resultTwo)
25 25

Another advantage to using keyword arguments is that a default value can be coded into the function. This means that we can call the function with some arguments missing and default values will be used for them. In the example above, the default value of b is 2 and if the function is called without specifying a value for that argument, the function will proceed as usual using the default value for b.

def example(a=1,b=2):
    return a**b


result = example(a=5)

print(result)
25

8.3.4 Splatting

Functions can potentially require dozens of arguments to be passed in which can make calling the function long and difficult to look at. One way to shorten the code is to put all of the arguments in a list or tuple.

def func(a,b,c,d):

    return a**b + c/d
args  = (5,4,6,3)

result = func(*args)

print(result)
627.0

8.3.5 Lambda Functions

When the function you want to construct is very simple (one line), there is a shortcut code for making it called a lambda function. The benefit is that they occupy less lines of code than the standard def functions. A lambda function is defined as shown below with the variable immediately after the lambda statement as the independent variable in the function.

f = lambda x: x**2

print(f(5))
25

As a simple application of lambda functions let’s consider a function from the scipy.integrate library called quad which will perform a numerical integration of a function. The quad function takes three arguments: the function to be integrated, and the upper and lower bound on the integral. We can use a lambda function for the first argument rather than using several lines of code to build one in the traditional fashion.

from scipy.integrate import quad
from math import sin, pi

quad(lambda x: sin(pi * x)**2,0,0.4 )
(0.15322553581056808, 1.7011451781741914e-15)