Inheritance

Tips for navigating the slides:
  • Press O or Escape for overview mode.
  • Press the copy icon on the upper right of code blocks to copy the code

Class outline:

  • Motivation
  • Inheritance
  • Multiple Inheritance
  • Composition
  • Identity

Motivation

Building "Animal Conserving"

A game where we take care of cute furry/ferocious animals:

A bunch of pairs of animals

What should be the classes?

A bunch of pairs of animals

                    Panda()
                    Lion()
                    Rabbit()
                    Vulture()
                    Elephant()
                    Food()
                    

A Food class

Let's start simple:


                    class Food:

                        def __init__(self, name, type, calories):
                            self.name = name
                            self.type = type
                            self.calories = calories
                    

How would we use that class?


                    broccoli = Food("Broccoli Rabe", "veggies", 20)
                    bone_marrow = Food("Bone Marrow", "meat", 100)
                    

An Elephant class


                    class Elephant:
                        species_name = "African Savanna Elephant"
                        scientific_name = "Loxodonta africana"
                        calories_needed = 8000

                        def __init__(self, name, age=0):
                            self.name = name
                            self.age = age
                            self.calories_eaten  = 0
                            self.happiness = 0

                        def play(self, num_hours):
                            self.happiness += (num_hours * 4)
                            print("WHEEE PLAY TIME!")

                        def eat(self, food):
                            self.calories_eaten += food.calories
                            print(f"Om nom nom yummy {food.name}")
                            if self.calories_eaten > self.calories_needed:
                                self.happiness -= 1
                                print("Ugh so full")

                        def interact_with(self, animal2):
                            self.happiness += 1
                            print(f"Yay happy fun time with {animal2.name}")
                    

How would we use that class?


                    el1 = Elephant("Willaby", 5)
                    el2 = Elephant("Wallaby", 3)
                    el1.play(2)
                    el1.interact_with(el2)
                    

A Rabbit class


                    class Rabbit:
                        species_name = "European rabbit"
                        scientific_name = "Oryctolagus cuniculus"
                        calories_needed = 200

                        def __init__(self, name, age=0):
                            self.name = name
                            self.age = age
                            self.calories_eaten = 0
                            self.happiness = 0

                        def play(self, num_hours):
                            self.happiness += (num_hours * 10)
                            print("WHEEE PLAY TIME!")

                        def eat(self, food):
                            self.calories_eaten += food.calories
                            print(f"Om nom nom yummy {food.name}")
                            if self.calories_eaten > self.calories_needed:
                                self.happiness -= 1
                                print("Ugh so full")

                        def interact_with(self, animal2):
                            self.happiness += 4
                            print(f"Yay happy fun time with {animal2.name}")
                    

How would we use that class?


                    rabbit1 = Rabbit("Mister Wabbit", 3)
                    rabbit2 = Rabbit("Bugs Bunny", 2)
                    rabbit1.eat(broccoli)
                    rabbit2.interact_with(rabbit1)
                    

Notice similarities?

Elephant Rabbit

                            # Class variables
                            species_name
                            scientific_name
                            calories_needed

                            # Instance variables
                            name
                            age
                            happiness

                            # Methods
                            eat(food)
                            play()
                            interact_with(other)
                            

                            # Class variables
                            species_name
                            scientific_name
                            calories_needed

                            # Instance variables
                            name
                            age
                            happiness

                            # Methods
                            eat(food)
                            play()
                            interact_with(other)
                            

Elephant and Rabbit are both animals, so they have similar attributes. Instead of repeating code, we can inherit the code.

Inheritance

Base classes and subclasses

When multiple classes share similar attributes, you can reduce redundant code by defining a base class and then subclasses can inherit from the base class.

Diagram of animal classes extending from Animal

Tip: The base class is also known as the superclass.

The base class

The base class contains method headers common to the subclasses, and code that is used by multiple subclasses.


                    class Animal:
                        species_name = "Animal"
                        scientific_name = "Animalia"
                        play_multiplier = 2
                        interact_increment = 1

                        def __init__(self, name, age=0):
                            self.name = name
                            self.age = age
                            self.calories_eaten  = 0
                            self.happiness = 0

                        def play(self, num_hours):
                            self.happiness += (num_hours * self.play_multiplier)
                            print("WHEEE PLAY TIME!")

                        def eat(self, food):
                            self.calories_eaten += food.calories
                            print(f"Om nom nom yummy {food.name}")
                            if self.calories_eaten > self.calories_needed:
                                self.happiness -= 1
                                print("Ugh so full")

                        def interact_with(self, animal2):
                            self.happiness += self.interact_increment
                            print(f"Yay happy fun time with {animal2.name}")
                    

