In lecture, we learned about inheritance and composition. To summarize these two concepts and their differences:
Inheritance is more useful when the class we are extending has a lot of methods, and we want all (or mostly all) of them to work with our new class. Inheritance is also more useful when we have instance variables we want to be public in our new class, as with composition we have to make sure they are always updated. Composition is more useful when we want to bring together methods from different classes or when we want to change most methods anyway.
You might be more familiar with inheritance, because it is more commonly used in other OOP (object-oriented programming) languages such as Java and C++. In statically typed language (such as Java and C++) if we have an object we probably already know what it is. Python is not statically typed, so a function where we expect to be given a certain object could receive anything. But often times it doesn't matter if we have the exact type of object we were expecting, it only matters if we have the same methods and variables. We don't check if the object is of the class the class, we just try the methods and see if they work. This is why composition works! This type of approach is often called duck typing.
Exercise 1: Below is the class Fruit
. Extend this class to create Banana
. Specifically, implement so that when a Banana is initialized only the color needs to be given with "banana" supplied to the fruit_type already. Also, write a peel()
method and have that called first whenever eat()
is called.
class Fruit:
def __init__(self, fruit_type, color):
self.fruit_type = fruit_type
self.color = color
def __str__(self):
return f"{self.color} {self.fruit_type}"
def eat(self):
print(f"The {self} has been eaten.")
(solution below)
.
.
.
.
.
.
.
class Fruit:
def __init__(self, fruit_type, color):
self.fruit_type = fruit_type
self.color = color
def __str__(self):
return f"{self.color} {self.fruit_type}"
def eat(self):
print(f"The {self} has been eaten.")
class Banana(Fruit):
def __init__(self, color):
super().__init__("banana", color)
def peel(self):
print(f"The {self} has been peeled.")
def eat(self):
self.peel()
super().eat()
a = Fruit("apple", "red")
b = Banana("yellow")
a.eat()
b.eat()
Exercise 2: Do the same thing as above except use class composition instead.
(solution below)
.
.
.
.
.
.
.
.
class Banana:
def __init__(self, color):
self.fruit = Fruit("banana", color)
def __str__(self):
return str(self.fruit)
def peel(self):
print(f"The {self} has been peeled.")
def eat(self):
self.peel()
self.fruit.eat()
a = Fruit("apple", "red")
b = Banana("yellow")
a.eat()
b.eat()
Exercise 3: Make a class StringList that has some of the functionality of both a string and a list. That is, we can use some string methods on it and some list methods. Specifically we want to implement __len__()
from string and list, lower()
from string, and insert()
and pop()
from list. We also want an initialization method the would work with a string. Remember that the list methods modify the object and the string methods do not.
(solution below)
.
.
.
.
.
.
.
.
class StringList:
def __init__(self, s):
self.string = s
self.list = list(s)
def lower(self):
return self.string.lower()
def __len__(self):
return len(self.string)
def insert(self, idx, c):
self.list.insert(idx, c)
self.string = ''.join(self.list)
def pop(self, idx = None):
# pop returns last element by default
if idx == None:
idx = len(self.list) - 1
value = self.list.pop(idx)
self.string = ''.join(self.list)
return value
s = StringList("Hello World!")
print(s.lower())
s.pop()
s.insert(0, "!")
print(s.lower())