CSCI 0050: More Python Notes, 7/24/19

A review of Homework 7: Warming up in Python

What questions came up?

  • For every function, list was not defined (when used as a type name)
  • Difficulty using testing (testlight.py)
  • How to nest functions?

A worked example, count_votes_for()

From the course page:

  • Write a function count_votes_for that takes a name and a list of votes, and returns the number of votes that match the given name.
In [6]:
from typing import List
from testlight import * # testlight.py must be in the same folder as this python script

def count_votes_for(who: str, votes: List[str]) -> int:
    """count how many times who is in the votes list"""
    total = 0
    for name in votes:
        if name == who:
            total = total + 1
    return total
    
def test_count_votes():
    test("no votes", count_votes_for("Kathi", []), 0)
    test("none for candidates", count_votes_for("Kathi", ["Will", "Will"]), 0)
    
# run our tests
test_count_votes()
Test 'no votes' passed
Test 'none for candidates' passed

How do I comment out a number of lines of code?

Tl;dr - There is not a concept of a block comment, like in Pyret. You may be able to turn your code into a string using """your code here""". But this does not work if you have strings containted in your code.

However since we are working with PyCharm, there is a feature that allows you to comment out a number of lines of code. To add or remove a "block comment", do one of the following:

  • Highlight the code you want to comment out and either:

    • On the main menu, choose Code. | Comment with Block Comment.
    • Or Press Ctrl + Shift + / .

    These commands will individually comment out selected lines of code.

A trickier situation using return, rewriting test_record_votes()

What happens if your function doesn't return an answer?

You should use the function on one line, then test that you observe the expected change in the test expression. An example of this is below.

We also noted that, if we created an empty votesList outside the test_record_votes() function, running out tests twice could cause them to fail. This is becuase changes in data that occured during tests would be repeated and recorded.

In [13]:
def record_vote(votes: List[str], name: str):
    """add name to list of votes"""
    votes.append(name)
   

def test_record_votes():
    votesList = [] # set up test data inside the test function
    record_vote(votesList, "Will")
    test("vote for will", "Will" in votesList, True)
    test("no votes for Kathi", "Kathi" not in votesList, True)
    record_vote(votesList, "Kathi")
    test("check Kathi", "Kathi" in votesList, True)
    
# run our tests
test_record_votes()
Test 'vote for will' passed
Test 'no votes for Kathi' passed
Test 'check Kathi' passed

Unexpected behavior inside our testing block

In [15]:
def record_vote(votes: List[str], name: str):
    """add name to list of votes"""
    votes.append(name)
   
votesList = ["A", "B", "C"]

def test_record_votes():
    votesList = [] # set up test data inside the test function
    record_vote(votesList, "Will")
    test("vote for will", "Will" in votesList, True)
    test("vote count for will", count_votes_for("Will", votesList), 1)
    test("no votes for Kathi", "Kathi" not in votesList, True)
    record_vote(votesList, "Kathi")
    test("check Kathi", "Kathi" in votesList, True)
    
# run our tests
test_record_votes()

# print our votesList, expecting ["Will", "Kathi"]
print(votesList)
Test 'vote for will' passed
Test 'vote count for will' passed
Test 'no votes for Kathi' passed
Test 'check Kathi' passed
['A', 'B', 'C']

Uh oh. Why are we getting votesList = ["A", "B", "C"] when we changed our data in test_record_votes()?

Lets look at the Environment and the Heap:

Environment (Known Names)

votesList => loc 1000
test_record_votes => function...

Python then grabs 4 'slots', and fills them in.

Python Heap

label slot
1000 "A"
1001 "B"
1002 "C"
1003

When we call test_record_votes(), a temporary area gets set up in the enviornment.

Environment (Known Names)

votesList => loc 1000
test_record_votes => function...
----------(temp area)--------
votesList => loc 1004

Python then grabs an additional 4 'slots' and starts filling out the changes we made inside the test_record_votes() function.

Python Heap

label slot
1000 "A"
1001 "B"
1002 "C"
1003
1004 "Will"
1005 "Kathi"
1006
1007

But once the call to test_record_votes() is finished, the temporary area disappears (e.g. votesList => loc 1004) and we are left with

Environment (Known Names)

votesList => loc 1000
test_record_votes => function...

Python Heap

label slot
1000 "A"
1001 "B"
1002 "C"
1003

Using loops

Let's say we want to count how many characters are in a word.

In [1]:
def count_z(words: List[str]) -> int:
    """count how many z are in the list of words"""
    total = 0
    for w in words:
        for c in w:
            if c == "z":
                total = total + 1
    return total         
    
def test_z():
    test("z", count_z(["pizza", "zebra", "Wednesday"]), 3)
    
test_z()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-1-0e9e09ff13c1> in <module>
----> 1 def count_z(words: List[str]) -> int:
      2     """count how many z are in the list of words"""
      3     total = 0
      4     for w in words:
      5         for c in w:

NameError: name 'List' is not defined

Creating data block like structures in Python

What if we wanted to indicate a position in Python? We did a similar exercise in Pyret when we created a date() data type. We can achieve the same effect in Python using the @dataclass notation.

The @dataclass command is part of a different paradigm of programming called object-oriented programming. You might be familiar with some concepts of this if you have ever programmed with Java. If you're interested in learning more about the similarities and differences, you can read more about it here.

In [2]:
from dataclasses import dataclass

@dataclass
class Posn:
    x: int
    y: int
        
Posn(3, 4)
Out[2]:
Posn(x=3, y=4)

Extending the @dataclass to make a ToDo List

We might think about how we can use this idea of a @dataclass to make a ToDo list. We might also want to search our ToDoList by description. We can do this as so:

In [28]:
from datetime import date
from typing import List
# set up ToDoTiem data

# make a short (3-item) ToDoList

# write a function find-items that takes search string and a list of
# ToDoItem and returns list of items whose descr contains search string

# define a class ToDoItem
@dataclass
class ToDoItem:
    descr: str
    due: date
    tags: List[str]

# create and example todo list
MyTD = [ToDoItem("buy milk", date(2019, 7, 27), ["shopping", "home"]),
        ToDoItem("grade hwk", date(2019, 7, 27), ["teaching"]),
        ToDoItem("meet students", date(2019, 7, 26), ["research"])]

# We can do this three ways: using a lambda, using a helper function, and using
# a for look

# 1. using a lambda
def find_items_lambda(term: str, TDL: List[ToDoItem]) -> List[ToDoItem]:
    """consumes a term and a list of todoitems, returns todoitems that contain
       the term in their description"""
    return list(filter(lambda item: term in item.descr, TDL))

# 2. using a helper function
def find_items_w_helper(term: str, TDL: List[ToDoItem]) -> List[ToDoItem]:
    """consumes a term and a list of todoitems, returns todoitems that contain
       the term in their description"""
    def has_descr(item: ToDoItem): return term in item.descr
    return list(filter(has_descr, TDL))

# 3. using a for loop
def find_items_for_loop(term: str, TDL: List[ToDoItem]) -> List[ToDoItem]:
    matches = []
    for item in TDL:
        if term in item.descr:
            matches.append(item)
    return matches

# look for milk in MyTD
print(find_items_lambda("milk", MyTD))
print(find_items_w_helper("milk", MyTD))
print(find_items_for_loop("milk", MyTD))

# are they all the same? Yes.
print(find_items_lambda("milk", MyTD) == 
      find_items_w_helper("milk", MyTD) ==
      find_items_for_loop("milk", MyTD))
[ToDoItem(descr='buy milk', due=datetime.date(2019, 7, 27), tags=['shopping', 'home'])]
[ToDoItem(descr='buy milk', due=datetime.date(2019, 7, 27), tags=['shopping', 'home'])]
[ToDoItem(descr='buy milk', due=datetime.date(2019, 7, 27), tags=['shopping', 'home'])]
True

Removing an item from our ToDo List

Let's say we want to remove an item. We can write a function that removes the first occurance that matches the description we provide.

In [33]:
MyTD = [ToDoItem("buy milk", date(2019, 7, 27), ["shopping", "home"]),
        ToDoItem("grade hwk", date(2019, 7, 27), ["teaching"]),
        ToDoItem("meet students", date(2019, 7, 26), ["research"])]

def rem_item(des: str, TDL: List[ToDoItem]) -> List[ToDoItem]:
    """remove item with given descr from the ToDoList, it it is there"""
    item_to_remove = find_items(des, TDL)[0]
    TDL.remove(item_to_remove)
    return MyTD

rem_item("milk", MyTD)
Out[33]:
[ToDoItem(descr='grade hwk', due=datetime.date(2019, 7, 27), tags=['teaching']),
 ToDoItem(descr='meet students', due=datetime.date(2019, 7, 26), tags=['research'])]

Remove all matching occurances from list

Now what if we wanted to remove all matching occurances of the description we provided? We can rewrite our function above to do so:

In [34]:
MyTD = [ToDoItem("buy milk", date(2019, 7, 27), ["shopping", "home"]),
        ToDoItem("grade hwk", date(2019, 7, 27), ["teaching"]),
        ToDoItem("meet students", date(2019, 7, 26), ["research"])]

def rem_item_all(des: str, TDL: List[ToDoItem]) -> List[ToDoItem]:
    """remove items with given descr from the ToDoList, if it is there"""
    list_of_matches = find_items(des, TDL)
    for item_to_remove in list_of_matches:
        TDL.remove(item_to_remove)
    return TDL

rem_item_all("e", MyTD)
Out[34]:
[ToDoItem(descr='buy milk', due=datetime.date(2019, 7, 27), tags=['shopping', 'home'])]

Updating fields of elements in a list

