```
from math import *
y = sin(x)*log(x)
```

Other functions:

```
n = len(somelist)
integers = range(5, n, 2)
```

Functions used with the dot syntax (called *methods*):

```
C = [5, 10, 40, 45]
i = C.index(10) # result: i=1
C.append(50)
C.insert(2, 20)
```

What is a function? So far we have seen that we put some objects in and sometimes get an object (result) out of functions. Now it is time to write our own functions!

Function = a collection of statements we can execute wherever and whenever we want

Function can take

*input objects*(arguments) and produce output objects (returned results)Functions help to organize programs, make them more understandable, shorter, reusable, and easier to extend

The mathematical function

$$ F(C)={9\over5}C+32 $$

can be implemented in Python as follows:

In [1]:

```
def F(C):
return (9.0/5)*C + 32
```

Note:

Functions start with

`def`

, then the name of the function, then a list of arguments (here`C`

) - the*function header*Inside the function: statements - the

*function body*Wherever we want, inside the function, we can "stop the function" and return as many values/variables we want

**A function does not do anything before it is called.**

In [2]:

```
def F(C):
return (9.0/5)*C + 32
a = 10
F1 = F(a) # call
temp = F(15.5) # call
print F(a+1) # call
sum_temp = F(10) + F(20) # two calls
Fdegrees = [F(C) for C in [0, 20, 40]] # multiple calls
```

**Note:**

The call `F(C)`

produces (returns) a `float`

object, which means that `F(C)`

is
replaced by this `float`

object. We can therefore
make the call `F(C)`

everywhere a `float`

can be used.

Make a Python function of the mathematical function

$$ y(t) = v_0t- \frac{1}{2}gt^2 $$

In [3]:

```
def yfunc(t, v0):
g = 9.81
return v0*t - 0.5*g*t**2
# sample calls:
y = yfunc(0.1, 6)
y = yfunc(0.1, v0=6)
y = yfunc(t=0.1, v0=6)
y = yfunc(v0=6, t=0.1)
```

In [4]:

```
def yfunc(t, v0):
g = 9.81
return v0*t - 0.5*g*t**2
v0 = 5
t = 0.6
y = yfunc(t, 3)
```

**Local vs global variables.**

When calling `yfunc(t, 3)`

, all these statements are in fact executed:

```
t = 0.6 # arguments get values as in standard assignments
v0 = 3
g = 9.81
return v0*t - 0.5*g*t**2
```

Inside `yfunc`

, `t`

, `v0`

, and `g`

are *local variables*, not visible outside
`yfunc`

and desroyed after `return`

.

Outside `yfunc`

(in the main program), `t`

, `v0`

, and `y`

are *global variables*, visible everywhere.

The `yfunc(t,v0)`

function took two arguments.
Could implement $y(t)$ as a function of $t$ only:

In [5]:

```
def yfunc(t):
g = 9.81
return v0*t - 0.5*g*t**2
```

In [6]:

```
t = 0.6
yfunc(t)
```

Problem: `v0`

must be defined in the calling program program before
we call `yfunc`

!

In [7]:

```
v0 = 5
yfunc(0.6)
```

Note: `v0`

and `t`

(in the main program) are global variables, while
the `t`

in `yfunc`

is a local variable.

Test this:

In [8]:

```
def yfunc(t):
print '1. local t inside yfunc:', t
g = 9.81
t = 0.1
print '2. local t inside yfunc:', t
return v0*t - 0.5*g*t**2
t = 0.6
v0 = 2
print yfunc(t)
print '1. global t:', t
print yfunc(0.3)
print '2. global t:', t
```

In [9]:

```
def yfunc(t):
g = 9.81
global v0 # now v0 can be changed inside this function
v0 = 9
return v0*t - 0.5*g*t**2
v0 = 2 # global variable
print '1. v0:', v0
print yfunc(0.8)
print '2. v0:', v0
```

