Inheritance and Composition

In lecture, we learned about inheritance and composition. To summarize these two concepts and their differences:

  • Both concepts allow us to take an existing class and write additional functionality on top.
  • Inheritance makes our new class automatically have the same methods and variables as the original, but can be finicky.
  • Composition gives us complete control over which methods can be used and how they are.

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.

In [1]:
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)

.

.

.

.

.

.

.

In [2]:
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()
The red apple has been eaten.
The yellow banana has been peeled.
The yellow banana has been eaten.

Exercise 2: Do the same thing as above except use class composition instead.

(solution below)

.

.

.

.

.

.

.

.

In [3]:
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()
The red apple has been eaten.
The yellow banana has been peeled.
The yellow banana has been eaten.

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)

.

.

.

.

.

.

.

.

In [4]:
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())
hello world!
!hello world