Functions and Branching¶

da PowerShell -- jupyter nbconvert --to html notebook.ipynb¶

Programming with Functions¶

$$\large A=P\left( 1+\left(\frac{r}{100}\right) \right)^n $$
In [5]:
def amount(n):
    P = 100
    r = 5.0
    return P*(1+r/100)**n
year1 = 7
a1 = amount(year1) # call
print(a1)
140.71004226562505
In [7]:
a_list = [amount(year) for year in range(8)] #multiple calls
print(a_list)
[100.0, 105.0, 110.25, 115.76250000000002, 121.55062500000003, 127.62815625000003, 134.00956406250003, 140.71004226562505]

Function Arguments and Local Variables¶

In [12]:
def amount(P, r, n):
    return P*(1+r/100.0)**n

# sample calls:
a1 = amount(100, 5.0, 10)
a2 = amount(10, r= 3.0, n=6)
a3 = amount(r= 4, n = 2, P=100)

Arguments can be positional arguments or keyword arguments¶

Arguments passed without specifying the name are called positional arguments
Arguments passed including the name are called keyword arguments.

Arguments passed to a function, as well as variables we define inside the function, become local variables.
global variables
are also accessible inside a function, just as everywhere else in the code.

In [33]:
P = 100
r = 4.0

def amount(n):
    global r
    r = 5.0
    return P*(1+r/100)**n

print(amount(n=7)) 
print(r)
140.71004226562505
5.0
In [43]:
P = 100
r = 6.0

def amount(r, n):
    r = r - 1.0
    a = P*(1+r/100)**n 
    return a, r

a0, r = amount(r,n=7) 
print(a0, r)
140.71004226562505 5.0

Multiple return values are returned as a tuple¶

In [50]:
def yfunc(t, v0):
    g = 9.81
    y = v0*t - 0.5*g*t**2 
    dydt = v0 - g*t 
    return y, dydt
pos_vel = yfunc(0.6, 3)
print(pos_vel) 
print(type(pos_vel))
(0.034199999999999786, -2.886)
<class 'tuple'>

Example: A function to compute a sum¶

$$\large L(x;n)=\sum_{i=1}^n \frac{x^i}{i} $$
In [56]:
def L(x,n):
    s = 0
    for i in range(1,n+1):
        s += x**i/i
    return s
#example use
x = 0.5
from math import log
print(L(x, 3), L(x, 10), -log(1-x))
0.6666666666666666 0.6930648561507935 0.6931471805599453
In [62]:
from math import log

def L2(x, n): 
    s = 0
    for i in range(1,n+1):
        s += x**i/i 
    value_of_sum = s
    error = -log(1-x) - value_of_sum 
    return value_of_sum, error

# typical call: 
x = 0.8; n = 20 
value, error = L2(x, n)
print(value, error)
1.6075458671216805 0.0018920453124200431

A function does not need a return statement¶

In [65]:
def somefunc(obj): 
    print(obj)

return_value = somefunc(3.4)
3.4
In [75]:
def table(x):
    print(f'x={x}, -ln(1-x)={-log(1-x)}')

for n in [1, 3, 10, 100]: 
    value, error = L2(x, n)
    print(f'n={n:4d} approx: {value:7.6f}, error: {error:7.6f}')

table(0.5)
n=   1 approx: 0.800000, error: 0.809438
n=   3 approx: 1.290667, error: 0.318771
n=  10 approx: 1.578875, error: 0.030563
n= 100 approx: 1.609438, error: 0.000000
x=0.5, -ln(1-x)=0.6931471805599453

Default Arguments and Doc Strings¶

In [94]:
def somefunc(arg1, arg2, kwarg1=True, kwarg2=0): 
    print(arg1, arg2, kwarg1, kwarg2)

