This page was generated from `source/notebooks/L3/3_functions.ipynb`_.
Binder badge

Functions

Python allows to define functions. Functions collect code that you would use repeatedly in your program.

[1]:
# include the required modules

import numpy as np
import matplotlib.pyplot as plt

%config InlineBackend.figure_format = 'retina'

plt.rcParams.update({'font.size': 12,
                     'axes.titlesize': 18,
                     'axes.labelsize': 16,
                     'axes.labelpad': 14,
                     'lines.linewidth': 1,
                     'lines.markersize': 10,
                     'xtick.labelsize' : 16,
                     'ytick.labelsize' : 16,
                     'xtick.top' : True,
                     'xtick.direction' : 'in',
                     'ytick.right' : True,
                     'ytick.direction' : 'in',})

Function definition

Every function definition begins with the word def followed by the name you want to give to the function, sinc in this case, then a list of arguments enclosed in parentheses, and finally terminated with a colon.

def function_name(parameters):
    """
    This is the docstring documenting the function.
    This is printed if you type help(function_name)
    """
    ## indented statements
    print("Hello, " + name + ". Good morning!")

The following example calculates \(sinc(x) = \sin(x)/x\) and sets it equal to y. The return statement of the last line tells Python to return the value of y to the user.

[180]:
def sinc(x):
    """
    This function calculates
    sin(x)/x
    """
    y = np.sin(x)/x
    return(y)

The code for sinc(x) works just fine when the argument is a single number or a variable that represents a single number. However, if the argument is a NumPy array, we run into an error.

[147]:
 x = np.arange(0, 5., 0.5)
[148]:
sinc(x)
/Users/fci/opt/anaconda3/lib/python3.7/site-packages/ipykernel_launcher.py:6: RuntimeWarning: invalid value encountered in true_divide

[148]:
array([        nan,  0.95885108,  0.84147098,  0.66499666,  0.45464871,
        0.23938886,  0.04704   , -0.10022378, -0.18920062, -0.21722892])

We may intercept the error by checking if the supplied array contains a 0. This can be done by looping through all elements of the array and using our flow control if, else statements.

[149]:
def sinc(x):
    """
    This function calculates
    sin(x)/x
    """
    y = []
    for xx in x:
        if xx==0.0:
            y+=[1.0]
        else: # adds result of sin(xx)/xx to y list if
            y+=[np.sin(xx)/xx] # xx is not zero
    return np.array(y) # converts y to array and returns array


[151]:
x = np.linspace(-10, 10, 256)
y = sinc(x)

plt.plot(x, y)
plt.axhline(color="gray", zorder=-1)
plt.axvline(color="gray", zorder=-1)
plt.show()
../../_images/notebooks_L3_3_functions_12_0.png

Loops are in general slowly executed and there is a faster way of checking the elements of an array by the np.where function of the NumPy library. There where function has the form

np.where(condition, output if True, output if False)

The where function applies the condition to the array element by element, and returns the second argument for those array elements for which the condition is True, and returns the third argument for those array elements that are False.

[152]:
def sinc(x):
    """
    This function calculates
    sin(x)/x
    """
    z = np.where(x==0.0, 1.0, np.sin(x)/x)
    return(z)

This code executes much faster, 25 to 100 times, depending on the size of the array, than the code using a for loop. Moreover, the new code is much simpler to write and read.

Variables in functions

Parameters and variables defined inside a function are not visible from outside the function. Hence, they are called to have a local scope. Variables inside a function live for the time as long as the function executes. They are destroyed once we return from the function. Hence, a function does not remember the value of a variable from its previous calls.

Note Local and global variables

  • local variables are visible to the inside of a function and live for the time the function is executed

  • global variable are visible outside and inside of a function but can not be changed inside a function except they are declared as global

Here is an example to illustrate the scope of a variable inside a function.

[153]:
def my_function():
    """
    Function to demonstrate the use of local and global variables
    sin(x)/x
    """
    global y #refer to a global variable

    x = 10
    y=15
    print("This is a local variable:",x)

x = 20
y = 30
my_function()
print("This is a global :",x)
print("This is a changed global :",y)
This is a local variable: 10
This is a global : 20
This is a changed global : 15

Functions with more than one input or output

Python functions can have any number of input arguments and can return any number of variables. For example, suppose you want a function that outputs n (x, y) coordinates around a circle of radius r centered at the point (x0 , y0 ). The inputs to the function would be r, x0, y0, and n. The outputs would be the n (x, y) coordinates. The following code implements this function.

[154]:
def circle(r, x0, y0, n):
    theta = np.linspace(0., 2.*np.pi, n, endpoint=False)
    x = r * np.cos(theta)
    y = r * np.sin(theta)
    return(x0+x, y0+y)

This function has four inputs and two outputs. In this case, the four inputs are simple numeric variables and the two outputs are NumPy arrays. In general, the inputs and outputs can be any combination of data types: arrays, lists, strings, etc. Of course, the body of the function must be written to be consistent with the prescribed data types. Functions can also return nothing to the calling program but just perform some task.

Positional and keyword arguments

It is often useful to have function arguments that have some default setting. This happens when you want an input to a function to have some standard value or setting most of the time, but you would like to reserve the possibility of giving it some value other than the default value.

