**1. The Basics of Functions**

If you are going to evaluate the same expression or perform the same sequence of operations many times in a program, it would be tedious and error-prone to keep retyping the same code. It is more efficient to define a function. Functions can make your code better structured, which means easier to write, understand and correct.

This is how to use a Python function to evaluate a simple polynomial:

` ````
def f(x):
return x**3-7.0*x**2+14.0*x-5.0
```

Note the elements of a function definition. We begin with the keyword **def** followed by the name of the function (in this case **f**). In round brackets **()** we provide parameters for the function: these are variables that the function will use. In this case **x** is the only parameter passed to the function. The definition line ends with a colon and the body of the function is indented.

Indenting puts the body of the of the function in a new block (for more info on blocks see the Python reference). In the body of the function we have a **return** statement. Return quite literally returns or outputs what was achieved in the preceding code and ends the block. It is permissible to have more than one return statement in a function, but the function will stop executing as soon as one of them is reached; the golden rule is to have at most one return statement per block. We will see later a more complicated function which has another block inside the function body and two return statements instead of one.

We call a function on some variable **x** in our code simply by typing **f(x)**; Python will then substitute the value from the return statement instead of **f(x)**:

` ````
>>> x = 5
>>> f(x)
15.0
>>> f(-1) #we can call our function directly on a literal
-27.0
>>> f(-1)**2 + f(-2) #expressions of the form f(x) are treated as literals by Python
660.0
```

We must be careful to pass parameters of the right type to functions; for example, passing a **str** to **f** will result in an error:

` ````
>>> f('a')
Traceback (most recent call last):
File "C:\<string>", line 1, in <module>
File "C:\<string>", line 2, in f
TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'
```

Of course, the function defined by us could use some other functions, defined before. Let’s say that we already defined a function called **power3(x)** and another one called **multiply(x,y)**:

` ````
def power3(x):
return x*x*x
def multiply(x,y):
return x*y
```

Note that the function multiply takes two parameters, and that these are separated by commas in the function definition. A function **g** can be written with the help of **power3** and **multiply**:

` ````
def g(x):
return power3(x)-multiply(7.0,x**2)+multiply(14.0,x)-5.0
```

Some may say that this is a tedious way of doing it. Indeed, it is only useful to use functions as substitutes for complicated, frequently occurring code. It is not sensible to define the function **power3** when we could just as easily write **x**3 in its place whenever we need to. Here is a more complicated example of a function:

` ````
def find(L, x):
'''Given list L and a value x, return the index of the first occurrence of x in L, or return the length of L if x is not
in L'''
for i in range(0, len(L)):
if L[i] == x:
return i
return len(L)
```

The comment at the beginning of the function body is called a **docstring** and it explains what the function does to anyone who reads your code. The docstring will be displayed when a user calls the help method on your function. It is important to write docstrings for all functions; without the docstring, it would take a while to figure out what this example function does!

Note that this function has two return statements. This is fine, because one of them will always be reached before the other. The function stops executing as soon as it reaches a return statement, and the value returned will be from that statement only. A function could also have no return statement at all, in which case it stops executing once all the code in the function body has executed. In this case, the function will still do something when we call it, but it will not return a value:

` ````
from pylab import *
def graph(L1, L2):
'''Given two lists L1 and L2 of the same length, plot the ordered
pairs (x,y) where x is an element of L1 and y is the element of L2
with the same index'''
plot(L1, L2)
show()
>>> L1=[1,2, 3]
>>> L2=[3, 4,5]
>>> graph(L1,L2) #a graph will be displayed once this line is executed
>>> x = graph(L1, L2) #a graph will be displayed again because the function is called
>>> x
>>>
```

Note that no value was stored in **x**, because our function did not return any value! We must be very careful with this as it could cause errors; Python will not tell us that the function does not return a value, instead it will simply not store any value in **x**.

**Activity 1:**

In one of the previous activities, you were asked to return the orbital period of a circular orbit of radius **R** of a particle in the gravitational field of a massive body **M**. We wrote this as ordinary code, but it is much more effective to encapsulate it into a function so we can use it whenever we want without retyping it. Write a function period which takes parameters **M** and **R** and returns the period. **Solution:**solution_activity_1.pdf

**2. Importing Modules and Mathematical Functions**

We can save a collection of functions in a separate file to use them in other programs we write; such a collection of functions is called a **module**. You can save the function **f(x)** giving it a **.py** suffix. Let’s call it **cubicpoly.py**. If this is to be used as a module later, then Python has to locate the file. Python searches first in the current directory and then in a list of directories called the search path. If you are running Python interactively, it may not be so clear what the search path is. You can find out by typing:

` ````
import sys
print(sys.path)
```

If it does not contain the directory you want, you can append a string containing the directory name to this list, before trying to import a module from that directory. For instance, to add the Windows C drive to your path you type:

` ``sys.path.append('C:\\')`

Now, in the Python shell window you should be able to import the module and use your function:

` ````
from cubicpoly import f
f(3)
```

Python will evaluate the function for x = 3.

Standard mathematical functions like **sin**, **log**, **sqrt** etc. are not part of the core language, but are provided by the standard library in the **math** module. Here is how to access a module, using math as an example in a program:

- Import a module and use the objects from the module:

` ````
import math
print(math.sin(0.5))
```

- Note that we must specify that sin comes from the math module by typing math.sin. If we don’t, Python will get confused and return an error.
- Use abbreviations:

` ````
import math as m
print(m.sin(0.5))
print(m.pi)
```

- This example also shows that the math module contains an alias for π, referred as
**pi**in the code; in general, we can include variables in modules we make ourselves. - Import the objects you need explicitly:

` ````
from math import sin, pi
print(sin(0.5))
print(pi)
```

You can import all objects from a module using the ”wildcard” *:

` ````
from math import *
print (cos(pi/3), log(54.3), sqrt(6))
```

The **import** * form is very powerful and useful, but it has a downside: you may import functions with the same names but with different effects from different modules. The last definition imported will have precedence. When importing many different modules, it is best to avoid * unless you are absolutely certain that there will be no overlap in function names.

Sometimes we will import files (modules) which have additional code in them aside from function and variable definitions (perhaps code used to test the functions in the module, or something). This code will always execute upon importing, which is something we may not want. As a rule, any additional code in a file that will be imported into another program must be enclosed in the following construction:

` ````
# function definitions go here
if __name__ == "__main__":
# additional code goes here
```

This ensures the block of code executes only if we run that file directly (making it the “main” file). If this is imported into another file and we run that, the code will not execute.

**Activity 2:**

Write a function **solve_linear(a, b)** that will return the root of the linear equation *ax + b =* 0, or return "No solutions" if the equation does not have a solution, or return "All real numbers" if all real numbers satisfy the equation. Write a function **solve_quadratic(a, b, c)** that will return the roots of the quadratic equation *ax*² *+ bx + c =* 0 (think of a creative way to return both roots and to return the roots even if they are complex), or return "No solutions" if the equation does not have a solution, or return "All complex numbers" if all complex numbers satisfy the equation. Put these two functions into a module called **algebra.py**. Now write another program which prompts the user for a choice to solve a linear or a quadratic equation, allows the user to enter all the required coefficients, and then uses an appropriate function from **algebra** to display the solutions. **Solution:** algebra.py solution_activity_2.py

Here are some questions to practice what you have learned:

functions_and_modules.questions.pdf

functions_and_modules.solutions.pdf