**What happens if we comment out global v0?**

`v0`

in `yfunc`

becomes a local variable (i.e., we have two `v0`

)

Say we want to compute $y(t)$ and $y'(t)=v_0-gt$:

In [10]:

```
def yfunc(t, v0):
g = 9.81
y = v0*t - 0.5*g*t**2
dydt = v0 - g*t
return y, dydt
# call:
position, velocity = yfunc(0.6, 3)
```

Separate the objects to be returned by comma, assign to variables separated by comma. Actually, a tuple is returned:

In [11]:

```
def f(x):
return x, x**2, x**4
```

In [12]:

```
s = f(2)
s
```

In [13]:

```
type(s)
```

The function

$$ L(x;n) = \sum_{i=1}^n {1\over i}\left( {x\over 1+x}\right)^{i} $$

is an approximation to $\ln (1+x)$ for a finite $n$ and $x\geq 1$.

**Corresponding Python function for $L(x;n)$:**

In [14]:

```
def L(x, n):
x = float(x) # ensure float division below
s = 0
for i in range(1, n+1):
s += (1.0/i)*(x/(1+x))**i
return s
x = 5
from math import log as ln
print L(x, 10), L(x, 100), ln(1+x)
```

We can return more: 1) the first neglected term in the sum and 2) the error ($\ln (1+x) - L(x;n)$):

In [15]:

```
def L2(x, n):
x = float(x)
s = 0
for i in range(1, n+1):
s += (1.0/i)*(x/(1+x))**i
value_of_sum = s
first_neglected_term = (1.0/(n+1))*(x/(1+x))**(n+1)
from math import log
exact_error = log(1+x) - value_of_sum
return value_of_sum, first_neglected_term, exact_error
# typical call:
x = 1.2; n = 100
value, approximate_error, exact_error = L2(x, n)
```

```
def somefunc(obj):
print obj
return_value = somefunc(3.4)
```

Here, `return_value`

becomes `None`

because if we do not explicitly return something, Python will insert `return None`

.

**Make a table of $L(x;n)$ vs. $\ln (1+x)$:**

In [16]:

```
def table(x):
print '\nx=%g, ln(1+x)=%g' % (x, log(1+x))
for n in [1, 2, 10, 100, 500]:
value, next, error = L2(x, n)
print 'n=%-4d %-10g (next term: %8.2e '\
'error: %8.2e)' % (n, value, next, error)
print table(10)
```

No need to return anything here - the purpose is to print.

Functions can have arguments of the form `name=value`

,
called *keyword arguments*:

```
def somefunc(arg1, arg2, kwarg1=True, kwarg2=0):
print arg1, arg2, kwarg1, kwarg2
```

In [17]:

```
def somefunc(arg1, arg2, kwarg1=True, kwarg2=0):
print arg1, arg2, kwarg1, kwarg2
```

In [18]:

```
somefunc('Hello', [1,2]) # drop kwarg1 and kwarg2
```

In [19]:

```
somefunc('Hello', [1,2], kwarg1='Hi')
```

In [20]:

```
somefunc('Hello', [1,2], kwarg2='Hi')
```

In [21]:

```
somefunc('Hello', [1,2], kwarg2='Hi', kwarg1=6)
```

If we use `name=value`

for *all* arguments *in the call*,
their sequence can in fact be arbitrary:

In [22]:

```
somefunc(kwarg2='Hello', arg1='Hi', kwarg1=6, arg2=[2])
```

Consider a function of $t$, with parameters $A$, $a$, and $\omega$:

$$ f(t; A,a, \omega) = Ae^{-at}\sin (\omega t) $$

**Possible implementation.**

Python function with $t$ as positional argument, and $A$, $a$, and $\omega$ as keyword arguments:

In [23]:

