Homework 4

Due Date: Tuesday, September 26th at 11:59 PM

Problem 1

The file circles.txt contains measurements of circle radii. Your task is to write a script that reports the average area of the circles. You will not use the numpy mean function. Here are the requirements:

  1. Open circles.txt, read in the data, and convert the data to floats.
  2. Write a function that computes the area of a circle.
  3. Write a function, called myave, that computes the average of the areas of the circles. At the very least, myave should accept the list of radii as one argument and the circle function that you wrote in step 2 as another argument. There are other ways of doing this task, but I want you to do it this way.
  4. Print out the result.

In [10]:
with open("circles.txt","r") as f:
    circles_text=f.read().split()

In [8]:
def mywave(radius):
    return (float(radius)*float(radius)*3.14)
def myave(func, radii_list):
    total_area=0
    num_area=0
    for radius in radii_list:
        total_area+=func(radius)
        num_area+=1
    return (total_area/num_area)

print(myave(mywave,circles_text))


3.194278912439731

Problem 2

The goal of this problem is to write a simple bank account withdraw system. The problem is based off of one in Structure and Interpretation of Computer Programs.

Instructions: Do each part in a different cell block and clearly label each part.

Part 1

Write a closure to make withdraws from a bank account. The outer function should be accept the initial balance as an argument (I'll refer to this argument as balance in this problem statement, but you can call it whatever you want). The inner function should accept the withdraw amount as an argument and return the new balance.

NOTE1: For this part, do not try to reassign balance in the inner function. Just see what happens when you return a new balance. You can store the new balance in a new name (call it new_bal if you want) or just return the new balance directly.

NOTE2: You may want to check for basic exceptions (e.g. attempting to withdraw more than the current balance).

Once you write your functions, demo them in your notebook as follows:

wd = make_withdraw(init_balance)
wd(withdraw_amount)
wd(new_withdraw_amount)

You should observe that this does not behave correctly. Why not?

Part 2

You can fix things up by updating balance within the inner function. But this won't work. Try it out and explain why it doesn't work. Try to use the language that we used in lecture. Hint: Python Execution Model.

Part 3

Now, make just one small change to your code from Part 2. Declare balance as a nonlocal variable using the nonlocal keyword. That is, before you get to the inner function, say nonlocal balance. Here's some information on the nonlocal statement: nonlocal.

Now test things out like you did in Part 1. It should be behaving correctly now.

Part 4

Finally, visualize your code with Python Tutor and embed your visualization in your notebook. Pay attention to the variable balance.


In [1]:
def make_withdraw(balance):
    def inner_func(withdraw):
        if (balance<withdraw):
            raise ValueError("balance is smaller than withdraw")
        return(balance-withdraw)
    return(inner_func)
initial_balance=10
withdraw_amount=2
new_withdraw_amount=4
wd=make_withdraw(initial_balance)
wd(withdraw_amount)
wd(new_withdraw_amount)


Out[1]:
6

In [2]:
def make_withdraw(balance):
    def inner_func(withdraw):
        p=balance-withdraw
        if (balance<withdraw):
            raise ValueError("balance is smaller than withdraw")
        balance=p
        return(p)
    return(inner_func)
initial_balance=10
withdraw_amount=2
new_withdraw_amount=4
wd=make_withdraw(initial_balance)
wd(withdraw_amount)
wd(new_withdraw_amount)


---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-2-88ece35f2e4a> in <module>()
     11 new_withdraw_amount=4
     12 wd=make_withdraw(initial_balance)
---> 13 wd(withdraw_amount)
     14 wd(new_withdraw_amount)

<ipython-input-2-88ece35f2e4a> in inner_func(withdraw)
      1 def make_withdraw(balance):
      2     def inner_func(withdraw):
----> 3         p=balance-withdraw
      4         if (balance<withdraw):
      5             raise ValueError("balance is smaller than withdraw")

UnboundLocalError: local variable 'balance' referenced before assignment

The reason is that balance refers to a immutable object. The scope of the object that balance binds to is within the function. After the function is called, the balance variable is skilled. So it won't affect the next call.


In [5]:
def make_withdraw(balance):   
    def inner_func(withdraw):
        nonlocal balance
        p=balance-withdraw
        if (balance<withdraw):
            raise ValueError("balance is smaller than withdraw")
        balance=p
        return(p)
    return(inner_func)
initial_balance=10
withdraw_amount=2
new_withdraw_amount=4
wd=make_withdraw(initial_balance)
wd(withdraw_amount)
wd(new_withdraw_amount)


Out[5]:
4

In [12]:
from IPython.display import HTML # Allows us to embed HTML into our notebook.
HTML('<iframe width="800" height="400" frameborder="0" src="http://pythontutor.com/visualize.html#code=def%20make_withdraw%28balance%29%3A%20%20%0A%20%20%20%20%0A%20%20%20%20def%20inner_func%28withdraw%29%3A%0A%20%20%20%20%20%20%20%20nonlocal%20balance%0A%20%20%20%20%20%20%20%20p%3Dbalance-withdraw%0A%20%20%20%20%20%20%20%20if%20%28balance%3Cwithdraw%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20raise%20ValueError%28%22balance%20is%20smaller%20than%20withdraw%22%29%0A%20%20%20%20%20%20%20%20balance%3Dp%0A%20%20%20%20%20%20%20%20return%28p%29%0A%20%20%20%20return%28inner_func%29%0Ainitial_balance%3D10%0Awithdraw_amount%3D2%0Anew_withdraw_amount%3D4%0Awd%3Dmake_withdraw%28initial_balance%29%0Awd%28withdraw_amount%29%0Awd%28new_withdraw_amount%29&cumulative=false&curInstr=0&heapPrimitives=false&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe>')


Out[12]:

Problem 3

Let's return to the data from Problem 1. Write two functions: 1.) The first function should return the average circle radius (you can re-use the one you already wrote if you'd like, but you might need to update it slightly for this problem) and 2.) The second function should just use numpy to compute the average.