The subclasses

To declare a subclass, put parentheses after the class name and specify the base class in the parentheses:


                    class Panda(Animal):
                    

Then the subclasses only need the code that's unique to them. They can redefine any aspect: class variables, method definitions, or constructor. A redefinition is called overriding.

The simplest subclass overrides nothing:


                    class AmorphousBlob(Animal):
                        pass
                    

Overriding class variables

Subclasses can override existing class variables and assign new class variables:


                    class Rabbit(Animal):
                        species_name = "European rabbit"
                        scientific_name = "Oryctolagus cuniculus"
                        calories_needed = 200
                        play_multiplier = 8
                        interact_increment = 4
                        num_in_litter = 12

                    class Elephant(Animal):
                        species_name = "African Savanna Elephant"
                        scientific_name = "Loxodonta africana"
                        calories_needed = 8000
                        play_multiplier = 4
                        interact_increment = 2
                        num_tusks = 2
                    

Exercise: LearnableContent


                    class LearnableContent:
                        """A base class for specific kinds of learnable content.
                        All kinds have title and author attributes,
                        but each kind may have additional attributes.
                        """
                        license = "Creative Commons"

                        def __init__(self, title, author):
                            self.title = title
                            self.author = author

                    # Create a Video subclass with
                    # license of "YouTube Standard License"

                    # Create an Article subclass with
                    # license of "CC-BY-NC-SA"

                    # Create a new Video instance with a title of "DNA" and an author of "Megan"
                    # Create a new Article instance with a title of "Water phases" and an author of "Lauren"
                    

Exercise: LearnableContent (solution)


                    class LearnableContent:
                        """A base class for specific kinds of learnable content.
                        All kinds have title and author attributes,
                        but each kind may have additional attributes.
                        """
                        license = "Creative Commons"

                        def __init__(self, title, author):
                            self.title = title
                            self.author = author

                    # Create a Video subclass with license of "YouTube Standard License"
                    class Video(LearnableContent):
                        license = "YouTube Standard License"
                    
                    # Create an Article subclass with license of "CC-BY-NC-SA"
                    class Article(LearnableContent):
                        license = "CC-BY-NC-SA"
                
                    # Create a new Video instance with a title of "DNA" and an author of "Megan"
                    dna_video = Video("DNA", "Megan")
                    
                    # Create a new Article instance with a title of "Water phases" and an author of "Lauren"
                    water_article = Article("Water phases", "Lauren")
                    

Overriding methods

If a subclass overrides a method, Python will use that definition instead of the superclass definition.


                    class Panda(Animal):
                        species_name = "Giant Panda"
                        scientific_name = "Ailuropoda melanoleuca"
                        calories_needed = 6000

                        def interact_with(self, other):
                            print(f"I'm a Panda, I'm solitary, go away {other.name}!")
                    

How would we call that method?


                    panda1 = Panda("Pandeybear", 6)
                    panda2 = Panda("Spot", 3)
                    panda1.interact_with(panda2)
                    

Exercise: Character methods


                    class Character:
                        """
                        >>> player = Character("Mario")
                        >>> player.name
                        'Mario'
                        >>> player.health
                        100
                        >>> player.damage(10)
                        >>> player.health
                        90
                        >>> player.boost(5)
                        >>> player.health
                        95
                        """
                        def __init__(self, name):
                            self.name = name
                            self.health = 100

                        def damage(self, amount):
                            self.health -= amount

                        def boost(self, amount):
                            self.health += amount
                    

                    class Boss(Character):
                        """
                        >>> mx_boss = Boss("Mx Boss Person")
                        >>> mx_boss.damage(100)
                        >>> mx_boss.health
                        99
                        >>> mx_boss.damage(10)
                        >>> mx_boss.health
                        98
                        >>> mx_boss.boost(1)
                        >>> mx_boss.health
                        100
                        """
                        def damage(self, amount):
                            # Bosses ignore the amount and instead
                            # always receive 1 unit of damage to their health

                        def boost(self, amount):
                            # Bosses always receive twice the amount
                            # of boost to their health
                    