Now what if we wanted to update the due date on an item with a specific description? We'll assume, for the moment, there there will be one and only one element that fits the description we provide.

NB: This is actually a function that Will has to write to update our grading system whenever anyone gets an extension.

There were serveral ways the class thought about doing this, which are shown below:

  1. Using map
  2. Remove and add a new ToDoItem
  3. Somehow change the date in the existing item
  4. Combine map and filter
In [47]:
MyTD = [ToDoItem("buy milk", date(2019, 7, 27), ["shopping", "home"]),
        ToDoItem("grade hwk", date(2019, 7, 27), ["teaching"]),
        ToDoItem("meet students", date(2019, 7, 26), ["research"])]

def update_duedate_1(des: str, new_date: date, TDL: List[ToDoItem]) -> List[ToDoItem]:
    """change the due date on the item with given descr"""
    def update_item(item: ToDoItem) -> ToDoItem:
        if des in item.descr:
            return ToDoItem(des, new_date, item.tags)
        else:
            return item
    return list(map(update_item, TDL))

print("Before update:\n")
print(MyTD)

# lets suppose I don't have to buy milk until the new year
print("\n After update:")
update_duedate_1("milk", date(2020, 1, 1), MyTD)
Before update:

[ToDoItem(descr='buy milk', due=datetime.date(2019, 7, 27), tags=['shopping', 'home']), ToDoItem(descr='grade hwk', due=datetime.date(2019, 7, 27), tags=['teaching']), ToDoItem(descr='meet students', due=datetime.date(2019, 7, 26), tags=['research'])]

 After update:
Out[47]:
[ToDoItem(descr='milk', due=datetime.date(2020, 1, 1), tags=['shopping', 'home']),
 ToDoItem(descr='grade hwk', due=datetime.date(2019, 7, 27), tags=['teaching']),
 ToDoItem(descr='meet students', due=datetime.date(2019, 7, 26), tags=['research'])]
In [48]:
MyTD = [ToDoItem("buy milk", date(2019, 7, 27), ["shopping", "home"]),
        ToDoItem("grade hwk", date(2019, 7, 27), ["teaching"]),
        ToDoItem("meet students", date(2019, 7, 26), ["research"])]

def update_duedate_2(des: str, new_date: date, TDL: List[ToDoItem]) -> List[ToDoItem]:
    """change the due date on the item with given descr"""
    item = find_items_lambda(des, TDL)[0]
    TDL.remove(item)
    TDL.append(ToDoItem(des, new_date, item.tags))
    return TDL

print("Before update:\n")
print(MyTD)
# lets suppose I don't have to buy milk until the new year
print("\n After update:")
update_duedate_2("milk", date(2020, 1, 1), MyTD)
Before update:

[ToDoItem(descr='buy milk', due=datetime.date(2019, 7, 27), tags=['shopping', 'home']), ToDoItem(descr='grade hwk', due=datetime.date(2019, 7, 27), tags=['teaching']), ToDoItem(descr='meet students', due=datetime.date(2019, 7, 26), tags=['research'])]

 After update:
Out[48]:
[ToDoItem(descr='grade hwk', due=datetime.date(2019, 7, 27), tags=['teaching']),
 ToDoItem(descr='meet students', due=datetime.date(2019, 7, 26), tags=['research']),
 ToDoItem(descr='milk', due=datetime.date(2020, 1, 1), tags=['shopping', 'home'])]
In [49]:
MyTD = [ToDoItem("buy milk", date(2019, 7, 27), ["shopping", "home"]),
        ToDoItem("grade hwk", date(2019, 7, 27), ["teaching"]),
        ToDoItem("meet students", date(2019, 7, 26), ["research"])]

def update_duedate_3(des: str, new_date: date, TDL: List[ToDoItem]) -> List[ToDoItem]:
    """change the due date on the item with given descr"""
    item = find_items(des, TDL)[0]
    #print(item)
    item.due = new_date
    #print(item)
    #print(TDL)
    return TDL

print("Before update:\n")
print(MyTD)

# lets suppose I don't have to buy milk until the new year
print("\n After update:")
update_duedate_3("milk", date(2020, 1, 1), MyTD)
Before update:

[ToDoItem(descr='buy milk', due=datetime.date(2019, 7, 27), tags=['shopping', 'home']), ToDoItem(descr='grade hwk', due=datetime.date(2019, 7, 27), tags=['teaching']), ToDoItem(descr='meet students', due=datetime.date(2019, 7, 26), tags=['research'])]

 After update:
Out[49]:
[ToDoItem(descr='buy milk', due=datetime.date(2020, 1, 1), tags=['shopping', 'home']),
 ToDoItem(descr='grade hwk', due=datetime.date(2019, 7, 27), tags=['teaching']),
 ToDoItem(descr='meet students', due=datetime.date(2019, 7, 26), tags=['research'])]

These three approaches are different in very subtle ways. We will see next class :)