In [1]:
import pytest

Exercise 09.1 (checking data validity)

The Fibonacci series is valid only for $n \ge 0$. Add to the Fibonacci function in this notebook a check that raises an exception if $n < 0$. Try some invalid data cases to check that an exception is raised.

Optional: Use pytest to test that an exception is raised for some $n < 0$ cases.


In [4]:
def f(n): 
    "Compute the nth Fibonacci number using recursion"
    if n < 0:
        # Raise error if n is less than 0
        raise ValueError('Fibonacci number can be calculated only for n greater than or equal to 0')
    if n == 0:
        return 0  # This doesn't call f, so it breaks out of the recursion loop
    elif n == 1:
        return 1  # This doesn't call f, so it breaks out of the recursion loop
    else:
        return f(n - 1) + f(n - 2)  # This calls f for n-1 and n-2 (recursion), and returns the sum

# Test cases
print(f(4))
print(f(-1))


3
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-4-bad68ae06f70> in <module>()
     13 # Test cases
     14 print(f(4))
---> 15 print(f(-1))

<ipython-input-4-bad68ae06f70> in f(n)
      3     if n < 0:
      4         # Raise error if n is less than 0
----> 5         raise ValueError('Fibonacci number can be calculated only for n greater than or equal to 0')
      6     if n == 0:
      7         return 0  # This doesn't call f, so it breaks out of the recursion loop

ValueError: Fibonacci number can be calculated only for n greater than or equal to 0

Test exceptions with pytest:


In [5]:
def f(n):
    "Compute the nth Fibonacci number using recursion"
    if n < 0:
        # Raise error if n is less than 0
        raise ValueError('Fibonacci number can be calculated only for n greater than or equal to 0')
    if n == 0:
        return 0  # This doesn't call f, so it breaks out of the recursion loop
    elif n == 1:
        return 1  # This doesn't call f, so it breaks out of the recursion loop
    else:
        return f(n - 1) + f(n - 2)  # This calls f for n-1 and n-2 (recursion), and returns the sum

# Check that n < 0 raises a ValueError
with pytest.raises(ValueError):
    f(-1)
with pytest.raises(ValueError):
    f(-12)
with pytest.raises(ValueError):
    f(-20)

Exercise 09.2 (catching and dealing with exceptions)

For the loan interest question in Activity 01 that involved user-input, restructure that problem such that it checks the validity of the user input (principal and number of days). For invalid input your program should prompt the user to try again.


In [6]:
def calculate_interest():
    # Initial values
    official_rate = 0.0025
    rate_over = 0.0149
    
    try:
        # Get input from user
        loan_principal = float(input('What is your loan principal? '))
        
        #If loan principal is less than or equal to 0 raise an error
        if loan_principal <= 0:
            raise ValueError('Loan principal must be greater than 0')
        
        days_period = float(input('What is the period (in days) of your loan? '))
        
        #If the number of days is less than or equal to 0 raise an error
        if days_period <= 0:
            raise ValueError('Loan period must be greater than 0')
        
        # Calculate interest
        interest = (loan_principal
                    * ((official_rate + rate_over) / 365) # daily interest rate
                    * days_period)
        
        return interest
    
    except:
        # Handle exceptions
        print('Invalid value entered, try again')
        return calculate_interest()

calculate_interest()


What is your loan principal? -5
Invalid value entered, try again
What is your loan principal? 150000
What is the period (in days) of your loan? 0
Invalid value entered, try again
What is your loan principal? 150000
What is the period (in days) of your loan? 28
Out[6]:
200.21917808219177

Exercise 09.3 (raising exceptions)

Modify your program from the bisection exercise in Activity 04 to raise an error if the maximum number of iterations is exceeded. Reduce the maximum allowed iterations to test that an exception is raised.

Add any other checks on the input data that you think are appropriate.


In [7]:
def f(x):
    #return x**3 - 6*x**2 + 4*x + 12
    return x**2 + x - 20 # Roots = -5, 4

In [11]:
def compute_root(f, x0, x1, tol, max_it):
    """Computes the root of f between x0 and x1 using bisection,
        stops if the value of f at the root is under tol or if max_it is reached
        and returns the root, the value of f at the root and the number of iterations"""
    # If tolerance is less than 0 return an error
    if tol < 0:
        raise ValueError('Tolerance must be greater than or equal to 0')
    
    # If x0 or x1 is a root return it
    if f(x0) == 0:
        return x0, f(x0), 0
    if f(x1) == 0:
        return x1, f(x1), 0
    
    # If f(x0)*f(x1) the function has no solution in the interval, so return an error
    if f(x0)*f(x1) > 0:
        raise RuntimeError('There is no solution between x0 and x1')
    
    # Initialize iteration counter
    i = 0
    
    while True:
        # Increment counter
        i += 1
        # If max_it is passed return an error
        if i > max_it:
            raise RuntimeError('Maximum number of iterations exceeded')
        
        # Compute x_mid
        x_mid = (x0 + x1) / 2

        # Compute f for the three values
        f_0, f_1, f_mid = f(x0), f(x1), f(x_mid)

        # Check the value of f_0*f_mid to determine how to update the endpoints
        if f_0*f_mid < 0:
            x1 = x_mid
        else:
            x0 = x_mid
        
        # Check if f is under tol
        if abs(f_mid) < tol:
            return x_mid, f_mid, i

    # We don't need another return statement because if we pass max_it we return an error

# Test for the function f
x, f_x, num_it = compute_root(f, x0=3, x1=6, tol=1.0e-6, max_it=1000) # Ok

print('Approximate root:', x)
print('Value of f:', f_x)
print('Number of iterations:', num_it)

print('-----------------------------------------------------------')

x, f_x, num_it = compute_root(f, x0=3, x1=6, tol=1.0e-6, max_it=10) # Maximum iterations exceeded

x, f_x, num_it = compute_root(f, x0=3, x1=6, tol=-5, max_it=1000) # Tolerance less than zero

x, f_x, num_it = compute_root(f, x0=5, x1=6, tol=1.0e-6, max_it=1000) # No root in the interval


Approximate root: 3.9999999403953552
Value of f: -5.364417994258019e-07
Number of iterations: 24
-----------------------------------------------------------
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
<ipython-input-11-6b7b3f8a35f6> in <module>()
     58 #x, f_x, num_it = compute_root(f, x0=3, x1=6, tol=-5, max_it=1000) # Tolerance less than zero
     59 
---> 60 x, f_x, num_it = compute_root(f, x0=5, x1=6, tol=1.0e-6, max_it=1000) # No root in the interval

<ipython-input-11-6b7b3f8a35f6> in compute_root(f, x0, x1, tol, max_it)
     15     # If f(x0)*f(x1) the function has no solution in the interval, so return an error
     16     if f(x0)*f(x1) > 0:
---> 17         raise RuntimeError('There is no solution between x0 and x1')
     18 
     19     # Initialize iteration counter

RuntimeError: There is no solution between x0 and x1