Exercise: Character methods (solution)


                    class Character:
                        """
                        >>> player = Character("Mario")
                        >>> player.name
                        'Mario'
                        >>> player.health
                        100
                        >>> player.damage(10)
                        >>> player.health
                        90
                        >>> player.boost(5)
                        >>> player.health
                        95
                        """
                        def __init__(self, name):
                            self.name = name
                            self.health = 100

                        def damage(self, amount):
                            self.health -= amount

                        def boost(self, amount):
                            self.health += amount
                    

                    class Boss(Character):
                        """
                        >>> mx_boss = Boss("Mx Boss Person")
                        >>> mx_boss.damage(100)
                        >>> mx_boss.health
                        99
                        >>> mx_boss.damage(10)
                        >>> mx_boss.health
                        98
                        >>> mx_boss.boost(1)
                        >>> mx_boss.health
                        100
                        """
                        def damage(self, amount):
                            # Bosses ignore the amount and instead
                            # always receive 1 unit of damage to their health
                            self.health -= 1

                        def boost(self, amount):
                            # Bosses always receive twice the
                            # amount of boost to their health
                            self.health += amount * 2
                    

Exercise: Clothing


                        class Clothing:
                            """
                            >>> blue_shirt = Clothing("shirt", "blue")
                            >>> blue_shirt.category
                            'shirt'
                            >>> blue_shirt.color
                            'blue'
                            >>> blue_shirt.is_clean
                            True
                            >>> blue_shirt.wear()
                            >>> blue_shirt.is_clean
                            False
                            >>> blue_shirt.clean()
                            >>> blue_shirt.is_clean
                            True
                            """
                        
                            def __init__(self, category, color):
                                self.category = category
                                self.color = color
                                self.is_clean = True
                        
                            def wear(self):
                                self.is_clean = False
                        
                            def clean(self):
                                self.is_clean = True
                        

                        class KidsClothing(Clothing):
                            """
                            >>> onesie = KidsClothing("onesie", "polka dots")
                            >>> onesie.wear()
                            >>> onesie.is_clean
                            False
                            >>> onesie.clean()
                            >>> onesie.is_clean
                            False
                            >>> dress = KidsClothing("dress", "rainbow")
                            >>> dress.clean()
                            >>> dress.is_clean
                            True
                            >>> dress.wear()
                            >>> dress.is_clean
                            False
                            >>> dress.clean()
                            >>> dress.is_clean
                            False
                            """
                          
                            # Override the clean() method
                            # so that kids clothing always stays dirty!
                        

Exercise: Clothing (solution)


                        class Clothing:
                            """
                            >>> blue_shirt = Clothing("shirt", "blue")
                            >>> blue_shirt.category
                            'shirt'
                            >>> blue_shirt.color
                            'blue'
                            >>> blue_shirt.is_clean
                            True
                            >>> blue_shirt.wear()
                            >>> blue_shirt.is_clean
                            False
                            >>> blue_shirt.clean()
                            >>> blue_shirt.is_clean
                            True
                            """
                        
                            def __init__(self, category, color):
                                self.category = category
                                self.color = color
                                self.is_clean = True
                        
                            def wear(self):
                                self.is_clean = False
                        
                            def clean(self):
                                self.is_clean = True
                        

                        class KidsClothing(Clothing):
                            """
                            >>> onesie = KidsClothing("onesie", "polka dots")
                            >>> onesie.wear()
                            >>> onesie.is_clean
                            False
                            >>> onesie.clean()
                            >>> onesie.is_clean
                            False
                            >>> dress = KidsClothing("dress", "rainbow")
                            >>> dress.clean()
                            >>> dress.is_clean
                            True
                            >>> dress.wear()
                            >>> dress.is_clean
                            False
                            >>> dress.clean()
                            >>> dress.is_clean
                            False
                            """
                          
                            # Override the clean() method
                            # so that kids clothing always stays dirty!
                            def clean(self):
                              self.is_clean = self.is_clean
                        

Using methods from the base class