```
from math import pi, exp, sin
def f(t, A=1, a=1, omega=2*pi):
return A*exp(-a*t)*sin(omega*t)
v1 = f(0.2)
v2 = f(0.2, omega=1)
v2 = f(0.2, 1, 3) # same as f(0.2, A=1, a=3)
v3 = f(0.2, omega=1, A=2.5)
v4 = f(A=5, a=0.1, omega=1, t=1.3)
v5 = f(t=0.2, A=9)
v6 = f(t=0.2, 9) # illegal: keyword arg before positional
```

**Important Python convention:**

Document the purpose of a function, its arguments, and its return values in a *doc string* - a (triple-quoted) string written right after the function header.

In [24]:

```
def C2F(C):
"""Convert Celsius degrees (C) to Fahrenheit."""
return (9.0/5)*C + 32
def line(x0, y0, x1, y1):
"""
Compute the coefficients a and b in the mathematical
expression for a straight line y = a*x + b that goes
through two points (x0, y0) and (x1, y1).
x0, y0: a point on the line (floats).
x1, y1: another point on the line (floats).
return: a, b (floats) for the line (y=a*x+b).
"""
a = (y1 - y0)/(x1 - x0)
b = y0 - a*x0
return a, b
```

A function can have three types of input and output data:

input data specified through positional/keyword arguments

input/output data given as positional/keyword arguments that will be modified and returned

output data created inside the function

*All output data are returned, all input data are arguments*

```
def somefunc(i1, i2, i3, io4, io5, i6=value1, io7=value2):
# modify io4, io5, io7; compute o1, o2, o3
return o1, o2, o3, io4, io5, io7
```

The function arguments are

pure input:

`i1`

,`i2`

,`i3`

,`i6`

input and output:

`io4`

,`io5`

,`io7`

In [25]:

```
from math import * # in main
def f(x): # in main
e = exp(-0.1*x)
s = sin(6*pi*x)
return e*s
x = 2 # in main
y = f(x) # in main
print 'f(%g)=%g' % (x, y) # in main
```

The execution starts with the first statement in the main program and proceeds line by line, top to bottom.

`def`

statements define a function, but the statements inside the function are not executed before the function is called.

Programs doing calculus frequently need to have functions as arguments in other functions, e.g.,

numerical integration: $\int_a^b f(x)dx$

numerical differentiation: $f'(x)$

numerical root finding: $f(x)=0$

- All three cases need $f$ as a Python function
`f(x)`

**Example: numerical computation of $f''(x)$.**

$$ f''(x) \approx {f(x-h) - 2f(x) + f(x+h)\over h^2} $$

In [26]:

```
def diff2(f, x, h=1E-6):
r = (f(x-h) - 2*f(x) + f(x+h))/float(h*h)
return r
```

No difficulty with `f`

being a function
(more complicated in Matlab, C, C++, Fortran, Java, ...).

`diff2`

function (read the output!)¶**Code:**

In [27]:

```
def g(t):
return t**(-6)
# make table of g''(t) for 13 h values:
for k in range(1,14):
h = 10**(-k)
print 'h=%.0e: %.5f' % (h, diff2(g, 1, h))
```

For $h < 10^{-8}$ the results are totally wrong!

We would expect better approximations as $h$ gets smaller

Problem 1: for small $h$ we subtract numbers of approx equal size and this gives rise to round-off errors

Problem 2: for small $h$ the round-off errors are multiplied by a big number

Remedy: use float variables with more digits

Python has a (slow) float variable (

`decimal.Decimal`

) with arbitrary number of digitsUsing 25 digits gives accurate results for $h \leq 10^{-13}$

Is this really a problem? Quite seldom - other uncertainies in input data to a mathematical computation makes it usual to have (e.g.) $10^{-2}\leq h \leq 10^{-6}$

In [28]:

```
def f(x):
return x**2 - 1
```

The *lambda* construction can define this function in one line:

In [29]:

```
f = lambda x: x**2 - 1
```

In general,

```
somefunc = lambda a1, a2, ...: some_expression
```