Write a decorator to time the evaluation of each function. You can use the timing decorator from lecture.

Notes and Hints

  1. Be fair!
  2. To be as fair as possible, do the following:
    1. Create an areas list/array outside of your averaging functions. This means that you should do a loop over the radii you read in from circles.txt, compute the area from each point, and store that area in an array. Do you know why this is more fair? Also, try to not use append. Instead, preallocate space for your area list/array.
    2. Your my_ave function should accept your areas data as a list. Remember, to allocate a list you should do [0.0]*N: if you use such a construct your list will will be filled in with zeros.
    3. Your np_ave function should accept your areas data as a numpy array. To allocate a numpy array do areas_np = np.zeros(len(radii)).
    4. Now both functions are using the best data types possible for their tasks.

In [10]:
# First we write our timer function
import time
def timer(f):
    def inner(*args):
        t0 = time.time()
        output = f(*args)
        elapsed = time.time() - t0
        print("Time Elapsed", elapsed)
        return output
    return inner

In [19]:
import cmath
def my_ave(area_list):
    return myave(area_list)

In [20]:
import cmath
def np_ave(area_np):
    return myave(area_np)

In [21]:
import numpy as np
def mywave(radius):
    return (float(radius)*float(radius)*3.14)
def myave(area_list):
    total_area=0
    num_area=0
    for area in area_list:
        total_area+=area_list[num_area]
        num_area+=1
    return (total_area/num_area)

with open("circles.txt","r") as f:
    circles_text=f.read().split()

area_list=[0.0]*len(circles_text)
area_np=np.zeros(len(circles_text))
num_radius=0
for radius in circles_text:
    area_list[num_radius]=mywave(circles_text[num_radius])
    area_np[num_radius]=mywave(circles_text[num_radius])
    num_radius+=1
print(my_ave(area_list))
print(np_ave(area_np))


3.194278912439731
3.19427891244

In [22]:
timer1=timer(my_ave)
timer2=timer(np_ave)
print(timer1(area_list))
print(timer1(area_np))


Time Elapsed 0.00010538101196289062
3.194278912439731
Time Elapsed 0.00025916099548339844
3.19427891244

Problem 4

Write a decorator to check if a quantity returned from a function is positive. An exception should be raised if the quantity is not positive.

Write three functions and decorate them with your decorator:

  1. A function that returns the discriminant $\displaystyle d = b^{2} - 4ac$
  2. A function that computes the absolute value (you must write this yourself...do not use built-ins)
  3. A function of your own choosing.

Show that your decorator behaves correctly. That is, for each function, show two cases (where applicable):

  1. A case where positivity is not violated
  2. A case where positivity is violated

In [34]:
def positive_check(f):
    def inner(*args):
        if (f(*args)<0):
             raise ValueError("The result is negative")
        else:
            return True
    return inner

In [35]:
def function1(a,b,c):
    return(b*b-4*a*c)

In [36]:
def function2(a):
    if a>0:
        return a
    else:
        return -a

In [37]:
def function3(a):
    return (a+1)*(a-2)

In [38]:
check1=positive_check(function1)
check2=positive_check(function2)
check3=positive_check(function3)

print(check1(1,3,1))

print(check2(1))

print(check3(3))
print(check3(0))
print(check1(1,1,1))
print(check2(0))


True
True
True
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-38-404872a5058b> in <module>()
      8 
      9 print(check3(3))
---> 10 print(check3(0))
     11 print(check1(1,1,1))
     12 print(check2(0))

<ipython-input-34-391904024b51> in inner(*args)
      2     def inner(*args):
      3         if (f(*args)<0):
----> 4              raise ValueError("The result is negative")
      5         else:
      6             return True

ValueError: The result is negative

Problem 5

Coming soon...