To refer to a superclass method, we can use super():


                    class Lion(Animal):
                        species_name = "Lion"
                        scientific_name = "Panthera"
                        calories_needed = 3000

                        def eat(self, food):
                            if food.type == "meat":
                                super().eat(food)
                    

How would we call that method?


                    bones = Food("Bones", "meat")
                    mufasa = Lion("Mufasa", 10)
                    mufasa.eat(bones)
                    

More on super()

super().attribute refers to the definition of attribute in the superclass of the first parameter to the method.


                    def eat(self, food):
                        if food.type == "meat":
                            super().eat(food)
                    

...is the same as:


                    def eat(self, food):
                        if food.type == "meat":
                            Animal.eat(self, food)
                    

super() is better style than BaseClassName, though slightly slower.

Overriding __init__

Similarly, we need to explicitly call super().__init__() if we want to call the __init__ functionality of the base class.


                    class Elephant(Animal):
                        species_name = "Elephant"
                        scientific_name = "Loxodonta"
                        calories_needed = 8000

                        def __init__(self, name, age=0):
                            super().__init__(name, age)
                            if age < 1:
                                self.calories_needed = 1000
                            elif age < 5:
                                self.calories_needed = 3000
                    

What would this display?


                    elly = Elephant("Ellie", 3)
                    elly.calories_needed        # 3000
                    

Exercise: Catplay


                        class Animal:
                            species_name = "Animal"
                            scientific_name = "Animalia"
                            play_multiplier = 2
                            interact_increment = 1
                        
                            def __init__(self, name, age=0):
                                self.name = name
                                self.age = age
                                self.calories_eaten  = 0
                                self.happiness = 0
                        
                            def play(self, num_hours):
                                self.happiness += (num_hours * self.play_multiplier)
                                print("WHEEE PLAY TIME!")
                        
                            def eat(self, food):
                                self.calories_eaten += food.calories
                                print(f"Om nom nom yummy {food.name}")
                                if self.calories_eaten > self.calories_needed:
                                    self.happiness -= 1
                                    print("Ugh so full")
                        
                            def interact_with(self, animal2):
                                self.happiness += self.interact_increment
                                print(f"Yay happy fun time with {animal2.name}")
                        

                        class Cat(Animal):
                            """
                            >>> adult = Cat("Winston", 12)
                            >>> adult.name
                            'Winston'
                            >>> adult.age
                            12
                            >>> adult.play_multiplier
                            3
                            >>> kitty = Cat("Kurty", 0.5)
                            >>> kitty.name
                            'Kurty'
                            >>> kitty.age
                            0.5
                            >>> kitty.play_multiplier
                            6
                            """
                            species_name = "Domestic cat"
                            scientific_name = "Felis silvestris catus"
                            calories_needed = 200
                            play_multiplier = 3
                          
                            def __init__(self, name, age):
                              #  Call the super class to set name and age
                              # If age is less than 1, set play multiplier to 6
                        

Exercise: Catplay (solution)


                        class Animal:
                            species_name = "Animal"
                            scientific_name = "Animalia"
                            play_multiplier = 2
                            interact_increment = 1
                        
                            def __init__(self, name, age=0):
                                self.name = name
                                self.age = age
                                self.calories_eaten  = 0
                                self.happiness = 0
                        
                            def play(self, num_hours):
                                self.happiness += (num_hours * self.play_multiplier)
                                print("WHEEE PLAY TIME!")
                        
                            def eat(self, food):
                                self.calories_eaten += food.calories
                                print(f"Om nom nom yummy {food.name}")
                                if self.calories_eaten > self.calories_needed:
                                    self.happiness -= 1
                                    print("Ugh so full")
                        
                            def interact_with(self, animal2):
                                self.happiness += self.interact_increment
                                print(f"Yay happy fun time with {animal2.name}")
                        

                        class Cat(Animal):
                            """
                            >>> adult = Cat("Winston", 12)
                            >>> adult.name
                            'Winston'
                            >>> adult.age
                            12
                            >>> adult.play_multiplier
                            3
                            >>> kitty = Cat("Kurty", 0.5)
                            >>> kitty.name
                            'Kurty'
                            >>> kitty.age
                            0.5
                            >>> kitty.play_multiplier
                            6
                            """
                            species_name = "Domestic cat"
                            scientific_name = "Felis silvestris catus"
                            calories_needed = 200
                            play_multiplier = 3
                          
                            def __init__(self, name, age):
                              super().__init__(name, age)
                              if self.age < 1:
                                self.play_multiplier = 6
                        

