COMP 364: A brief interlude on inheritance

Recall that we can understand the world as a hierarchy of types.

The same can be done with Python types:

A very important feature of object-oriented programming languages is inheritance.

A derived (aka child) class is a "specialization" of its base (aka parent) class.

Child classes (subclasses) inherit all attributes of the parent class (base class).

In order to distinguish themselves, child classes can define their own versions of inherited attributes "override", or define their own additional attributes.

We already saw how to make our own class.

When we don't specify any parent class, the parent class is object by default.

In [148]:
#inherits all its atributes from `object` class
class MyClass:
    pass

We can use the built-in function issubclass(first_class, second_class) to see if a class is a subclass of another.

In [151]:
#let's create an instance of the type MyClass
issubclass(MyClass, object)
Out[151]:
True

Since MyClass doesn't define any of its own new attributes or override any of object's attributes it have exactly all of object's attributes.

In [153]:
#calls the __init__() method from `object` class
m = MyClass()

Another useful built-in function is isinstance(object, class) which tells us if an object is an instance of a given class.

In [154]:
isinstance(m, MyClass)
Out[154]:
True
In [155]:
isinstance(m, object)
Out[155]:
True

The useful thing with inheritance is that it lets us create specialized types without having to re-define every attribute and method each time.

The syntax for creating a subclass from a base class is as follows:

class Parent:
    pass
class Child(Parent):
    pass

When creating a child (subclass) class, you specify in round brackets, the name of the parent class.

This tells Python to look in the parent class for any attributes of the Child class that get called it they are not defined in the child class.

Note:

In [156]:
class MyClass:
    pass

Is equivalent to

In [ ]:
class MyClass(object):
    pass

Let's make a class that describes Animals in general.

Just for clarity we're explicitly inheriting from object.

In [157]:
class Animal(object):
    def __init__(self, n):
        self.num_legs = n

Now we can create instances of the Animal class.

In [158]:
a = Animal(4)

When Python print() is called on on object, it calls that object's __str__ method.

In [159]:
print(a)
<__main__.Animal object at 0x10889a588>

We want a prettier print message than the parent's, so let's override it.

In [160]:
class Animal(object):
    def __init__(self, n, e):
        self.num_legs = n
        self.food = e
    def __str__(self):
        return f"This animal has {self.num_legs} legs and is a {self.food} eater."
In [161]:
a = Animal(4, "meat")
In [162]:
print(a)
This animal has 4 legs and is a meat eater.

Python looks first in the child class for __str__ because it found a definition it stops looking and calls it instead of the parent's.

Now let's specialize the Animal class with Dog.

Dogs should also have num_legs and food attributes so let's initialize them using the parent's __init__.

In [171]:
class Dog(Animal):
    def __init__(self, b):
        Animal.__init__(self, 4, "meat")
        self.breed = b
In [172]:
pyr = Dog("Great Pyrenees")
In [173]:
print(pyr)
This animal has 4 legs and is a meat eater.
In [174]:
print(pyr.breed)
Great Pyrenees

Child classes can define their own methods.

In [175]:
class Dog(Animal):
    def __init__(self, b):
        """
        call the parent init function.
            equivalent to:
                self.num_legs = 4
                self.food = "meat" 
        """

        Animal.__init__(self, 4, "meat")
        self.breed = b
    def bark(self):
        if self.breed == "Great Pyrenees":
            print("WOOF " * 10)
        else:
            print("woof")
In [176]:
p = Dog("Great Pyrenees")
p.bark()
WOOF WOOF WOOF WOOF WOOF WOOF WOOF WOOF WOOF WOOF 
In [177]:
d = Dog("Golden Retriever")
d.bark()
woof

Objects of the parent class won't have a bark() method.

In [178]:
a = Animal(4, "meat")

a.bark()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-178-90dc5d3b38a0> in <module>()
      1 a = Animal(4, "meat")
      2 
----> 3 a.bark()

AttributeError: 'Animal' object has no attribute 'bark'

Let's say we want a better print message than the generic Animal one.

In [179]:
class Dog(Animal):
    def __init__(self, b):
        """
        call the parent init function.
            equivalent to:
                self.num_legs = 4
                self.food = "meat" 
        """

        Animal.__init__(self, 4, "meat")
        self.breed = b
    def bark(self):
        if self.breed == "Great Pyrenees":
            print("WOOF " * 10)
        else:
            print("woof")
    def __str__(self):
        return Animal.__str__(self) + f" And it is a {self.breed} dog."
In [180]:
p = Dog("Great Pyrenees")
In [181]:
print(p)
This animal has 4 legs and is a meat eater. And it is a Great Pyrenees dog.

We can inherit from Animal again to define a new kind of animal.

