Appearance
Inheritance and Polymorphism
In OOP (Object-Oriented Programming) design, when we define a class, we can inherit from an existing class. The new class is called a subclass, while the class being inherited from is referred to as the base class, parent class, or super class.
For example, we have already written a class named Animal
with a run()
method that prints directly:
python
class Animal(object):
def run(self):
print('Animal is running...')
When we need to write Dog
and Cat
classes, we can inherit directly from the Animal
class:
python
class Dog(Animal):
pass
class Cat(Animal):
pass
For Dog
, Animal
is its parent class; for Animal
, Dog
is its subclass. Cat
is similar to Dog
.
What are the benefits of inheritance? The biggest benefit is that the subclass inherits all the functionalities of the parent class. Since Animal
implements the run()
method, both Dog
and Cat
, as its subclasses, automatically inherit the run()
method without doing anything:
python
dog = Dog()
dog.run()
cat = Cat()
cat.run()
The output is:
Animal is running...
Animal is running...
Of course, we can also add some methods to the subclass, like in the Dog
class:
python
class Dog(Animal):
def run(self):
print('Dog is running...')
def eat(self):
print('Eating meat...')
The second benefit of inheritance requires us to make a slight improvement to the code. You may have noticed that when either Dog
or Cat
runs, it displays Animal is running...
, which is logically inconsistent. The proper approach is to display Dog is running...
and Cat is running...
respectively, so we modify the Dog
and Cat
classes as follows:
python
class Dog(Animal):
def run(self):
print('Dog is running...')
class Cat(Animal):
def run(self):
print('Cat is running...')
When we run the code again, the output is:
Dog is running...
Cat is running...
When both the subclass and the parent class have a run()
method, we say that the subclass's run()
method overrides the parent class's run()
. At runtime, the subclass's run()
method is always called. This gives us another benefit of inheritance: polymorphism.
To understand what polymorphism is, we first need to clarify data types. When we define a class, we are actually defining a new data type. Our defined data types are no different from Python's built-in data types like str
, list
, and dict
:
python
a = list() # a is of type list
b = Animal() # b is of type Animal
c = Dog() # c is of type Dog
We can use isinstance()
to check whether a variable is of a certain type:
python
>>> isinstance(a, list)
True
>>> isinstance(b, Animal)
True
>>> isinstance(c, Dog)
True
It appears that a
, b
, and c
indeed correspond to the types list
, Animal
, and Dog
.
However, let's try this:
python
>>> isinstance(c, Animal)
True
It seems c
is not just a Dog
; c
is also an Animal
!
This makes sense because Dog
inherits from Animal
. When we create an instance of Dog
, we are correct in saying that its data type is Dog
, but it is also correct to say that it is an Animal
because Dog
is a kind of Animal
!
Therefore, in an inheritance relationship, if an instance's data type is a subclass, it can also be considered a parent class type. However, the reverse is not true:
python
>>> b = Animal()
>>> isinstance(b, Dog)
False
A Dog
can be considered an Animal
, but an Animal
cannot be considered a Dog
.
To understand the benefits of polymorphism, we need to write a function that accepts a variable of type Animal
:
python
def run_twice(animal):
animal.run()
animal.run()
When we pass an instance of Animal
, run_twice()
will print:
python
>>> run_twice(Animal())
Animal is running...
Animal is running...
When we pass an instance of Dog
, run_twice()
will print:
python
>>> run_twice(Dog())
Dog is running...
Dog is running...
When we pass an instance of Cat
, run_twice()
will print:
python
>>> run_twice(Cat())
Cat is running...
Cat is running...
At first glance, this may seem trivial. However, consider that if we define a Tortoise
type that also derives from Animal
:
python
class Tortoise(Animal):
def run(self):
print('Tortoise is running slowly...')
When we call run_twice()
and pass an instance of Tortoise
:
python
>>> run_twice(Tortoise())
Tortoise is running slowly...
Tortoise is running slowly...
You will find that adding a new subclass of Animal
requires no modifications to run_twice()
. In fact, any function or method that relies on Animal
as a parameter can operate without modification, thanks to polymorphism.
The benefit of polymorphism is that when we need to pass Dog
, Cat
, Tortoise
, etc., we only need to accept Animal
types because Dog
, Cat
, Tortoise
, etc., are all Animal
types. Then, we can operate based on the Animal
type. Since Animal
has a run()
method, any type passed in, as long as it is an instance of Animal
or a subclass, will automatically call the appropriate run()
method for that specific type. This is the essence of polymorphism:
For a variable, we only need to know it is of Animal
type without needing to know its exact subclass. We can safely call the run()
method, and the specific run()
method invoked depends on the exact type of the object at runtime—whether it is an Animal
, Dog
, Cat
, or Tortoise
. This is the true power of polymorphism: the caller simply calls without worrying about the details, and when we add a new subclass of Animal
, as long as the run()
method is correctly implemented, we don't need to worry about how the original code calls it. This is known as the famous "Open/Closed Principle":
- Open for extension: allows for new subclasses of
Animal
to be added. - Closed for modification: no need to modify functions like
run_twice()
that depend on theAnimal
type.
Inheritance can also be multi-level, similar to the relationship from grandfather to father to son. Any class can ultimately trace back to the root class object
. These inheritance relationships resemble an upside-down tree. For example, consider the following inheritance tree:
┌───────────────┐
│ object │
└───────────────┘
│
┌────────────┴────────────┐
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Animal │ │ Plant │
└─────────────┘ └─────────────┘
│ │
┌─────┴──────┐ ┌─────┴──────┐
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Dog │ │ Cat │ │ Tree │ │ Flower │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
Static Languages vs. Dynamic Languages
In static languages (like Java), if you need to pass an Animal
type, the object being passed must be of type Animal
or one of its subclasses; otherwise, you will not be able to call the run()
method.
In dynamic languages like Python, it is not strictly necessary to pass an Animal
type. We only need to ensure that the object has a run()
method:
python
class Timer(object):
def run(self):
print('Start...')
This is the "duck typing" characteristic of dynamic languages; it does not require a strict inheritance hierarchy. An object can be treated as a certain type as long as it "looks like a duck and walks like a duck."
Python's "file-like object" is an example of duck typing. For a real file object, it has a read()
method that returns its contents. However, many objects are considered "file-like objects" as long as they have a read()
method. Many functions accept parameters of "file-like objects," meaning you do not have to pass a real file object; any object implementing the read()
method can be passed instead.
Summary
Inheritance allows a subclass to directly inherit all
the functionalities of the parent class, eliminating the need to start from scratch. The subclass only needs to add its unique methods or can override unsuitable methods from the parent class.
The duck typing characteristic of dynamic languages like Python determines that inheritance is not as mandatory as it is in static languages.