is equivalent to

```
def somefunc(a1, a2, ...):
return some_expression
```

Lambda functions can be used directly as arguments in function calls:

```
value = someotherfunc(lambda x, y, z: x+y+3*z, 4)
```

**Verbose standard code:**

In [30]:

```
def g(t):
return t**(-6)
dgdt = diff2(g, 2)
print dgdt
```

**More compact code with lambda:**

In [31]:

```
dgdt = diff2(lambda t: t**(-6), 2)
print dgdt
```

Sometimes we want to peform different actions depending on a condition. Example:

$$ f(x) = \left\lbrace\begin{array}{ll} \sin x, & 0\leq x\leq \pi\\ 0, & \hbox{otherwise} \end{array}\right. $$

A Python implementation of $f$ needs to test on the value of $x$ and branch into two computations:

In [32]:

```
from math import sin, pi
def f(x):
if 0 <= x <= pi:
return sin(x)
else:
return 0
print f(0.5)
print f(5*pi)
```

**if-else (the else block can be skipped):**

```
if condition:
<block of statements, executed if condition is True>
else:
<block of statements, executed if condition is False>
```

**Multiple if-else.**

```
if condition1:
<block of statements>
elif condition2:
<block of statements>
elif condition3:
<block of statements>
else:
<block of statements>
<next statement>
```

**A piecewisely defined function.**

$$ N(x) = \left\lbrace\begin{array}{ll} 0, & x < 0\\ x, & 0\leq x < 1\\ 2-x, & 1\leq x < 2\\ 0, & x \geq 2 \end{array}\right. $$

**Python implementation with multiple if-else-branching.**

In [33]:

```
def N(x):
if x < 0:
return 0
elif 0 <= x < 1:
return x
elif 1 <= x < 2:
return 2 - x
elif x >= 2:
return 0
```

**Common construction:**

```
if condition:
variable = value1
else:
variable = value2
```

**More compact syntax with one-line if-else:**

```
variable = (value1 if condition else value2)
```

Example:

In [34]:

```
def f(x):
return (sin(x) if 0 <= x <= 2*pi else 0)
```

In [35]:

```
def double(x): # some function
return 2*x
def test_double(): # associated test function
"""Call double(x) to check that it works."""
x = 4 # some chosen x value
expected = 8 # expected result from double(x)
computed = double(x)
success = computed == expected # boolean value: test passed?
msg = 'computed %s, expected %s' % (computed, expected)
assert success, msg
```

**Rules for test functions:**

name begins with

`test_`

no arguments

must have an

`assert success`

statement, where`success`

is`True`

if the test passed and`False`

otherwise (`assert success, msg`

prints`msg`

on failure)

The optional `msg`

parameter writes a message if the test fails.

In [36]:

```
def double(x): # some function
return 2*x
def test_double(): # associated test function
tol = 1E-14 # tolerance for float comparison
x_values = [3, 7, -2, 0, 4.5, 'hello']
expected_values = [6, 14, -4, 0, 9, 'hellohello']
for x, expected in zip(x_values, expected_values):
computed = double(x)
msg = '%s != %s' % (computed, expected)
assert abs(expected - computed) < tol, msg
```

A test function *will run silently* if all tests pass. If one test
above fails, `assert`

will raise an `AssertionError`

.

Easy to recognize where functions are verified

Test frameworks, like

`nose`

and`pytest`

, can automatically run*all*your test functions (in a folder tree) and report if any bugs have sneaked inThis is a very well established standard

```
Terminal> py.test -s .
Terminal> nosetests -s .
```

We recommend `py.test`

- it has superior output.

**Unit tests.**

A test function as `test_double()`

is often referred to as a *unit test*
since it tests a small unit (function) of a program. When all unit
tests work, the whole program is supposed to work.

Many find test functions to be a difficult topic

The idea

*is*simple: make problem where you know the answer, call the function, compare with the known answerJust write some test functions and it will be easy