Exercise: Dog weight


                        class Animal:
                            species_name = "Animal"
                            scientific_name = "Animalia"
                            play_multiplier = 2
                            interact_increment = 1
                        
                            def __init__(self, name, age=0):
                                self.name = name
                                self.age = age
                                self.calories_eaten  = 0
                                self.happiness = 0
                        
                            def play(self, num_hours):
                                self.happiness += (num_hours * self.play_multiplier)
                                print("WHEEE PLAY TIME!")
                        
                            def eat(self, food):
                                self.calories_eaten += food.calories
                                print(f"Om nom nom yummy {food.name}")
                                if self.calories_eaten > self.calories_needed:
                                    self.happiness -= 1
                                    print("Ugh so full")
                        
                            def interact_with(self, animal2):
                                self.happiness += self.interact_increment
                                print(f"Yay happy fun time with {animal2.name}")
                        

                        class Dog(Animal):
                            """
                            >>> spot = Dog("Spot", 5, 20)
                            >>> spot.name
                            'Spot'
                            >>> spot.age
                            5
                            >>> spot.weight
                            20
                            >>> spot.calories_needed
                            400
                            >>> puppy = Dog("Poppy", 1, 7)
                            >>> puppy.name
                            'Poppy'
                            >>> puppy.age
                            1
                            >>> puppy.weight
                            7
                            >>> puppy.calories_needed
                            140
                            """
                            species_name = "Domestic dog"
                            scientific_name = "Canis lupus familiaris"
                            calories_needed = 200
                          
                            def __init__(self, name, age, weight):
                              #  Call the super class to set name and age
                              #  Set the weight attribute
                              #  Set calories_needed to 20x the weight
                        

Exercise: Dog weight (solution)


                        class Animal:
                            species_name = "Animal"
                            scientific_name = "Animalia"
                            play_multiplier = 2
                            interact_increment = 1
                        
                            def __init__(self, name, age=0):
                                self.name = name
                                self.age = age
                                self.calories_eaten  = 0
                                self.happiness = 0
                        
                            def play(self, num_hours):
                                self.happiness += (num_hours * self.play_multiplier)
                                print("WHEEE PLAY TIME!")
                        
                            def eat(self, food):
                                self.calories_eaten += food.calories
                                print(f"Om nom nom yummy {food.name}")
                                if self.calories_eaten > self.calories_needed:
                                    self.happiness -= 1
                                    print("Ugh so full")
                        
                            def interact_with(self, animal2):
                                self.happiness += self.interact_increment
                                print(f"Yay happy fun time with {animal2.name}")
                        

                        class Dog(Animal):
                            """
                            >>> spot = Dog("Spot", 5, 20)
                            >>> spot.name
                            'Spot'
                            >>> spot.age
                            5
                            >>> spot.weight
                            20
                            >>> spot.calories_needed
                            400
                            >>> puppy = Dog("Poppy", 1, 7)
                            >>> puppy.name
                            'Poppy'
                            >>> puppy.age
                            1
                            >>> puppy.weight
                            7
                            >>> puppy.calories_needed
                            140
                            """
                            species_name = "Domestic dog"
                            scientific_name = "Canis lupus familiaris"
                            calories_needed = 200
                          
                            def __init__(self, name, age, weight):
                              super().__init__(name, age)
                              self.weight = weight
                              self.calories_needed = 20 * weight
                        

Layers of inheritance

Object base class

Every Python 3 class implicitly extends the object class.

Diagram of Animal extending from object

Adding layers of inheritance

But we can also add in more levels ourselves.

Diagram of animals extending from Herbivore and Carnivore which extend from Animal which extends from object

Adding layers of inheritance

First we define the new classes:


                    class Herbivore(Animal):

                        def eat(self, food):
                            if food.type == "meat":
                                self.happiness -= 5
                            else:
                                super().eat(food)

                    class Carnivore(Animal):

                        def eat(self, food):
                            if food.type == "meat":
                                super().eat(food)
                    

