Lab 6 Solutions
Solution Files
Topics
Consult this section if you need a refresher on the material for this lab. It's okay to skip directly to the questions and refer back here should you get stuck.
Object-Oriented Programming
Object-oriented programming (OOP) is a style of programming that
allows you to think of code in terms of "objects." Here's an example of
a Car
class:
class Car:
num_wheels = 4
def __init__(self, color):
self.wheels = Car.num_wheels
self.color = color
def drive(self):
if self.wheels <= Car.num_wheels:
return self.color + ' car cannot drive!'
return self.color + ' car goes vroom!'
def pop_tire(self):
if self.wheels > 0:
self.wheels -= 1
Here's some terminology:
- class: a blueprint for how to build a certain type of object.
The
Car
class (shown above) describes the behavior and data that allCar
objects have. instance: a particular occurrence of a class. In Python, we create instances of a class like this:
>>> my_car = Car('red')
my_car
is an instance of theCar
class.data attributes: a variable that belongs to the instance (also called instance variables). Think of a data attribute as a quality of the object: cars have wheels and color, so we have given our
Car
instanceself.wheels
andself.color
attributes. We can access attributes using dot notation:>>> my_car.color 'red' >>> my_car.wheels 4
method: Methods are just like normal functions, except that they are bound to an instance. Think of a method as a "verb" of the class: cars can drive and also pop their tires, so we have given our
Car
instance the methodsdrive
andpop_tire
. We call methods using dot notation:>>> my_car = Car('red') >>> my_car.drive() 'red car goes vroom!'
constructor: Constructors build an instance of the class. The constructor for car objects is
Car(color)
. When Python calls that constructor, it immediately calls the__init__
method. That's where we initialize the data attributes:def __init__(self, color): self.wheels = Car.num_wheels self.color = color
The constructor takes in one argument,
color
. As you can see, this constructor also creates theself.wheels
andself.color
attributes.self
: in Python,self
is the first parameter for many methods (in this class, we will only use methods whose first parameter isself
). When a method is called,self
is bound to an instance of the class. For example:>>> my_car = Car('red') >>> car.drive()
Notice that the
drive
method takes inself
as an argument, but it looks like we didn't pass one in! This is because the dot notation implicitly passes incar
asself
for us.
Inheritance
Python classes can implement a useful abstraction technique known as inheritance. To illustrate this concept, consider the followingDog
and Cat
classes.
class Dog():
def __init__(self, name, owner):
self.is_alive = True
self.name = name
self.owner = owner
def eat(self, thing):
print(self.name + " ate a " + str(thing) + "!")
def talk(self):
print(self.name + " says woof!")
class Cat():
def __init__(self, name, owner, lives=9):
self.is_alive = True
self.name = name
self.owner = owner
self.lives = lives
def eat(self, thing):
print(self.name + " ate a " + str(thing) + "!")
def talk(self):
print(self.name + " says meow!")
Notice that because dogs and cats share a lot of similar qualities, there is a lot of repeated code! To avoid redefining attributes and methods for similar classes, we can write a single base class from which the similar classes inherit. For example, we can write a class called Pet and redefine Dog as a subclass of Pet:
class Pet():
def __init__(self, name, owner):
self.is_alive = True # It's alive!!!
self.name = name
self.owner = owner
def eat(self, thing):
print(self.name + " ate a " + str(thing) + "!")
def talk(self):
print(self.name)
class Dog(Pet):
def talk(self):
print(self.name + ' says woof!')
Inheritance represents a hierarchical relationship between two or more
classes where one class is a (no relation to the Python is
operator)
more specific version of the other, e.g.
a dog is a pet. Because Dog
inherits from Pet
, we
didn't have to redefine __init__
or eat
. However, since
we want Dog
to talk
in a way that is unique to dogs, we did
override the talk
method.
We can use the super()
function to refer to a class's superclass. For example, calling super()
within the class definition of Dog
allows us to access the same object but as if it were an instance of its superclass, in this case Pet
. This is a little bit of a simplification, and if you're interested you can read more at https://docs.python.org/3/library/functions.html#super.
Here's an example of an alternate equivalent definition of Dog
that uses super()
to explicitly call the __init__
method of the parent class:
class Dog(Pet):
def __init__(self, name, owner):
super().__init__(name, owner)
# this is equivalent to calling Pet.__init__(self, name, owner)
def talk(self):
print(self.name + ' says woof!')
Keep in mind that creating the __init__
function shown above is actually not necessary, because creating a Dog
instance will automatically call the __init__
method of Pet
. Normally when defining an __init__
method in a subclass, we take some additional action to calling super().__init__
. For example, we could add a new instance variable like the following:
def __init__(self, name, owner, has_floppy_ears):
super().__init__(name, owner)
self.has_floppy_ears = has_floppy_ears
Required Questions
What Would Python Do?
These questions use inheritance. For an overview of inheritance, see the inheritance portion of Composing Programs.
Q1: WWPD: Classy Cars
Below is the definition of a Car
class that we will be using in the following WWPD questions.
Note: The
Car
class definition can also be found incar.py
.
class Car:
num_wheels = 4
gas = 30
headlights = 2
size = 'Tiny'
def __init__(self, make, model):
self.make = make
self.model = model
self.color = 'No color yet. You need to paint me.'
self.wheels = Car.num_wheels
self.gas = Car.gas
def paint(self, color):
self.color = color
return self.make + ' ' + self.model + ' is now ' + color
def drive(self):
if self.wheels < Car.num_wheels or self.gas <= 0:
return 'Cannot drive!'
self.gas -= 10
return self.make + ' ' + self.model + ' goes vroom!'
def pop_tire(self):
if self.wheels > 0:
self.wheels -= 1
def fill_gas(self):
self.gas += 20
return 'Gas level: ' + str(self.gas)
For the later unlocking questions, we will be referencing the MonsterTruck
class below.
Note: The
MonsterTruck
class definition can also be found incar.py
.
class MonsterTruck(Car):
size = 'Monster'
def rev(self):
print('Vroom! This Monster Truck is huge!')
def drive(self):
self.rev()
return super().drive()
You can find the unlocking questions below.
Use Ok to test your knowledge with the following "What Would Python Display?" questions:
python3 ok -q wwpd-car -u
Important: For all WWPD questions, type
Function
if you believe the answer is<function...>
,Error
if it errors, andNothing
if nothing is displayed.
>>> deneros_car = Car('Tesla', 'Model S')
>>> deneros_car.model
______'Model S'
>>> deneros_car.gas = 10
>>> deneros_car.drive()
______'Tesla Model S goes vroom!'
>>> deneros_car.drive()
______'Cannot drive!'
>>> deneros_car.fill_gas()
______'Gas level: 20'
>>> deneros_car.gas
______20
>>> Car.gas
______30
>>> deneros_car = Car('Tesla', 'Model S')
>>> deneros_car.wheels = 2
>>> deneros_car.wheels
______2
>>> Car.num_wheels
______4
>>> deneros_car.drive()
______'Cannot drive!'
>>> Car.drive()
______Error (TypeError)
>>> Car.drive(deneros_car)
______'Cannot drive!'
>>> deneros_car = MonsterTruck('Monster', 'Batmobile')
>>> deneros_car.drive()
______Vroom! This Monster Truck is huge!
'Monster Batmobile goes vroom!'
>>> Car.drive(deneros_car)
______'Monster Batmobile goes vroom!'
>>> MonsterTruck.drive(deneros_car)
______Vroom! This Monster Truck is huge!
'Monster Batmobile goes vroom!'
>>> Car.rev(deneros_car)
______Error (AttributeError)
Parsons Problems
To work on these problems, open the Parsons editor:
python3 parsons
Q2: Cool Cats
The Cat
class models a cat: you can find the implementation below. Now, you will implement
NoisyCat
; NoisyCat
s are very similar to Cat
s, but talk
s twice as much. However, in exchange for such great powers,
it gives up one of its initial lives
.
Use superclass methods wherever possible.
class Cat:
def __init__(self, name, owner, lives=9):
self.is_alive = True
self.name = name
self.owner = owner
self.lives = lives
def talk(self):
return self.name + ' says meow!'
class NoisyCat(Cat):
"""
>>> my_cat = NoisyCat("Furball", "James")
>>> my_cat.name
'Furball'
>>> my_cat.is_alive
True
>>> my_cat.lives
8
>>> my_cat.talk()
'Furball says meow! Furball says meow!'
>>> friend_cat = NoisyCat("Tabby", "James", 2)
>>> friend_cat.talk()
'Tabby says meow! Tabby says meow!'
>>> friend_cat.lives
1
"""
def __init__(self, name, owner, lives=9):
super().__init__(name, owner, lives)
self.lives -= 1
def talk(self):
words = super().talk()
words = words + " " + words
return words
Coding Practice
Q3: Cat Adoption
So far, you've implemented the NoisyCat
based off of the Cat
class.
However, you now want to be able to create lots of different Cat
s!
Build on the Cat
class from the earlier problem by adding
a class method called adopt_a_cat
.
This class method allows you to create Cat
s that can then be adopted.
Specifically, adopt_a_cat
should return a new instance of a Cat
whose owner is owner
.
This Cat
instance's name and number of lives depends on the owner
.
Its name should be chosen from cat_names
(provided in the skeleton code),
and should correspond to the name at the index len(owner)
%
(modulo)
the number of possible cat names.
Its number of lives should be equal to len(owner)
+ the
length of the chosen name.
class Cat:
def __init__(self, name, owner, lives=9):
self.is_alive = True
self.name = name
self.owner = owner
self.lives = lives
def talk(self):
return self.name + ' says meow!'
@classmethod
def adopt_a_cat(cls, owner):
"""
Returns a new instance of a Cat.
This instance's owner is the given owner.
Its name and its number of lives is chosen programatically
based on the spec's noted behavior.
>>> cat1 = Cat.adopt_a_cat("Ifeoma")
>>> isinstance(cat1, Cat)
True
>>> cat1.owner
'Ifeoma'
>>> cat1.name
'Felix'
>>> cat1.lives
11
>>> cat2 = Cat.adopt_a_cat("Ay")
>>> cat2.owner
'Ay'
>>> cat2.name
'Grumpy'
>>> cat2.lives
8
"""
cat_names = ["Felix", "Bugs", "Grumpy"]
name = cat_names[len(owner) % len(cat_names)]
num_lives = len(name) + len(owner) return cls(name, owner, num_lives)
Use Ok to test your code:
python3 ok -q Cat.adopt_a_cat
Accounts
Let's say we'd like to model a bank account that can handle interactions
such as depositing funds or gaining interest on current funds.
In the following questions, we will be building off of the Account
class.
Here's our current definition of the class:
class Account:
"""An account has a balance and a holder.
>>> a = Account('John')
>>> a.deposit(10)
10
>>> a.balance
10
>>> a.interest
0.02
>>> a.time_to_retire(10.25) # 10 -> 10.2 -> 10.404
2
>>> a.balance # balance should not change
10
>>> a.time_to_retire(11) # 10 -> 10.2 -> ... -> 11.040808032
5
>>> a.time_to_retire(100)
117
"""
max_withdrawal = 10
interest = 0.02
def __init__(self, account_holder):
self.balance = 0
self.holder = account_holder
def deposit(self, amount):
self.balance = self.balance + amount
return self.balance
def withdraw(self, amount):
if amount > self.balance:
return "Insufficient funds"
if amount > self.max_withdrawal:
return "Can't withdraw that amount"
self.balance = self.balance - amount
return self.balance
Q4: Retirement
Add a time_to_retire
method to the Account
class.
This method takes in an amount
and returns how many years the holder would
need to wait in order for the current balance
to grow to at least amount
,
assuming that the bank adds balance
times the interest
rate to the total
balance at the end of every year.
def time_to_retire(self, amount):
"""Return the number of years until balance would grow to amount."""
assert self.balance > 0 and amount > 0 and self.interest > 0
future = self.balance
years = 0
while future < amount:
future += self.interest * future
years += 1
return years
Use Ok to test your code:
python3 ok -q Account
We take of our current balance, and simulate the growth from interest over many years. We stop once we hit the target value.
Note that the problem solving procedure does not differ very much from an non OOP problem. The main difference here is make sure that we do not change the account balance while in the process of calculating the future balance. Therefore, something along these lines is necessary:
future = self.balance
Video walkthrough:
Q5: FreeChecking
Implement the FreeChecking
class, which is like the Account
class from
lecture except that it charges a withdraw fee after 2 withdrawals.
If a withdrawal is unsuccessful, it still counts towards the number of free
withdrawals remaining, but no fee for the withdrawal will be charged.
Hint: Don't forget that
FreeChecking
inherits fromAccount
! Check the Inheritance section in Topics for a refresher.
class FreeChecking(Account):
"""A bank account that charges for withdrawals, but the first two are free!
>>> ch = FreeChecking('Jack')
>>> ch.balance = 20
>>> ch.withdraw(100) # First one's free
'Insufficient funds'
>>> ch.withdraw(3) # And the second
17
>>> ch.balance
17
>>> ch.withdraw(3) # Ok, two free withdrawals is enough
13
>>> ch.withdraw(3)
9
>>> ch2 = FreeChecking('John')
>>> ch2.balance = 10
>>> ch2.withdraw(3) # No fee
7
>>> ch.withdraw(3) # ch still charges a fee
5
>>> ch.withdraw(5) # Not enough to cover fee + withdraw
'Insufficient funds'
"""
withdraw_fee = 1
free_withdrawals = 2
def __init__(self, account_holder):
super().__init__(account_holder)
self.withdrawals = 0
def withdraw(self, amount):
self.withdrawals += 1
fee = 0
if self.withdrawals > self.free_withdrawals:
fee = self.withdraw_fee
return super().withdraw(amount + fee)
# Alternative solution where you don't need to include init.
# Check out the video solution for more.
def withdraw(self, amount):
self.free_withdrawals -= 1
if self.free_withdrawals >= 0:
return super().withdraw(amount)
return super().withdraw(amount + self.withdraw_fee)
Use Ok to test your code:
python3 ok -q FreeChecking
We can take advantage of inheritance to make sure we add just what we need
to withdraw
.
- For starters, a withdrawal with a fee is the same as the original
withdraw amount plus the amount from the fee. We can therefore represent
a
FreeChecking
withdraw as a "regular"Account
withdraw in this way. - On top of the note from before, we need to do a little bit of extra bookkeeping to make sure the first few withdrawals do not add the extra fee. We can either create a new instance attribute or modify an existing one.
Video walkthrough:
Submit
Make sure to submit this assignment by running:
python3 ok --submit