[155]:
def circle(r, x0=0.0, y0=0.0, n=12):
    theta = np.linspace(0., 2.*np.pi, n, endpoint=False)
    x = r * np.cos(theta)
    y = r * np.sin(theta)
    return x0+x, y0+y

The default values of the arguments x0, y0, and n are specified in the argument of the function definition in the def line. Arguments whose default values are specified in this manner are called keyword arguments, and they can be omitted from the function call if the user is content using those values.

[158]:
x,y=circle(3,x0=3,n=100)
[159]:
plt.figure(figsize=(3,3))
plt.plot(x,y)
plt.show()
../../_images/notebooks_L3_3_functions_29_0.png

Functions with variable number of arguments

While it may seem odd, it is sometimes useful to leave the number of arguments unspecified. A simple example is a function that computes the product of an arbitrary number of numbers:

[160]:
def product(*args):
    print("args = {}".format(args))
    p = 1
    for num in args:
        p *= num
    return p
[163]:
product(2,3,4,5,6)
args = (2, 3, 4, 5, 6)
[163]:
720

The print(“args…) statement in the function definition is not necessary, of course, but is put in to show that the argument args is a tuple inside the function. Here it used because one does not know ahead of time how many numbers are to be multiplied together.

The *args argument is also quite useful in another context: when passing the name of a function as an argument in another function. In many cases, the function name that is passed may have a number of parameters that must also be passed but aren’t known ahead of time. If this all sounds a bit confusing—functions calling other functions—a concrete example will help you understand.

[164]:
def f1(x, a, p):
        return a*x**p
[142]:
def test(f, x, h=2, *params):
    return f(x+h,*params)
[166]:
test(f1, 3, 2, 2, 3)
[166]:
250

The order of the parameters is important. The function test uses x, the first argument of f1, as its principal argument, and then uses a and p, in the same order that they are defined in the function f1, to fill in the additional arguments—the parameters—of the function f1.

Unnamed functions (lambda function)

In Python but also in other higher lever programming languages we can also create unnamed functions, using the lambda keyword:

[167]:
f1 = lambda x: x**2

# is equivalent to

def f2(x):
    return x**2
[168]:
f1(2), f2(2)
[168]:
(4, 4)

Lambda functions are used when you need a function for a short period of time. This is commonly used when you want to pass a function as an argument to higher-order functions, that is, functions that take other functions as their arguments.

[169]:
def testfunc(num):
    return lambda x : x * num

In the above example, we have a function that takes one argument, and the argument is to be multiplied with a number that is unknown. Let us demonstrate how to use the above function:

[170]:
result1 = testfunc(10)
result2 = testfunc(1000)

print(result1(9))
print(result2(9))
90
9000

Functions as arguments of functions

Functions can be arguments of function, as we have seen above. This is a very useful thing, which we may use in out physical modeling for numerical differentiation.

Numerical Differentiation. What we want to calculate, is the derivative of a function \(f(x)\) where the function values are given at certain positions \(x_{i}\). Since we do not want to calculate the symbolic derivative, we have to get along with an numerical approximation. This can be obtained by looking at the definition of the derivative, i.e. the first derivative

\begin{equation} f^{\prime}(x)=\lim_{\delta x->0}\frac{f(x+\delta x)-f(x)}{\delta x} \end{equation}

If the function values are given at the positions \(x_{i}\) with \(\delta x_{i}=x_{i+1}-x_{i}\), the an approximate value of the first derivative can be found from

\begin{equation} f^{\prime}(x_{i})\approx\frac{f(x+\delta x)-f(x)}{\delta x}=\frac{f(x_{i+1})-f(x_{i})}{x_{i+1}-x_{i}} \end{equation}

This already delivers a good approximation of the first derivative of a function as we see in the next examples.

So lets turn that into a function.

[171]:
def D(f, x, h=1.e-9, *params):
    return (f(x+h, *params)-f(x, *params))/(h)

where our function shall be given by:

[172]:
def f0(x):
    return 4.*x**2
[173]:
D(f0,3)
[173]:
24.000001985768904
[174]:
x=np.linspace(-5,5,100)
[175]:
y=f0(x)
yp=D(f0,x)
[176]:
plt.plot(x,y,label='original')
plt.plot(x,yp,label='derivative')
plt.xlabel('x')
plt.ylabel('$y$, $y^{\prime}$')
plt.legend()
plt.show()
../../_images/notebooks_L3_3_functions_55_0.png

We may similarly also define a second derivative

\begin{equation} f^{\prime\prime}(x_{i})\approx\frac{f(x+2\delta x)-2f(x+\delta x)+f(x)}{\delta x^2}=\frac{f(x_{i+2})-2f(x_{i+1})+f(x_{i})}{(x_{i+1}-x_{i})^2} \end{equation}

[177]:
def D2(f, x, h=1.e-6, *params):
    return (f(x+2*h, *params)-2*f(x+h, *params)+f(x,*params))/(h**2)
[178]:
y=f0(x) # original function
yp=D(f0,x) # first derivative
ypp=D2(f0,x) # second derivative
[179]:
plt.plot(x,y,label='original')
plt.plot(x,yp,label='1st derivative')
plt.plot(x,ypp,label='2nd derivative')
plt.xlabel('x')
plt.ylabel('$y$, $y^{\prime}$,$y^{\prime\prime}$')
plt.legend()
plt.show()
../../_images/notebooks_L3_3_functions_59_0.png