# Defining and Using Python Functions

## Contents

# 4. Defining and Using Python Functions#

*Estimated time to complete:* one to two hours.

## 4.1. Introduction#

Our main objective is to learn how to define our own Python *functions,* and see how these can help us to deal with sub-tasks done repeatedly (with variation) within a larger task.

Typically, core mathematical tasks (like evaluating a function \(f(x)\) or solving an equation \(f(x) = 0\)) will be done within Python functions designed to communicate with other parts of the Python code, rather than getting their input interactively or returning results by displaying them on the screen.

In this course, the notebook approach is prioritized; that makes it easier to produce a single document that combines text and mathematical information, Python code, and computed results, making for a more “literate”, understandable presentation.

**Example A. A very simple function for computing the mean of two numbers**

To illustrate the syntax, let’s start with a very simple function: computing the mean of two numbers.

```
def mean(a, b):
mean_of_a_and_b = (a + b)/2
return mean_of_a_and_b
```

This can be used as follows:

```
mean_of_2_and_6 = mean(2, 6)
print('The mean of 2 and 6 is', mean_of_2_and_6)
```

```
The mean of 2 and 6 is 4.0
```

### Notes#

The definition of a function begins with the command

`def`

This is followed by:

a name for the function,

a parenthesized, comma-seperated list of variable names, which are called the input

*arguments*,and finally a

**colon**, which as always introduces an indented list or*block*of statements; in this case the statements that describe the actions performed by the function.Indentation is done with

**four spaces**. (Tabs are also legal but advised against, so please forget that I mentioned them.)The end of the block of code for the function is indicated simply by the end of the indentation; there is no

`end`

line as in some other programming languages. The last line of the definiton is often a`return`

statement, but there can also be multiple`return`

statements at various places in the code block, or no`return`

at all.

When using the function, it is given a list of expressions (variable names or values or more elaborate formulas) and the values of these are

*copied*to the corresponding internal variables given by the list of input arguments mentioned in the function’s`def`

line. (However, with lists and arrays, the idea of*copying*will require clarification!)As soon as the function gets to a statement beginning with

`return`

, it evaluates the expression on that line, and ends, sending back this as the value of the function.The name used for a variable into which the value of a function is assigned (here,

`mean_of_2_and_6`

) does not have to be the same as the name used internally in the`return`

statement (here,`mean_of_a_and_b`

).

In general, what follows `return`

is an expression (formula) that is evaluated to get the value that is then sent back; listing the name of one or more variables is just one simple way to do this. For example, the above function `mean`

could instead be defined with

```
def mean(a, b):
return (a + b)/2
```

Also, multiple values can be given in a `return`

line; they are then output as a tuple

```
def mean_and_difference(a, b):
return (a + b)/2, b - a
```

```
mean_and_difference(2, 5)
```

```
(3.5, 3)
```

## 4.2. Variables “created” inside a function definition are local to it#

A more subtle point: all the variables appearing in the function’s `def`

line (here `a`

and `b`

) and any created inside with an assignment statement (here just `mean_of_a_and_b`

) are purely *local* to the function; they do not exist outside the function. For that reason, when you call a function, you have to do something with the return value, like assign it to a variable (as done here with `mean_of_a_and_b`

) or use it as input to another function (see below).

**Aside:** there is a way that a variable can be shared between a function and the rest of the file in which the function definition appears; so-called *global variables*, using `global`

statements.
However, it is generally good practice to avoid them as much as possible, and I will do so in this course.

To illustrate this point about local variables, let us look for the values of variables `a`

and `mean_of_a_and_b`

in the code after the function is called:

```
mean310 = mean(3, 10)
```

```
print('After using function mean:')
print(f'a = {a}')
```

```
After using function mean:
```

```
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Input In [6], in <cell line: 2>()
1 print('After using function mean:')
----> 2 print(f'a = {a}')
NameError: name 'a' is not defined
```

```
print('mean_of_a_and_b =', mean_of_a_and_b)
```