In [182]:
class Cat(Animal):
    def __init__(self, c):
        Animal.__init__(self, 4, "mice")
        self.color = c
    
In [183]:
c = Cat("Black")

Now we have something like this:

                        +-----------+
                        |           |
                        |   object  |
                        +-----------+
                              ^
                              |
                              |
                        +-----+-----+
                        |   Animal  |
                        |           |
                        +-----------+
                            ^ ^
                            | |
                 +----------+ +------------+
                 |                         |
          +------+----+               +----+------+
          |  Dog      |               |    Cat    |
          |           |               |           |
          +-----------+               +-----------+
In [184]:
class SpecificDog(Dog):
    def __init__(self, name, breed):
        Dog.__init__(self, breed)
        self.name = name
In [185]:
mydog = SpecificDog("Sasja", "Great Pyrenees")
In [186]:
mydog.name
Out[186]:
'Sasja'
In [187]:
mydog.bark()
WOOF WOOF WOOF WOOF WOOF WOOF WOOF WOOF WOOF WOOF 
                        +-----------+
                        |           |
                        |   object  |
                        +-----------+
                              ^
                              |
                              |
                        +-----+-----+
                        |   Animal  |
                        |           |
                        +-----------+
                            ^ ^
                            | |
                 +----------+ +------------+
                 |                         |
          +------+----+               +----+------+
          |  Dog      |               |    Cat    |
          |           |               |           |
          +-----------+               +-----------+
               ^
               |
               |               
         +-----+-----+
         |SpecificDog|
         |           |
         +-----------+

Summary

  • Classes/types define the attributes of objects.
  • Every object belongs to a class/type
  • Classes can inherit attributes from parent (base) classes.
  • Derived classes have all their parents attributes except:
    • New attributes defined in the derived classs
    • Attributes that were overridden in the derived class

I will define below all the clases we defined above but without using inheritance (other than default inheritance from object).

In [ ]:
class Animal:
    def __init__(self, n, f):
        self.num_legs = n
        self.food = f
    def __str__(self):
        return f"This animal has {self.num_legs} legs and is a {self.food} eater."
class Dog:
    def __init__(self, b):
        self.num_legs = 4
        self.food = "meat"
        self.breed = b
    def bark(self):
        if self.breed == "Great Pyrenees":
            print("WOOF " * 10)
        else:
            print("woof")
    def __str__(self):
        return f"This animal has {self.num_legs} legs and is a {self.food} eater. And it is a {self.breed} dog."
class Cat:
    def __init__(self, c):
        self.num_legs = 4
        self.food = "mice"
        self.breed = b
class SpecificDog:
    def __init__(self, n, b):
        self.num_legs = 4
        self.food = "meat"
        self.breed = b
        self.name = n

Lots of repetitive code!

versus:

In [ ]:
class Animal(object):
    def __init__(self, n, e):
        self.num_legs = n
        self.food = e
    def __str__(self):
        return f"This animal has {self.num_legs} legs and is a {self.food} eater."
    
class Dog(Animal):
    def __init__(self, b):
        Animal.__init__(self, 4, "meat")
        self.breed = b
    def bark(self):
        if self.breed == "Great Pyrenees":
            print("WOOF " * 10)
        else:
            print("woof")
    def __str__(self):
        return Animal.__str__(self) + f" And it is a {self.breed} dog."

class Cat(Animal):
    def __init__(self, c):
        Animal.__init__(self, 4, "mice")
        self.color = c
        
class SpecificDog(Dog):
    def __init__(self, name, breed):
        Dog.__init__(self, breed)
        self.name = name

Now you can customize any class and add your own functionality to it.

Note that the name of the class you are extending has to be accessible from the current namespace.

For example, if we wanted to specialize the Seq object we have to import it into our namespace.

In [188]:
from Bio.Seq import Seq
In [189]:
class MySeq(Seq):
    def __init__(self, seq):
        Seq.__init__(self, seq)
        self.myattribute = "HELLO"
    #Seq doesn't compute GC content so I'll add that functionality
    def compute_gc(self):
        return len([b for b in self if b in ["G", "C"]]) / len(self)
In [190]:
s = MySeq("AAATTCGAGAG")
In [191]:
print(s.transcribe())
AAAUUCGAGAG
In [192]:
print(s.myattribute)
HELLO

I added my own attributes without having to re-write the whole Seq class.

In [193]:
s.compute_gc()
Out[193]:
0.36363636363636365
In [ ]:
class Student:
    def __init__(self, name, gpa):
        self.name = name
        self.gpa = gpa
class Course:
    def __init__(self, students):
        self.students = studnet
In [ ]:
students = []
with open("students.txt", "r") as s:
    for line in s:
        students.append(Student(line[0], line[1]))
for s in students:
    if s.grade > 2:
        print(s.name)