somefunc('Hello', [1,2]) # drop kwarg1 and kwarg2
somefunc('Hello', [1,2], 'Hi')
somefunc('Hello', [1,2], 'Hi', 6)
somefunc('Hello', [1,2], kwarg2='Hi') #kwarg2
somefunc('Hello', [1,2], kwarg2='Hi', kwarg1=6) 
Hello [1, 2] True 0
Hello [1, 2] Hi 0
Hello [1, 2] Hi 6
Hello [1, 2] True Hi
Hello [1, 2] 6 Hi
$$\large y(t)=v_0-\frac{1}{2}gt^2 $$
In [97]:
def yfunc(t, v0=5, g=9.81):
    y = v0*t - 0.5*g*t**2 
    dydt = v0 - g*t 
    return y, dydt

#example calls:
y1, dy1 = yfunc(0.2)
y2, dy2 = yfunc(0.2,v0=7.5) 
y3, dy3 = yfunc(0.2,7.5,10.0)

Documentation of Python functions¶

In [100]:
def amount(P, r, n):
    """Compute the growth of an investment over time."""
    a = P*(1+r/100.0)**n 
    return a

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

help(line)
Help on function line in module __main__:

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).

If-Tests for Branching the Program Flow¶

\begin{cases}¶

$$\large f(x) = \begin{cases} \sin x, & 0 \le x \le \pi \\ 0, & \text{altrove} \end{cases} $$
In [108]:
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))
0.479425538604203
0
$$\large N(x) = \begin{cases} 0, & x \lt x \le 0 \\ x, & 0 \le x \lt 1 \\ 2-x, & 1 \le x \lt 2 \\ 0, & 1 x \ge 2 \end{cases} $$
In [113]:
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

Inline if-tests for shorter code¶

In [116]:
def f(x):
    return (sin(x) if 0 <= x <= pi else 0)

Functions as Arguments to Functions¶

Arguments to Python functions can be any Python object, including another function.¶

$$\large f"(x)\simeq \frac{f(x-h)-2f(x)+f(x+h)}{h^2} $$
In [121]:
def diff2(f, x, h=1E-6):
    r = (f(x-h) - 2*f(x) + f(x+h))/float(h*h)
    return r

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

print(diff2(f,1.5))
1.999733711954832

Using the keyword lambda, we can define our f on a single line, as follows:¶

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

f = lambda x: x**2 - 1

somefunc = lambda a1, a2, a3: a1+a2+a3

df2 = diff2(lambda x: x**2-1,1.5) 
print(df2)
1.999733711954832

Solving Equations with Python Functions¶

Finding roots on an interval with the bisection method¶

In [149]:
from math import exp

def bisection(f, a, b, tol=1e-3):
    if f(a)*f(b) > 0:
        print(f'No roots or more than one root in [{a},{b}]')
        return None

    m = (a + b) / 2
    while abs(f(m)) > tol:
        if f(a)*f(m) < 0:
            b = m
        else:
            a = m
        m = (a + b) / 2

    return m

#call the method for f(x)= x**2-4*x+exp(-x) 
f = lambda x: x**2 - 4*x + exp(-x)

sol = bisection(f, -0.5, 1, 1e-6)
print(f'x = {sol:g} is an approximate root, f({sol:g}) = {f(sol):g}')
x = 0.213348 is an approximate root, f(0.213348) = -3.41372e-07

Newton’s method gives faster convergence¶

Il metodo di bisezione converge piuttosto lentamente, mentre altri metodi sono molto più diffusi per la risoluzione di equazioni non lineari. In particolare, numerose varianti del metodo di Newton sono ampiamente utilizzate nella pratica. Il metodo di Newton si basa su una linearizzazione locale della funzione non lineare f(x). Partendo da un'ipotesi iniziale x0, sostituiamo f(x) con una funzione lineare g(x) che soddisfi g(x) ≈ f(x) in un piccolo intervallo intorno a x0. Quindi, risolviamo l'equazione g(x) = 0 per trovare un'ipotesi aggiornata x1 e ripetiamo il processo di linearizzazione attorno a quel punto. L'applicazione ripetuta di questi passaggi converge rapidamente verso la soluzione vera, a condizione che l'ipotesi iniziale x0 sia sufficientemente vicina.