Then we change the base classes for the subclasses:


                    class Rabbit(Herbivore):
                    class Panda(Herbivore):
                    class Elephant(Herbivore):

                    class Vulture(Carnivore):
                    class Lion(Carnivore):
                    

Multiple inheritance

Multiple inhteritance

A class may inherit from multiple base classes in Python.

Diagram of some animals extending from both Carnivore/Predator and Herbivore/Prey

The new base classes

First we define the new base classes:


                    class Predator(Animal):

                        def interact_with(self, other):
                            if other.type == "meat":
                                self.eat(other)
                                print("om nom nom, I'm a predator")
                            else:
                                super().interact_with(other)

                    class Prey(Animal):
                        type = "meat"
                        calories = 200
                    

Inheriting from multiple base classes

Then we inherit from them by putting both names in the parentheses:


                    class Rabbit(Prey, Herbivore):
                    class Lion(Predator, Carnivore):
                    

Python can find the attributes in any of the base classes:


                    r = Rabbit("Peter", 4)           # Animal __init__
                    r.play(1)                        # Animal method
                    r.type                           # Prey class variable
                    r.eat(Food("carrot", "veggies")) # Herbivore method
                    l = Lion("Scar", 12)             # Animal __init__
                    l.eat(Food("zazu", "meat"))      # Carnivore method
                    l.interact_with(r)               # Predator method
                    

Identity

Checking identity

exp0 is exp1
evaluates to True if both exp0 and exp1 evaluate to the same object


                    mufasa = Lion("Mufasa", 15)
                    nala = Lion("Nala", 8)

                    mufasa is mufasa     # True
                    mufasa is nala       # False
                    mufasa is not nala   # True
                    nala is not None     # True
                    

Composition

Composition

An object can contain references to objects of other classes.

What examples of composition are in an animal conservatory?

  • An animal has a mate.
  • An animal has a mother.
  • An animal has children.
  • A conservatory has animals.

Referencing other instances

An instance variable can refer to another instance:


                    class Animal:

                        def mate_with(self, other):
                            if other is not self and other.species_name == self.species_name:
                                self.mate = other
                                other.mate = self
                    

How would we call that method?


                    mr_wabbit = Rabbit("Mister Wabbit", 3)
                    jane_doe = Rabbit("Jane Doe", 2)
                    mr_wabbit.mate_with(jane_doe)
                    

Referencing a list of instances

An instance variable can also refer to a list of instances:


                    class Rabbit(Animal):

                        def reproduce_like_rabbits(self):
                            if self.mate is None:
                                print("oh no! better go on ZoOkCupid")
                                return
                            self.babies = []
                            for _ in range(0, self.num_in_litter):
                                self.babies.append(Rabbit("bunny", 0))
                    

How would we call that function?


                    mr_wabbit = Rabbit("Mister Wabbit", 3)
                    jane_doe = Rabbit("Jane Doe", 2)
                    mr_wabbit.mate_with(jane_doe)
                    jane_doe.reproduce_like_rabbits()
                    

Relying on a common interface

If all instances implement a method with the same function signature, a program can rely on that method across instances of different subclasses.


                    def partytime(animals):
                        """Assuming ANIMALS is a list of Animals, cause each
                        to interact with all the others exactly once."""
                        for i in range(len(animals)):
                            for j in range(i + 1, len(animals)):
                                animals[i].interact_with(animals[j])
                    

How would we call that function?


                    elly = Elephant("Elly", 5)
                    pandy = Panda("PandeyBear", 4)
                    scar = Lion("Scar", 12)
                    jane_doe = Rabbit("Jane Doe", 2)
                    partytime([elly, pandy, scar, jane_doe])
                    

Composition vs. Inheritance

Inheritance is best for representing "is-a" relationships

  • Rabbit is a specific type of Animal
  • So, Rabbit inherits from Animal

Composition is best for representing "has-a" relationships

  • A conservatory has a collection of animals it cares for
  • So, a conservatory has a list of animals as an instance variable

Quiz

What would Python print?


                    class Parent:
                        def f(s):
                            print("Parent.f")

                        def g(s):
                            s.f()

                    class Child(Parent):
                        def f(me):
                            print("Child.f")

                    a_child = Child()
                    a_child.g()