The fact that a successful test function runs silently is annoying - can (during development) be convenient to insert some print statements so you realize that the statements are run

If tests:

```
if x < 0:
value = -1
elif x >= 0 and x <= 1:
value = x
else:
value = 1
```

User-defined functions:

In [37]:

```
def quadratic_polynomial(x, a, b, c):
value = a*x*x + b*x + c
derivative = 2*a*x + b
return value, derivative
# function call:
x = 1
p, dp = quadratic_polynomial(x, 2, 0.5, 1)
p, dp = quadratic_polynomial(x=x, a=-4, b=0.5, c=0)
```

Positional arguments must appear before keyword arguments:

In [38]:

```
def f(x, A=1, a=1, w=pi):
return A*exp(-a*x)*sin(w*x)
```

An integral

$$ \int_a^b f(x)dx $$

can be approximated by *Simpson's rule*:

$$ \begin{align*} \int_a^b f(x)dx \approx {b-a\over 3n}\biggl( & f(a) + f(b) + 4\sum_{i=1}^{n/2} f(a + (2i-1)h)\\ & + 2\sum_{i=1}^{n/2-1} f(a+2ih)\biggr) \end{align*} $$

Problem: make a function `Simpson(f, a, b, n=500)`

for
computing an integral of `f(x)`

by Simpson's rule.
Call `Simpson(...)`

for ${3\over2}\int_0^\pi\sin^3x dx$ (exact
value: 2) for $n=2,6,12,100,500$.

In [39]:

```
def Simpson(f, a, b, n=500):
"""
Return the approximation of the integral of f
from a to b using Simpson's rule with n intervals.
"""
h = (b - a)/float(n)
sum1 = 0
for i in range(1, n/2 + 1):
sum1 += f(a + (2*i-1)*h)
sum2 = 0
for i in range(1, n/2):
sum2 += f(a + 2*i*h)
integral = (b-a)/(3*n)*(f(a) + f(b) + 4*sum1 + 2*sum2)
return integral
```

In [40]:

```
def Simpson(f, a, b, n=500):
if a > b:
print 'Error: a=%g > b=%g' % (a, b)
return None
# Check that n is even
if n % 2 != 0:
print 'Error: n=%d is not an even integer!' % n
n = n+1 # make n even
h = (b - a)/float(n)
sum1 = 0
for i in range(1, n/2 + 1):
sum1 += f(a + (2*i-1)*h)
sum2 = 0
for i in range(1, n/2):
sum2 += f(a + 2*i*h)
integral = (b-a)/(3*n)*(f(a) + f(b) + 4*sum1 + 2*sum2)
return integral
```

In [41]:

```
def h(x):
return (3./2)*sin(x)**3
from math import sin, pi
def application():
print 'Integral of 1.5*sin^3 from 0 to pi:'
for n in 2, 6, 12, 100, 500:
approx = Simpson(h, 0, pi, n)
print 'n=%3d, approx=%18.15f, error=%9.2E' % \
(n, approx, 2-approx)
application()
```

Property of Simpson's rule: 2nd degree polynomials are integrated exactly!

In [42]:

```
def test_Simpson(): # rule: no arguments
"""Check that quadratic functions are integrated exactly."""
a = 1.5
b = 2.0
n = 8
g = lambda x: 3*x**2 - 7*x + 2.5 # test integrand
G = lambda x: x**3 - 3.5*x**2 + 2.5*x # integral of g
exact = G(b) - G(a)
approx = Simpson(g, a, b, n)
success = abs(exact - approx) < 1E-14 # tolerance for floats
msg = 'exact=%g, approx=%g' % (exact, approx)
assert success, msg
```

Can either call `test_Simpson()`

or run nose or pytest:

```
Terminal> nosetests -s Simpson.py
Terminal> py.test -s Simpson.py
...
Ran 1 test in 0.005s
OK
```