Object-Oriented Programming (OOP) is one of the major paradigms in programming. Last week, I gave an intro talk on the main principles of OOP, contrasting it to other programming paradigms like imperative and functional programming. We also looked at some examples of using OOP in Python, Fortran and C++. This week, we’re getting our hands dirty actually writing some OOP from scratch in Python.
Outline
- Classes and Instances
- Abstraction
- Encapsulation
- Inheritance
- Polymorphism
Code shamelessly stolen from https://www.youtube.com/watch?v=qxOqZQ1fWNw
Recap on OOP
The Four Pillars
- Abstraction: hide the details
- Encapsulation: protect the details
- Inheritance: things can be a subtype of another thing
- Polymorphism: things can act like other things
Before we begin…
- Download the tests and unpack the tests:
$ tar xvf inventory_tests.tar
- Install pytest:
$ pip3 install --user pytest
- You might need to add the following to your
PATH:
$ export PATH=~/.local/bin:$PATH
$ which pytest
Classes and Instances
Inventories and Items
- Make a file
inventory.py - Let’s start with a very simple class:
class Item:
"""A generic item
Attributes
----------
name : str
The name of the item
"""
def __init__(self, name):
self.name = name
if __name__ == "__main__":
sword = Item("Sword")
falafel = Item("Falafel")
print(sword)
print(falafel)
Then run it:
$ python3 ./inventory.py
<__main__.Item object at 0x...>
<__main__.Item object at 0x...>
The 0x... is the address in memory of the instance
More useful printing
Let’s add the “magic method” __str__ which converts an object to a
string:
class Item:
...
def __str__(self):
return self.name
- Now what happens when you run the file?
Beginner
- We’re going to be making a container for
Items, so will need to know how big they are - Give
Items asizeattribute - Add the
sizein__str__so we know how big theItemis when weprintit- Hint: the tests are looking for something like “size 2”
- Don’t forget to update the docstring for
Item - Download the tests from … and run
pytest -q test_inventory01.pyto check your work
Advanced
- The
__repr__method returns a string that can reproduce the object usingeval:eval(repr(Item("Falafel", 1))) == Item("Falafel", 1)
- Add the
__repr__method - Also add a
test_repr_packmethod totest_inventory01.pyto check__repr__works- Hint: You might also add
__eq__to test for equality
- Hint: You might also add
Inheritance and Polymorphism
Different types of Items
- All
Items have a name and a size Items are kind of abstract though, and differentItems do different things- What do all good games need? Weapons and food!
class Weapon(Item):
pass
class Food(Item):
pass
...
sword = Weapon("Sword", 8)
falafel = Food("Falafel", 1)
print(sword)
print(falafel)
isinstance(sword, Weapon) # True
isinstance(sword, Food) # False
isinstance(sword, Item) # True
passtells Python to do nothing- We’ve essentially made two aliases for
Item
- We’ve essentially made two aliases for
isinstancetells us if an object is an instance of a particular type- Let’s do something a little fancy to tell the difference:
class Item:
...
def __str__(self):
class_name = self.__class__.__name__
return ("{} is a {} of size {}".format(self.name, class_name, self.size)
__class__is the class object of an object- In Python, everything is an object!
-
__name__is the name of the class print(sword)is now a bit more informative
Specialising subtypes
- If
Weapons andFoodbehaved completely identically, we wouldn’t need to subclass them - Let’s give a
powerattribute toWeapons:
class Weapon(Item):
"""An Item suitable for fighting with
Attributes
----------
name : str
The name of the item
size : int
How many spaces in a Pack the Item takes up
power : int, float
How much damage the Weapon does
"""
def __init__(self, name, size, power):
self.name = name
self.size = size
self.power = power
- Here we are overriding the
__init__method fromItem - When we make a new
Weapon, we now callWeapon.__init__instead ofItem.__init__ - But aren’t we just repeating code from
Item.__init__? - What if we had a whole chain of inheritance?
super()is the answer: this essentially gets the parent type of our object:
class Weapon(Item):
def __init__(self, name, size, power):
super().__init__(name, size)
self.power = power
def __str__(self):
return "{} and power {}".format(super().__str__(), self.power)
- What about entirely new methods?
class Weapon(Item):
...
def attack(self):
"""Attack wildly
"""
print("You attack for {} damage!".format(self.power))
- We can
attackwithWeapons, but not with a genericItem:
sword = Weapon("Sword", 8, 4)
falafel = Food("Falafel", 1, 12)
# This is ok
sword.attack()
# This is not!
falafel.attack()
Beginner
- Add a
potencyattribute toFood - Add a
eatmethod toFoodthat prints a string that says you heal “potencyhealth” - Don’t forget the docstrings!
- Check your work by running
pytest -q test_inventory02.py
Advanced
- Make a subclass of
WeaponcalledRangedWeaponthat has arangeattribute - Override the
attackmethod to take adistanceargument that reduces thepowerto zero outside ofrange- Hint: use
super()to callWeapon.attackwith the adjustedpower
- Hint: use
- Don’t forget to add tests to check your work
Abstraction and Encapsulation
Putting the Items away
- Making a pack that can store items
- Has a list that we can add items to
- We’ll want some control over what items we store
- e.g. make sure they are items, and we don’t overfill the pack
addandremovemethods- we’re encapsulating the data, protecting it from direct modification
class Pack:
def __init__(self, capacity):
self.capacity = capacity
# Single underscore to mark this as "private"
self._contents = []
def add(self, item):
"""Add an Item to the Pack
Parameters
----------
item : Item
An Item to store in the pack
"""
self._contents.append(item)
- We’ve encapsulated the container to hide the implementation detail
of how
Items are actually stored_contentsis hidden
- Prevents direction modification of the
_contents - Allows us to change the implementation details later, providing we
don’t change the interface (
add(self, item))
More useful
- A
Packhas acapacity: we can’t put infiniteItems in it Pack.addshould check we can store anItembefore letting us do so!- Let’s add a method for finding out how much stuff we’ve got in our
Pack:
class Pack:
...
def space_used(self):
"""Returns the total size of the items in this Pack
"""
total_size = 0
for item in self._contents:
total_size += item.size
return total_size
- We’ve abstracted over the details of calculating how much space
we’ve used in the
Pack - This goes hand-in-hand with the encapsulation of
_contents
Beginner
- Implement
space_left,is_fullandis_empty- Hint:
space_leftshould return an integer, andis_full/emptyshould returnTrueorFalse
- Hint:
- Add a check to
addto make sure we only putWeapons andFoodin thePack- Hint: what do they have in common?
- Add a check to
addto make sure there’s enough space to fit theItemin thePack- Hint:
raiseaValueErrorif there isn’t
- Hint:
- Check your work by running
pytest -q test_inventory03.py
Advanced
- Implement
has_itemwhich takes a string and returnsTrueif there is anItemof that name in thePack - Implement
removewhich takes a string, and if there’s anItemof that name in thePack, remove it from_contentsand return theItem- Hint: you probably want
enumerateandlist.pop
- Hint: you probably want
- Don’t forget to add tests and check your work
Other things to do!
- Change
_contentto a dictionary with theItemname as the key, and a tuple containing theItemand how many there are as the value