Classes and Iterators
Warm-up Exercise: Write a class Journal
to keep a record of journal entries. We want an .add()
method to add an entry (that should be a string) and we want the str()
function to return each entry on a new line, prefixed by the date it was added. The date.today()
function from the datetime
module will help here.
import datetime as dt
class Journal:
def __init__(self):
self.entries = []
def add(self, entry):
self.entries.append((dt.date.today(), entry))
def __str__(self):
return '\n'.join([f"{str(e[0])}: {e[1]}" for e in self.entries])
j = Journal()
j.add("Today was a good day :)")
j.add("The day is still good!")
print(j)
Exercise 1: Write a class Student
that allows us to store important information about a student. Specifically we want to be able to create a student with a name and id number. We want to be able to change the name but not the id number, but we want to be able to retrieve both. When str()
is called on a student it should return the name and id number.
(solution below)
.
.
.
.
.
.
.
.
class Student:
def __init__(self, name, id_num):
self.__name = name
self.__id = id_num
def set_name(self, name):
self.__name = name
def get_name(self):
return self.__name
def get_id(self):
return self.__id
def __str__(self):
return f"{self.__name} (id #{self.__id})"
s1 = Student('Robert', 14)
print(s1)
s1.set_name('Bob')
print(s1)
Exercise 2: Modify the previous Student
class. We want to implement the magic method __lt__
so that we can sort a list of students by name and then by ID number. (That is, Student A < Student B is A's name is alphabetically before Student B's or if they have the same name but Student A has a lower ID number.)
Note: .__lt__(self, other)
corresponds to self < other
and returns True
whenever this is true. This is the only method you need to be able to sort a list of your class's objects.
(solution below)
.
.
.
.
.
.
.
.
class Student:
def __init__(self, name, id_num):
self.__name = name
self.__id = id_num
def change_name(self, name):
self.__name = name
def set_name(self):
return self.__name
def get_id(self):
return self.__id
def __str__(self):
return f"{self.__name} (id #{self.__id})"
def __lt__(self, other):
if self.__name < other.__name:
return True
elif self.__name == other.__name:
return self.__id < other.__id
else:
return False
# for testing
list = [Student('Alice', 12), Student('Charlie', 6), Student('Bob', 3), Student('Alice', 4)]
list.sort()
print([str(s) for s in list])
There are also __gt__
, __le__
, __ge__
, __eq__
, and __ne__
which correspond to >, <=, >=, ==, and !=, respectively. If you implement __eq__
then you don't need to implement __ne__
(but not vice-versa). All of the rest must be implemented separately (or you can use a decorator, which we haven't learned about yet). You only need to implement __gt__
or __lt__
to be able to sort a list of your class's objects.
Exercise 3: Write a class CheatingCoin
which allows you to create a coin that flips normally except every n times it always comes up heads.
That is, you can let coin = CheatingCoin(n)
and call coin.flip()
. Every time you call coin.flip()
you have a 50-50 chance of True
or False
, except that every n-th time it is guaranteed to be True
.
(solution below)
.
.
.
.
.
.
.
.
import random
class CheatingCoin:
def __init__(self, n):
self.__flips = 0
self.__cheat_flip = n
def flip(self):
self.__flips += 1
if self.__flips == self.__cheat_flip:
self.__flips = 0
return True
else:
return random.randint(0,1) == 1
coin = CheatingCoin(3)
print(coin.flip())
print(coin.flip())
print(coin.flip())
We can modify the above class to allow us to flip and check the result of a CheatingCoin
like this:
if coin:
# executes if the flip was heads/True
print("Heads!")
else:
# executes if the flip was tails/False
print("Tails!")
by implementing the __bool__
method. Because CheatingCoin
was already defined we can do a neat trick where we just set this identifier to a lambda expression (in Python functions are just a type of value, so we can do this!). This isn't great practice, but it's kind of cool and worth seeing. This does the same as just writing the method with the header def __bool__(self):
to begin with, which is what you should normally do in this type of situation.
CheatingCoin.__bool__ = lambda self : self.flip() # normally just define __bool__ with the class
if coin:
# executes if the flip was heads/True
print("Heads!")
else:
# executes if the flip was tails/False
print("Tails!")
Exercise 4: Write a function SortedList
which allows us to have a list that we always keep sorted. You should implement the following methods:
add(self, e)
- a normal method to add a new element e
to the list
__add__(self, other)
- a magic method for +, will create a new SortedList
with all the elements in self
and other
__len__(self)
- a magic method for len()
, returns the length of the list
__contains__(self, e)
- a magic method for in
, returns if the element e
is in the list or not
getList(self)
- returns a (sorted) list containing all of the elements we have
Make sure that no one can modify the values the SortedList
contains without calling the add()
method.
(solution below)
.
.
.
.
.
.
.
.
class SortedList:
def __init__(self):
self.__list = []
def add(self, e):
self.__list.append(e)
self.__list.sort() # there are more efficient ways but this works
def getList(self):
return self.__list.copy() # if we don't copy, they could modify the list itself
def __add__(self, other):
new_l = SortedList();
for e in self.__list:
new_l.add(e)
for e in other.__list:
new_l.add(e)
return new_l
def __len__(self):
return len(self.__list)
def __contains__(self, e):
return e in self.__list
sl1 = SortedList()
sl1.add(1)
sl1.add(5)
sl1.add(4)
sl2 = SortedList()
sl2.add(2)
sl2.add(2)
sl2.add(6)
print(len(sl1))
print(1 in sl1)
print(3 in sl1)
sl = sl1 + sl2
print(sl.getList())