Lab 6: Object-Oriented Programming
Due at 11:59pm on 11/06/2016.
Starter Files
Download lab06.zip. Inside the archive, you will find starter files for the questions in this lab, along with a copy of the OK autograder.
Submission
By the end of this lab, you should have submitted the lab using ok. You may submit more than once before the
deadline; only the final submission will be graded.
- Question 1 is for practice, you don't need to submit anything for it.
- Please complete questions 2-6 for credit.
- Question 7 is an optional challenge question. If you complete question 7, you can skip another question and still receive full credit.
- Submit the
classes.pyfile took.
What is OOP?
In this lab, you will use Object-oriented programming (OOP) — a style of programming that allows you to think of code in terms of "objects." With OOP, you can construct objects with their own qualities and actions, which lets you create an object that is specific to your program. This is incredibly powerful — you can read up on all the details here. For now, we'll guide you through it as you build your very own text-based adventure game!
OOP terminology
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(object):
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
Carclass (shown above) describes the behavior and data that allCarobjects have. instance: a particular occurrence of a class. In Python, we create instances of a class like this:
>>> my_car = Car('red')my_caris an instance of theCarclass.attribute or field: a variable that belongs to the class. Think of an attribute as a quality of the object: cars have wheels and color, so we have given our
Carclassself.wheelsandself.colorattributes. We can access attributes using dot notation:>>> my_car.color 'red' >>> my_car.wheels 4method: Methods are just like normal functions, except that they are tied to an instance or a class. Think of a method as a "verb" of the class: cars can drive and also pop their tires, so we have given our
Carclass the methodsdriveandpop_tire. We call methods using dot notation:>>> my_car = Car('red') >>> my_car.drive() 'red car goes vroom!'constructor: As with data abstraction, constructors describe how to build an instance of the class. Most classes have a constructor. In Python, the constructor of the class defined as
__init__. For example, here is theCarclass's constructor:def __init__(self, color): self.wheels = Car.num_wheels self.color = colorThe constructor takes in one argument,
color. As you can see, the constructor also creates theself.wheelsandself.colorattributes.self: in Python,selfis the first parameter for many methods (in this class, we will only use methods whose first parameter isself). When a method is called,selfis bound to an instance of the class. For example:>>> my_car = Car('red') >>> car.drive()Notice that the
drivemethod takes inselfas an argument, but it looks like we didn't pass one in! This is because the dot notation implicitly passes incarasselffor us.
Prologue
It's the day of the DATA 8 silly hat lecture, and John needs to get from San Francisco to Berkeley without messing up his silly hat. He'd take the BART, but his silly hat is too tall! It'd be great if he had a car. A monster truck would be best, but a car will do.
In car.py, you'll find a class called Car. A class is a blueprint for
creating objects of that type. In this case, the Car class statement tells us
how to create Car objects.
So let's build John a car! Don't worry, you won't need to do any physical work —
the constructor will do it for you. The constructor of a class is a
method that constructs an instance of the class. In Python, the constructor
method is named __init__. Note that there must be two underscores on each side
of init. The Car class' constructor looks like this:
def __init__(self, model_type):
self.wheels = Car.num_wheels
self.color = 'No color yet. You need to paint me.'
self.model = model_type
self.gas = Car.gas
The __init__ method for Car has two parameters. The first one, self, is
bound to the newly created Car object. The second parameter, model_type,
is bound to the argument passed to the class, meaning when we construct a Car
object, we must also provide an argument for model_type.
Let's make our car. First, open up the Python interpreter and load car.py:
>>> python -i car.py
John would like to drive a Tesla to lecture. Try constructing an instance of
Car with 'Tesla' as the model_type.
>>> johns_car = Car('Tesla')
'Tesla' is passed in as the model_type, and the constructor binds the
instance attribute model_type to 'Tesla'. An object is an instance of a
class. In this case, johns_car is a Car object or an instance of the Car
class.
That's cool and all, but what color is John's new car? Let's talk about
attributes of instances and classes. Here's a snippet of the code in
car.py of the instance and class attributes in the Car class:
class Car(object):
num_wheels = 4
gas = 30
headlights = 2
size = 'Tiny'
def __init__(self, model_type):
self.wheels = Car.num_wheels
self.color = 'No color yet. You need to paint me.'
self.model = model_type
self.gas = Car.gas
To find out the color of johns_car, we'll look at the color instance
attribute. An instance attribute is a quality that is specific to an
instance, and is usually first initialized in the __init__ method. For
example, the color of johns_car doesn't affect the color of other cars. On the
other hand, a class attribute is a quality that is shared among all
instances of the class. For example, the Car class has four class attributes
seen at the beginning of a class: num_wheels = 4, gas = 30, headlights = 2
and size = 'Tiny'.
We can access attributes and call methods using dot notation, which relates
the object or class with its attributes or methods by separating the two parts
with a period. For instance attributes and methods, Python implicitly binds the
instance to self.
Use OK to follow along as you read the text:
python ok -q prologue -u --local
For example, in the following line, johns_car is the instance that Python
binds to self to access its color attribute.
>>> johns_car.color
______'No color yet. You need to paint me.'
Looks like we need to paint johns_car!
Let's use the paint method. Methods are functions that are bound to an
object. Think of methods as "verbs" of the class. We can paint a car some color,
so we've defined a paint method in the Car class.
>>> johns_car.paint('black')
______'Tesla is now black'
>>> johns_car.color
______'black'
Awesome! But why don't we need to pass two arguments? Similar to accessing
instance attributes, Python binds the instance johns_car to self.
John's black Tesla is pretty cool, but there's still not enough head room (look
at the class attribute size, it's 'Tiny'). How about we create a monster
truck for him instead? In car.py, we've defined a MonsterTruck class. Let's
look at the source code of MonsterTruck:
class MonsterTruck(Car):
size = 'Monster'
def rev(self):
print('Vroom! This Monster Truck is huge!')
def drive(self):
self.rev()
return Car.drive(self)
Wow! The truck may be big, but the source code is tiny! Let's make sure that the truck still does what we expect it to do. Try creating a new instance of John's monster truck:
>>> johns_truck = MonsterTruck('Monster Truck')
Now make sure it behaves as you would expect a Car to. Can you still paint it?
Is it even drivable?
Well, the class MonsterTruck is defined as class MonsterTruck(Car):, meaning
its base class is Car. Likewise, the class MonsterTruck is a subclass of
the Car class. That means the MonsterTruck class inherits all the
attributes and methods that were defined in Car, including its constructor!
Inheritance makes setting up a hierarchy of classes easier because the amount of code you need to write to define a new class of objects is reduced. You only need to add (or override) new attributes or methods so it's unique from those in the existing class.
>>> johns_car.size
______'Tiny'
>>> johns_truck.size
______'Monster'
Wow, what a difference in size! This is because the class attribute size of
MonsterTruck overrides the size class attribute of Car, so all
MonsterTruck instances are Monster-sized.
In addition, the drive method in MonsterTruck overrides the one in Car.
To show off all MonsterTruck instances, we defined a rev method specific to
MonsterClass and not its base class Car. Everything else — the constructor
__init__, paint, num_wheels — are inherited from Car.
Question 1: What would Python print?
Use OK to test your knowledge with the following What would Python print questions:
python ok -q car -u --localHint: If an error occurs, write
Error.
>>> johns_car = Car('Tesla')
>>> johns_car.model
______'Tesla'
>>> johns_car.gas = 10
>>> johns_car.drive()
______'Tesla goes vroom!'
>>> johns_car.drive()
______'Tesla cannot drive!'
>>> johns_car.fill_gas()
______Your car is full.
>>> johns_car.gas
______30
>>> Car.headlights
______2
>>> johns_car.headlights
______2
>>> Car.headlights = 3
>>> johns_car.headlights
______3
>>> johns_car.headlights = 2
>>> Car.headlights
______3
>>> johns_car.wheels = 2
>>> johns_car.wheels
______2
>>> Car.num_wheels
______4
>>> johns_car.drive()
______'Tesla cannot drive!'
>>> Car.drive()
______Error (TypeError)
>>> Car.drive(johns_car)
______'Tesla cannot drive!'
>>> MonsterTruck.drive(johns_car)
______Error (AttributeError)
>>> sumukhs_car = MonsterTruck('Batmobile')
>>> sumukhs_car.drive()
______Vroom! This Monster Truck is huge!
'Batmobile goes vroom!'
>>> Car.drive(sumukhs_car)
______'Batmobile goes vroom!'
>>> MonsterTruck.drive(sumukhs_car)
______Vroom! This Monster Truck is huge!
'Batmobile goes vroom!'
>>> Car.rev(sumukhs_car)
______Error (AttributeError)
Variables
Wait! Before you go on your adventure, you must take this handy chart with you... Now that you've learned OOP, this chart notes the three types of variables you should be aware of.
| Class Attributes | Instance Attributes | Local Variables | |
|---|---|---|---|
| Trait | A class attribute is a variable specific to the class. Instances of a class share the same class attributes. | An instance attribute is a variable that is specific to each instance of a class. | A local variable is a variable you see inside functions or methods. The scope resides in the block it is defined in. |
| Example |
|
|
|
| Explanation |
num_wheels is a class attribute of the
Car class. On the other hand, wheels
is an instance attribute that is initialized to be the value of
num_wheels when the instance was constructed.
|
We construct two Car instances. Each has its own
self.model instance attribute. One is
'Uber Car', and the other is 'Taxi'.
|
In the first line of code, x is a global variable,
not a local variable. The three local variables
are self, x, and i; they
are local to some_method.
|
Note: The format to access class attributes is <class name>.<class
attribute>, such as Car.num_wheels, or <instance>.<class attribute>, such
as car1.num_wheels.
Adventure Game!
In this lab, you will implement a text adventure game. To start the game, type
python adventure.py
The ZIP archive provided at the start of lab contains all the starter code. All
of your changes will be made to classes.py, although you have to create
yourself as a player in data.py before implementing anything else.
classes.py: Implementation for classes used in the gamedata.py: All of the objects used in the gameadventure.py: Interpreter of the game
Question 2: Who am I?
It is time for you to enter a world of adventure! First, you need to create
yourself as a Player object in data.py. Take a look at the Player class in
classes.py and create a Player object at the bottom of data.py.
The Player constructor takes two arguments:
nameshould be your preferred name (as a string)- the starting
place
Your Player should start at sather_gate.
# Player:
# The Player should start at sather_gate.
me = None
me = Player('Kelly', sather_gate)
Question 3: Where do I go?
Once you've created your player, you can start the adventure game:
python adventure.py
You will see the following output:
Welcome to the adventure game!
It's a bright sunny day.
You are a cute little squirrel named [your name],
wandering around Berkeley campus looking for food.
Let's go to FSM (Free Speech Movement Cafe)
and see what we can find there!
There are 7 possible commands:
talk to [character]
unlock [place]
help
take [thing]
go to [place]
look
check backpack
adventure>
First, we need to be able to go to places. If you try the go to command, you'll
notice it doesn't do anything.
In classes.py, implement the go_to method in the Player class. go_to
takes in a location that you want to go to and changes the player's
instance attribute place to point to the new place. At the end of the method
you should also print some text saying where you are.
Hint: The
get_neighbormethod inPlacewill return the correspondingPlaceobject iflocationis among your available exits. Otherwise,get_neighborwill return the currentPlaceand will print "Can't go to [destination place] from [starting place]. Take a look at the doctest for more details.
def go_to(self, location):
"""Go to a location if it's among the exits of player's current place.
>>> sather_gate = Place('Sather Gate', 'You are at Sather Gate', [], [])
>>> gbc = Place('GBC', 'You are at Golden Bear Cafe', [], [])
>>> sather_gate.add_exits([gbc])
>>> sather_gate.locked = True
>>> gbc.add_exits([sather_gate])
>>> me = Player('player', sather_gate)
>>> me.go_to('GBC')
You are at GBC
>>> me.place is gbc
True
>>> me.place.name
'GBC'
>>> me.go_to('GBC')
Can't go to GBC from GBC.
Try looking around to see where to go.
You are at GBC
>>> me.go_to('Sather Gate')
Sather Gate is locked! Go look for a key to unlock it
You are at GBC
"""
destination_place = self.place.get_neighbor(location)
if destination_place.locked:
print(destination_place.name, 'is locked! Go look for a key to unlock it')
"*** YOUR CODE HERE ***"
else:
self.place = destination_place
print('You are at', self.place.name)
Use OK to test your code:
python ok -q Player.go_to --local
Question 4: How do I talk?
Now you can go wherever you want! Try going to Wheeler Hall. There, you'll find
Derrick. Try talking to him with the talk to command. This also doesn't work
:(
Next, implement the talk_to method in Player. talk_to takes in the name of
a Character object, and prints out the Character's response. Take a look at
the doctest for more details.
Hint:
talk_totakes in an argumentperson, which is a string. Thecharactersinstance attribute inself.placeis a dictionary mappingCharacternames (strings) toCharacterobjects.Once you've got the
Characterobject, what method in theCharacterclass will make them talk?
def talk_to(self, person):
"""Talk to person if person is at player's current place.
>>> john = Character('John', 'Have to run for lecture!')
>>> sather_gate = Place('Sather Gate', 'You are at Sather Gate', [john], [])
>>> me = Player('player', sather_gate)
>>> me.talk_to(john)
Person has to be a string.
>>> me.talk_to('John')
John says: Have to run for lecture!
>>> me.talk_to('Albert')
Albert is not here.
"""
if type(person) != str:
print('Person has to be a string.')
"*** YOUR CODE HERE ***"
elif person not in self.place.characters:
print(person, 'is not here.')
else:
print(person,'says:', self.place.characters[person].talk())
Use OK to test your code:
python ok -q Player.talk_to --local
Question 5: How do I take items?
Now you can talk to people in adventure world! To make it even better, let's
implement the take method in the Player class. take takes in a Thing
object and puts it into your backpack. Currently, you don't have a backpack,
so let's create an instance variable backpack and initialize it to an empty
list.
After you've done initialized your empty backpack, take a look at the doctests
for take and implement the method.
Hint: the
thingsinstance attribute in thePlaceclass is a dictionary that mapsThingnames (strings) toThingobjects.The
takemethod in thePlaceclass will also come in handy.
def take(self, thing):
"""Take a thing if thing is at player's current place
>>> hotdog = Thing('Hotdog', 'A hot looking hotdog')
>>> gbc = Place('GBC', 'You are at Golden Bear Cafe', [], [hotdog])
>>> me = Player('Player', gbc)
>>> me.backpack
[]
>>> me.take(hotdog)
Thing should be a string.
>>> me.take('dog')
dog is not here.
>>> me.take('Hotdog')
Player takes the Hotdog
>>> me.take('Hotdog')
Hotdog is not here.
>>> isinstance(me.backpack[0], Thing)
True
>>> len(me.backpack)
1
"""
if type(thing) != str:
print('Thing should be a string.')
"*** YOUR CODE HERE ***"
elif thing not in self.place.things:
print(thing, 'is not here.')
else:
taken = self.place.take(thing)
print(self.name, 'takes the', taken.name)
self.backpack.append(taken)
Use OK to test your code:
python ok -q Player.take --local
Question 6: No door can hold us back!
FSM is locked! There's no way for us to get in, and you're getting pretty desperate for that sweet, delicious, caffeinated nectar of the gods.
We'll need to do two things in order to get into FSM and get our caffeine fix.
Firstly, define a new class Key, that is a subclass of Thing, but
overwrites the use method to unlock the door to FSM.
Hint 1: Refer back to the
MonsterTruckexample if you need a refresher on how to define a subclass and overwrite methods. Make sure you define Key after you've defined Thing.Hint 2: Place has an
lockedinstance attribute that you may need to change.
class Thing(object):
def __init__(self, name, description):
self.name = name
self.description = description
def use(self, place):
print("You can't use a {0} here".format(self.name))
""" Implement Key here! """
class Key(Thing):
def use(self, place):
if place.locked:
place.locked = False
print(place.name, 'is now unlocked!')
else:
print(place.name, 'is already unlocked!')
You'll also need to finish the implementation of unlock in
Player. It takes a string place that you want to unlock, and if
you have a key, call the key's use method to unlock the place. If
you have no key, then the method should print that the place can't be
unlocked without a key.
You'll need to implement both Key and unlock for the doctests to pass.
def unlock(self, place):
"""If player has a key, unlock a locked neighboring place.
>>> key = Key('SkeletonKey', 'A Key to unlock all doors.')
>>> gbc = Place('GBC', 'You are at Golden Bear Cafe', [], [key])
>>> fsm = Place('FSM', 'Home of the nectar of the gods', [], [])
>>> gbc.add_exits([fsm])
>>> fsm.locked = True
>>> me = Player('Player', gbc)
>>> me.unlock(fsm)
Place must be a string
>>> me.go_to('FSM')
FSM is locked! Go look for a key to unlock it
You are at GBC
>>> me.unlock(fsm)
Place must be a string
>>> me.unlock('FSM')
FSM can't be unlocked without a key!
>>> me.take('SkeletonKey')
Player takes the SkeletonKey
>>> me.unlock('FSM')
FSM is now unlocked!
>>> me.unlock('FSM')
FSM is already unlocked!
>>> me.go_to('FSM')
You are at FSM
"""
if type(place) != str:
print("Place must be a string")
return
key = None
for item in self.backpack:
if type(item) == Key:
key = item
"*** YOUR CODE HERE ***"
if not key:
print(place, "can't be unlocked without a key!")
else:
place_to_unlock = self.place.get_neighbor(place)
key.use(place_to_unlock)
Use OK to test your code:
python ok -q Player.unlock --local
Question 7: Knapsack (Challenge)
You've successfully completed your adventure, and as a reward Professor DeNero leads you to the first floor of Soda, unlocks a small wooden door, and offers you your choice of the treasures of Berkeley Computer Science. Shiny electronics, relics of a forgotten time, stacks of punch cards, Software Engineering books from 2009. Your small backpack can only carry so much, you should come up with a strategy to take the max possible value!
Each treasure is an instance of the class Treasure, which has
instance attributes weight and value. Complete the definition of
the method knapsack, which takes a max_weight and a
list_of_treasures, and returns the most valuable combination of
treasures that have a combined weight less than or equal to
max_weight.
Hint: Tree recursion.
Hint Hint: Think about two cases for each treasure, either take it, or leave it.
def knapsack(self, max_weight, list_of_treasures):
"""Return the total value of the most valuable combination of treasures
which have a combined weight less than max_weight
>>> t1 = Treasure('Treasure 1', 'Software Engineering 2008', 5, 6)
>>> t2 = Treasure('Treasure 2', "Paul Hilfinger's First Computer", 10, 50)
>>> t3 = Treasure('Treasure 3', "John's Silly Hat", 6, 3)
>>> t4 = Treasure('Treasure 4', 'Whiteboard Marker', 4, 2)
>>> t5 = Treasure('Treasure 5', 'USB with a Linux Distro', 2, 4)
>>> treasure_list = [t1, t2, t3, t4, t5]
>>> soda = Place('Soda', 'Soda', [], [])
>>> me = Player('Player', soda)
>>> me.knapsack(10, treasure_list) # Treasures 3, 4, 5
12
>>> me.knapsack(2, treasure_list) # Treasure 4
4
>>> me.knapsack(100, treasure_list) # Treasures 1, 2, 3, 4, 5
27
"""
"*** YOUR CODE HERE ***"
list_of_treasures = [t for t in list_of_treasures if t.weight <= max_weight]
if len(list_of_treasures) == 0 or max_weight < 0:
return 0
leave_treasure = self.knapsack(max_weight, list_of_treasures[1:])
treasure = list_of_treasures[0]
take_treasure = treasure.value + self.knapsack(max_weight - treasure.weight,
list_of_treasures[1:])
return max(take_treasure, leave_treasure)
Use OK to test your code:
python ok -q Player.knapsack --local