```
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Input In [7], in <cell line: 1>()
----> 1 print('mean_of_a_and_b =', mean_of_a_and_b)
NameError: name 'mean_of_a_and_b' is not defined
```

```
print(f'mean310 = {mean310}')
```

```
mean310 = 6.5
```

The same name can even be used locally in a function and also outside it in the same file; they are different objects with independent values:

```
def double(a):
print(f'At the start of function "double", a = {a}')
a = 2 * a
print(f'A bit later in that function, a = {a}')
return a
a = 1
print(f'Before calling function "double", a = {a}')
b = double(a)
print(f'After calling function "double", b = {b}, but a = {a} again.')
```

```
Before calling function "double", a = 1
At the start of function "double", a = 1
A bit later in that function, a = 2
After calling function "double", b = 2, but a = 1 again.
```

**Warning about keeping indentation correct:** The line `a = 1`

is after the function definition, not part of it, as indicated by the reduced indentation; however Python code editing tools like JuperLab and Spyder will default to indenting each new line as much as the previous one when you end a line by typing “return”.
Thus, when typing in a function `def`

, it is important to manually reduce the indentation (“dedent”) at the end of the definition.
The same is true for all statements that end with a colon and so control a following block of code, like `if`

, `else`

, `elif`

and `for`

.

**Example B. More on multiple output values, with tuples**

Often, a function computes and returns several quantities; one example is a function version of our quadratic equation solver, which takes three input parameters and computes a pair of roots. Here is a very basic function for this, ignoring for now possible problems like division by zero:

```
from math import sqrt
```

```
def solve_quadratic(c_2, c_1, c_0):
'''
Note: c_i is the coefficient of x^i
'''
discriminant = c_1**2 - 4 * c_0 * c_2
root0 = (-c_1 + sqrt(discriminant))/(2 * c_2)
root1 = (-c_1 - sqrt(discriminant))/(2 * c_2)
return root0, root1
```

Now what is returned is a tuple, and it can be stored into a single variable:

```
roots = solve_quadratic(2, -10, 8)
print(f'Variable "roots" has value {roots}')
print(f'One root is {roots[0]} and the other is {roots[1]}')
```

```
Variable "roots" has value (4.0, 1.0)
One root is 4.0 and the other is 1.0
```

However, it is often convenient to store each returned value into a separate variable, using tuple notation at left in the assignment statement:

```
(rootA, rootB) = solve_quadratic(2, -10, 8)
print(f'One root is {rootA} and the other is {rootB}')
```

```
One root is 4.0 and the other is 1.0
```

### Note: With tuples, parentheses are optional#

When tuples were introduced in the section on Python Variables, Including Lists and Tuples, and Arrays from Package Numpy,
they were described as “a parenthesized list of values separated by commas”,
but note that above, no parentheses were used: the `return`

line was

```
return root0, root1
```

not

```
return (root0, root1)
```

though the latter version would also work.

In fact, you can always omit the parentheses when specifying a tuple, just giving a comma-separated list of values, even on the left side of an assignment statement. Thus, the above example could also be done like this:

```
rootA, rootB = solve_quadratic(2, -10, 8)
print(f'One root is {rootA} and the other is {rootB}')
```

```
One root is 4.0 and the other is 1.0
```

## 4.3. Single-member tuples: not an oxymoron#

Tuples can have a single member, but then to make it clear to Python that it is a tuple, there must always be a comma after that sole element. Compare:

```
tuple_a = (1,)
print(tuple_a)
```

```
(1,)
```

```
tuple_b = 2,
print(tuple_b)
```

```
(2,)
```

```
not_a_tuple_c = (3)
print(not_a_tuple_c)
```

```
3
```

## 4.4. Input argument lists are also tuples#

You can also think of the collection of input arguments to a function as a tuple. This works nicely for composition of functions: the output tuple from one function is passed as the input tuple for the next, as we will see soon.

The input to and output from functions is probably the main place that we will use tuples in this course.

The ability to store the collection of output values from a function in a single tuple can also be convenient when you wish to store that collection for later use.

Here I illustrate the strategy of dividing a task into several smaller, simpler pieces, each of which can then be reused independently:

Step 1 is to compute the roots

step 2 is to print them, sorted into increasing order.

```
def printtwonumbersinorder(numbers):
'''
Print two numbers in increasing order.
Warning: this assumes real input values,
and is not very stylish when the two numbers are equal.
'''
if numbers[0] <= numbers[1]:
# Easy; they are already in order
print(f'The results, in order, are {numbers[0]:g} and {numbers[1]:g}')
else:
# swap them:
print(f'The results, in order, are {numbers[1]:g} and {numbers[0]:g}')
```

```
bothroots = solve_quadratic(2, -10, 8)
print('The unsorted tuple of roots is', bothroots)
printtwonumbersinorder(bothroots)
```

```
The unsorted tuple of roots is (4.0, 1.0)
The results, in order, are 1 and 4
```

We can compose functions, and so do these two steps in one:

```
printtwonumbersinorder(solve_quadratic(1, 5, -6))
```

```
The results, in order, are -6 and 1
```

**Aside: documenting functions with triple quoted comments, and help**

Note that the code block for function `printtwonumbersinorder`

starts with a comment surrounded by a triple of quote characters at each end, and this sort of comment can continue over multiple lines.

In addition to making it easier to have long comments, this sort of comment provides some *self-documentation* for the function — not just in the code for the function, but also in the Python `help`

system:

```
help(printtwonumbersinorder)
```

```
Help on function printtwonumbersinorder in module __main__:
printtwonumbersinorder(numbers)
Print two numbers in increasing order.
Warning: this assumes real input values,
and is not very stylish when the two numbers are equal.
```

About the reference to `module __main__`

: that is the standard name for code that is not explicitly part of any other module, such as anything defined in the current file rather than imported from elsewhere. Compare to what we get when a function comes from another module:

```
help(sqrt)
```

```
Help on built-in function sqrt in module math:
sqrt(x, /)
Return the square root of x.
```

As you might expect, all objects provided by standard modules like `math`

and `numpy`

have some documentation provided; `help`

is useful in cases like this where you cannot see the Python code that defines the function.

When a function `def`

lacks such a *self-documentation* comment, `help`

still tells us something; the syntax for using it, and where it come from:

```
help(mean)
```

```
Help on function mean in module __main__:
mean(a, b)
```

### Exercise A. A robust function for solving quadratics#

Refine the above function `solve_quadratic`

for solving quadratics, and make it **robust**; handling all possible input triples of real numbers in a “reasonable” way.
(If you have done Exercise C of the section Decision Making With if, else, and elif,
this is essentially just integrating that code into a function.)

Not all choices of those coefficients will give two distinct real roots, so work out all the possibilities, and try to handle them all.

Its input arguments are three real (floating point) numbers, giving the coefficients \(a\), \(b\), and \(c\) of the quadratic equation \(ax^2 + bx + c = 0\).

It always

**returns**a result (usually a pair of numerical values for the roots) for any “genuine” quadratic equation, meaning one with \(a \ne 0\).If the quadratic has real roots, these are output with a

`return`

statement — no`print`

commands in the function.In cases where it is not a genuine quadratic, or there are no real roots, return the special value

`None`

.As an

**optional**further refinement: in the “prohibited” case \(a = 0\), have it produce a custom error message, by “raising an exception”. We will learn more about handling “exceptional” situations later, but for now you could just use the command:raise ZeroDivisionError(“The coefficient ‘a’ of x^2 cannot have the value zero.”)

or

```
raise ValueError("The coefficient 'a' of x^2 cannot have the value zero.")
```

Ultimately put this in a cell in a Jupyter notebook (suggested name: “functionsExerciseA.ipynb”); if you prefer to develop it with Spyder, I suggest the filename “quadratic_solver_c.py”

### Testing#

Test and demonstrate this function with a list of test cases, including:

\(2x^2 - 10x + 8 = 0\)

\(x^2 -2x + 1 = 0\)

\(x^2 + 2 = 0\)

\(x^2 + 6x + 25 = 0\)

\(4x - 10 = 0\)

### Notebook organization#

To start developing good notebook organizational practice:

As always, start with a title cell giving a title, your name and the date.

Follow this by an introduction cell giving a brief description of what the notebook does — see above; follow the links here!

Put any

`import`

statements straight after the introduction, before any other Python code.Define the function in one cell (more generally, define each function in its own cell).

Follow the function definition[s] with the test cases, each in its own cell.

If anything noteworthy arises in a test case, add comments on this in a Markdown after that test case cell.

## 4.5. Keyword arguments: specifying input values by name#

Sometimes a function has numerous input arguments, and then it might be hard to remember what order they go in.

Even with just a few arguments, there can be room for confusion;
for example, in the above function `solve_quadratic`

do we give the coefficients in order `c_2, c_1, c_0`

as for \(c_2 x^2 + c_1 x + c_0 = 0\), or in order `c_0, c_1, c_2`

as for \(c_0 + c_1 x + c_2 x^2 = 0\)?

To improve readability and help avoid errors, Python has a nice optional feature of specifying input arguments *by name*; they are then called *keyword arguments*, and can be given in any order.

For example:

```
moreroots = solve_quadratic(c_1=3, c_2=1, c_0=-10)
printtwonumbersinorder(moreroots)
```

```
The results, in order, are -5 and 2
```

When you are specifying the parameters by name, there is no need to have them in any particular order. For example, if you like to write polynomials “from the bottom up”, as with \(-10 + 3x + x^2\), which is \(c_0 + c_1 x + c_2 x^2\), you could do this:

```
sameroots = solve_quadratic(c_0=-10, c_1=3, c_2=1)
printtwonumbersinorder(sameroots)
```

```
The results, in order, are -5 and 2
```

## 4.6. Functions as input to other functions#

In mathematical computing, we often wish to define a (Python) function that does something with a (mathematical) function. A simple example is implementing the basic difference quotient approximation of the derivative

with a function `Df_approximation`

, whose input will include the function \(f\) as well as the two numbers \(x\) and \(h\).

Python makes this fairly easy, since Python functions, like numbers, can be the values of variables, and given as input to other functions in the same way: in fact the statement

```
def f(...): ...
```

creates variable `f`

with this function as its value.

So here is a suitable definition:

```
def Df_approximation(f, x, h):
return (f(x+h) - f(x))/h
```

and one way to use it is as follows:

```
def p(x):
return 2*x**2 - 10*x + 8
x0 = 1
h = 1e-4
Df_x0_h = Df_approximation(p, x0, h)
print(f'Df({x0}) is approximately {Df_x0_h}')
```

```
Df(1) is approximately -5.999800000004996
```

A bit more about keyword arguments: they can be used together with positional arguments, but once an argument is given in keyword form, all later ones must be also. Thus it works to do this:

```
Dfxh = Df_approximation(p, x=x0, h=1e-6)
print(f'Df({x0}) is approximately {Dfxh}')
```

```
Df(1) is approximately -5.99999799888451
```

but the following fails:

```
Dfxh = Df_approximation(p, x=2, 0.000001)
print(f'Df(2) is approximately {Dfxh}')
```

```
Input In [30]
Dfxh = Df_approximation(p, x=2, 0.000001)
^
SyntaxError: positional argument follows keyword argument
```

## 4.7. Optional input arguments and default values#

Sometimes it makes sense for a function to have default values for arguments, so that not all argument values need to be specified.
For example, the value \(h= 10^{-8}\) is in some sense close to “ideal”, so let us make that the default, by giving `h`

a “suggested” value as part of the function’s (new, improved) definition:

```
def Df_approximation(f, x, h=1e-8):
return (f(x+h) - f(x))/h
```

The value for input argument `h`

can now optionally be omitted when the function is used, getting the same result as before:

```
Df_x0_h = Df_approximation(p, x0)
print(f'Df({x0}) is approximately {Df_x0_h}')
```

```
Df(1) is approximately -5.999999963535174
```

or we can specify a value when we want to use a different one:

```
big_h = 0.01
Df_x0_h = Df_approximation(p, x0, big_h)
print(f'Using h={big_h}, Df({x0}) is approximately {Df_x0_h}')
```

```
Using h=0.01, Df(1) is approximately -5.979999999999919
```

### Arguments with default values must come after all others in the `def`

#

When default values are given for some arguments but not all, these must appear in the function definition after all the arguments without default values, as is the case with `h=1e-8`

above.

### Exercise B. A function with function inputs (and exceptions)#

A usually more accurate formula for approximating derivatives is the *Centered Difference Rule*
$\(Df(x) \approx \frac{f(x+h) - f(x-h)}{2 h}\)$

Write a function that uses this, with usage

`Dfxh = Df_CD_approximation(f, x, h)`

Give \(h\) the default value \(10^{-12}\)

Raise an exception if the forbidden value \(h=0\) is used.

Choose and implement a few test cases.

You may do this all in the same notebook as above.

If you wish to also do this in a Python code file, use a different file than the quadratic solving exercise; maybe named “centered_differences.py”.

## 4.8. Optional topic: anonymous functions, a.k.a. lambda functions#

**Note:**
*Some students might be interested in “anonymous functions”, also known as “lambda functions”, so here is a brief introduction.
However, this topic is not needed for this course; it is only a convenience, and if you are new to computer programming, I suggest that you skip this section for now.*

One inconvenience in the above example with `Df_approximation`

is that we had to first put the values of each input argument into three variables. Sometimes we would rather skip that step, and indeed we have seen that we could put the numerical argument values in directly:

```
Df_x_h = Df_approximation(p, 4, 1e-4)
print(f'The derivative is approximately {Df_x_h}')
```

```
The derivative is approximately 6.000200000002565
```

However, we still needed to define the function first, and give it a name, `p`

.

If the function is only ever used this one time, we can avoid this, specifying the function directly as an input argument value to the function `Df_approximation`

, without first naming it. This is done with what is called an *anonymous function*, or for mysterious historical reasons, a *lambda function*.

For the example above, we can do this:

```
Df_x_h = Df_approximation(lambda x: 2*x**2 - 10*x + 8, 4, 1e-4)
print(f'The derivative is approximately {Df_x_h}')
```

```
The derivative is approximately 6.000200000002565
```

We can even do it all in a single line by composing two functions, `print`

and `Df_approximation`

:

```
print(f'The derivative is approximately {Df_approximation(lambda x: 2*x**2 - 10*x + 8, 4, 1e-4)}')
```

```
The derivative is approximately 6.000200000002565
```

Here, the expression

```
lambda x: 2*x**2 - 10*x + 8
```

creates a function that is mathematically the same as function `p`

above; it just has no name.

In general, the form is a single-line expression with four elements:

It starts with

`lambda`

next is a list of input argument names, separated by commas if there are more than one (but no parentheses!?)

then a colon

and finally, a formula involving the input variables.

We can, if we want, assign a lambda function to a variable, so we could have defined `p`

as

```
p = lambda x: 2*x**2 - 10*x + 8
```

though I am not sure if that has any advantage over doing this with `def`

:

```
def p(x): return 2*x**2 - 10*x + 8
```

As an example of that, and also of having a lambda function that returns multiple values, here is yet another quadratic equation solver:

```
solve_quadratic = lambda a, b, c: ( (-b + sqrt(b**2 - 4*a*c))/(2 * a), (-b - sqrt(b**2 - 4*a*c))/(2 * a) )
```

```
print(f'The roots of 2*x**2 - 10*x + 8 are {solve_quadratic(2, -10, 8)}')
```

```
The roots of 2*x**2 - 10*x + 8 are (4.0, 1.0)
```

Anonymous functions have most of the fancy features of functions created with `def`

, with the big exception that they must be defined on a single line. For example, they also allow the use of keyword arguments, allowing the input argument values to be specified by keyword in any order. It is also possible to give default values to some arguments at the end of the argument list.

To show off a few of these refinements:

```
printtwonumbersinorder(solve_quadratic(b=-10, a=2, c=8))
```

```
The results, in order, are 1 and 4
```