In matematica, un passaggio dell'algoritmo si presenta come¶

$$\large x_{n+1}=z_n-\frac{f(x_n)}{f'(x_n)} $$
In [155]:
from math import exp

def Newton(f, dfdx, x0, tol= 1e-3):
    f0 = f(x0)
    while abs(f0) > tol:
        x1 = x0 - f0/dfdx(x0) 
        x0 = x1
        f0 = f(x0)
    return x0

#call the method for f(x)= x**2-4*x+exp(-x) 
f = lambda x: x**2-4*x+exp(-x)
dfdx = lambda x: 2*x-4-exp(-x)

sol = Newton(f,dfdx,0,1e-6)
print(f'x = {sol:g} is an approximate root, f({sol:g}) = {f(sol):g}')
x = 0.213348 is an approximate root, f(0.213348) = 4.52213e-09

porre un limite al numero di iterazioni¶

In [158]:
from math import exp

def Newton2(f, dfdx, x0, max_it=20, tol= 1e-3):
    f0 = f(x0)
    iter = 0
    while abs(f0) > tol and iter < max_it:
        x1 = x0 - f0/dfdx(x0) 
        x0 = x1
        f0 = f(x0)
        iter += 1

    converged = iter < max_it 
    return x0, converged, iter

#call the method for f(x)= x**2-4*x+exp(-x) 
f = lambda x: x**2-4*x+exp(-x)
dfdx = lambda x: 2*x-4-exp(-x)

sol, converged, iter = Newton2(f,dfdx,0,tol=1e-3)

if converged:
    print(f'Newtons method converged in {iter} iterations')
else:
    print(f'The method did not converge')
Newtons method converged in 2 iterations

Il metodo di Newton solitamente converge molto più velocemente del metodo di bisezione, ma presenta lo svantaggio che la funzione f deve essere differenziata manualmente. Nel Capitolo 8 vedremo alcuni esempi di come questo passaggio può essere evitato.

Writing Test Functions to Verify our Programs¶

Molti programmatori portano questo approccio un passo oltre e scrivono il test prima di scrivere la funzione vera e propria. Questo approccio viene spesso definito sviluppo guidato dai test ed è un metodo sempre più popolare per lo sviluppo software.

Per una data funzione, che spesso accetta uno o più argomenti, scegliamo argomenti tali da calcolare manualmente il risultato della funzione.

In [172]:
def double(x): # some function
    return 2*x

def test_double(): # associated test function
    x = 4 # some chosen x value
    expected = 8 # expected result from double(x) 
    computed = double(x)
    global success 
    success = computed == expected # Boolean value: test passed? 
    msg = f'computed {computed}, expected {expected}'
    assert success, msg

test_double()
success
Out[172]:
True

La funzione di test deve avere almeno una istruzione di tipo assert success, dove success è una variabile o espressione booleana, che è true se il test è stato superato e false in caso contrario. Possiamo includere più di una istruzione assert se lo desideriamo, ma ne serve sempre almeno una.

  • La funzione di test non deve accettare argomenti. La funzione da testare

in genere verrà chiamata con uno o più argomenti, ma questi dovrebbero essere definiti come variabili locali allo interno della funzione di test.

  • Il nome della funzione dovrebbe essere sempre test_, seguito dal nome

della funzione che vogliamo testare. Seguire questa convenzione è utile perché rende ovvio a chiunque legga il codice che la funzione è una funzione di test ed è anche utilizzata da strumenti che possono eseguire automaticamente tutte le funzioni di test in un dato file o directory.

A test function can include multiple tests¶

In [177]:
from math import sin, pi

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

def test_f():
    x1, exp1 = -1.0, 0.0
    x2, exp2 = pi/2, 1.0 
    x3, exp3 = 3.5, 0.0
    tol = 1e-10
    assert abs(f(x1)-exp1) < tol, f'Failed for x = {x1}' 
    assert abs(f(x2)-exp2) < tol, f'Failed for x = {x2}' 
    assert abs(f(x3)-exp3) < tol, f'Failed for x = {x3}'

tol deve essere abbastanza maggiore di $10^{-16}$ che è il limite della macchina¶

Se vogliamo compattare il codice usiamo list e un loop for¶

In [181]:
from math import sin, pi
def f(x):
    if 0 <= x <= pi:
        return sin(x)
    else:
        return 0

def test_f():
    x_vals = [-1, pi/2, 3.5]
    exp_vals = [0.0, 1.0, 0.0]
    tol = 1e-10
    for x, exp in zip(x_vals, exp_vals):
        assert abs(f(x)-exp) < tol, \
        f'Failed for x = {x}, expected {exp}, but got {f(x)}'

Python tools for automatic testing¶

Un vantaggio del seguire la convenzione di denominazione per le funzioni di test definita sopra è che esistono strumenti che possono essere utilizzati per eseguire automaticamente tutte le funzioni di test in un file o in una cartella e segnalare eventuali bug nel codice. L'uso di tali strumenti di test automatici è essenziale in progetti di sviluppo più ampi con più persone che lavorano sullo stesso codice, ma può essere molto utile anche per i propri progetti. Lo strumento consigliato e più utilizzato si chiama pytest o py.test, dove pytest è semplicemente il nuovo nome di py.test. Possiamo eseguire pytest dalla finestra del terminale e passargli come argomento un nome di file o di cartella, come in

In [ ]:
Terminal> pytest .
Terminal> pytest my_python_project.py

Se gli passiamo un nome di file, pytest cercherà funzioni in questo file con un nome che inizia con test_, come specificato dalla convenzione di denominazione di cui sopra. Tutte queste funzioni saranno identificate come funzioni di test e chiamate da pytest, indipendentemente dal fatto che vengano effettivamente chiamate da altre parti del codice. Dopo l'esecuzione, pytest stamperà un breve riepilogo di quanti test ha trovato, superato e fallito.

Per progetti software più grandi, potrebbe essere più opportuno fornire un nome di directory come argomento a pytest, come nella prima riga sopra. In questo caso, lo strumento cercherà nella directory specificata (in questo caso ., la directory in cui ci troviamo attualmente) e in tutte le sue sottodirectory i file Python con nomi che iniziano o terminano con test (ad esempio, test_math.py, math_test.py, ecc.).

In progetti software di grandi dimensioni in genere contengono migliaia di funzioni di test, ed è molto comodo raccoglierle in un file separato e utilizzare strumenti automatici come pytest. Per i programmi più piccoli che scriviamo in questo corso, può essere altrettanto semplice scrivere le funzioni di test nello stesso file delle funzioni in fase di test.

È importante ricordare che le funzioni di test vengono eseguite silenziosamente se il test supera; ovvero, otteniamo un output solo in caso di errore di asserzione, altrimenti non viene visualizzato nulla sullo schermo. Quando si utilizza pytest, viene sempre fornito un riepilogo che specifica quanti test sono stati eseguiti, ma se includiamo le chiamate alle funzioni di test direttamente nel file .py ed eseguiamo questo file normalmente, non ci sarà nessun output se il test supera. Questo può creare confusione e a volte ci si chiede se il test sia stato effettivamente chiamato. Quando si scrive una funzione di test per la prima volta, può essere utile includere un'istruzione di stampa all'interno della funzione, semplicemente per verificare che la funzione venga effettivamente chiamata. Questa istruzione dovrebbe essere rimossa una volta che si è certi che la funzione funziona correttamente e ci si è abituati al funzionamento delle funzioni di test.

In [ ]: