Lesson 7 - Inheritance and polymorphism in Python
In the previous lesson, Arena with warriors in Python, we made an arena that simulates two warriors in battle. In today's Python tutorial, we're going to expand our knowledge of object-oriented programming. In the introduction of this course, we mentioned that the OOP is based on three fundamental concepts: encapsulation, inheritance and polymorphism. We're already familiar with the encapsulation and learned to use underscores to declare private class members. Today, we're going to be taking a look at the two remaining concepts.
Inheritance
Inheritance is one of the basic features of OOP and is used to create new data structures based on old ones. Let's look at a simple example:
Imagine that we were hired to create a database that will keep track of animals at a zoo There will be two types of users in our system: users and administrators. Users will be common zookeepers who will be able to edit information about the animals, e.g. their weight or wingspan. The administrator will be able to modify animal data as well and add or delete animals from the database. Administrators will have an extra attribute, a phone number so they can be contacted in case of system failure. It'd be pointless and confusing to redeclare the new class completely since they have many attributes and methods in common. Both the user and the administrator will have names, ages and be able to log in and log out. Today will mostly be about going over the concepts, we'll program a fully functioning example in the next lesson (the following code is just a draft):
class User: def __init__(self, name, password, age): self.__name = name self.__password = password self.__age = age def log_in(self, password): ... def log_out(self): ... def set_weight(self, animal): ... ...
We've just made a general outline of the class, but I'm sure you get the gist
of it. Without knowing what inheritance is, we would define our
Admin
class like this:
class Admin: def __init__(self, name, password, age, phone_number): self.__name = name self.__password = password self.__age = age self.__phone_number = phone_number def log_in(self, password): ... def log_out(self): ... def set_weight(self, animal): ... def add_animal(self, animal): ... def remove_animal(self, animal): ... ...
We can see that our code is very redundant in this class. All further changes
would have to take a place in both classes, it'd make the code very hard to
maintain. Now, we'll declare the Admin
class using inheritance.
We'll let the Admin
class to be inherited from the
User
class. We don't have to declare attributes and methods again,
Python will add them automatically:
class Admin(User): def __init__(self, name, password, age, phone_number): super().__init__(name, password, age) self.__phone_number = phone_number def add_animal(self, animal): ... def remove_animal(self, animal): ... ...
We see that we've used parentheses ()
for inheritance. We write
there the name of the class we're inheriting from.
Don't mind the "odd" super()
method for now, we'll explain it
later as it's necessary if we want to call the parent's method. But back to the
example.
In the example above, the private attributes won't be accessible to the
descendant. Only the attributes and methods will. Private attributes and methods
are understood as a special logic that belongs to a certain class. Meaning that,
they are hidden from descendants - even a descendant is actually using this
logic, it's unable to modify it. To achieve the desired result, we'll use a
new access modifier - an underscore _
, which works
just like two underscores __
but allows the inheritance of these
attributes. For programmers, it means. "You can see this, but don't use it!". We
call such attributes protected.
The beginning of the User
class would look like the
following:
class User: def __init__(self, name, password, age): self._name = name self._password = password self._again = again
Now, if we created user and administrator instances, they'd both have
name
attributes and log_in()
methods. Python will
inherit the User
class and redeclare all its attributes
automatically.
The advantages of using inheritance are clear. We don't have to copy the same attributes to two classes. All we have to do is declare new ones. The rest will be inherited. The benefits are tremendous, we can extend existing components of new methods and reuse them. We don't have to write lots of redundant code, and most importantly - when we change a single attribute in the parent class, this change is then inherited everywhere automatically. We don't need to change something manually in 20 classes, which could potentially cause errors. We're humans and we'll always make mistakes, we have to choose the right programming techniques to make as less mistakes as possible.
The parent class is sometimes called the ancestor, our User
class here, and the class that inherits from it, a descendant, our
Admin
class. Descendants may add new methods or override the
existing methods of the parent class (see example below). Sometimes, you may
encounter the terms superclass and subclass which essentially mean the same
thing.
Another way to design the object model would be to create a
User
parent class whose sole purpose would be inheritance. The
User
would be then inherited by Attendant
and
Admin
classes. This technique would be useful if we had lots of
user types. In that case, we're talking about a class hierarchy, which we'll get
into further along in this online course. Our example was simple so two classes
were enough. There are also design
patterns that are established and approved object models used for common
situations. All of that stuff is advanced and interesting, and I promise we'll
get to them later as well. In object modeling, we draw inheritance as an empty
arrow pointing to the parent class. In our case, the graphical notation would
look like this:
Class type check
In Python, we can ask whether an object is an instance of a particular class.
1. We can use the build-in type()
class for it:
>>> a = 1 >>> type(a) == type(1) True >>> type(a) == type("Python!") False >>> type(a) == int True >>> type(a) == str False
2. But it's better to use the build-in isinstance()
function:
>>> a = 1 >>> isinstance(a, 1) True >>> isinstance(a, "Python") False >>> isinstance(a, int) True >>> isinstance(a, str) False
All objects in Python inherit from the object
class. In Python
2.X, it was necessary to inherit our classes from this one. Now, Python will
inherit it automatically.
Languages can support either single or "multiple inheritance". With the single inheritance, a class can inherit only from one other class. With the "multiple inheritance", a class can inherit from several classes at the same time. Python supports multiple inheritance, similarly as the C++ language does.
Polymorphism
Don't be scared of the obscure name of this technique, it's actually very
simple. Polymorphism allows us to use a unified interface to work with objects
of different types. Imagine that we have, for example, many objects representing
geometric shapes (circles, squares, triangles, and so on). It'd certainly be
helpful if we could communicate with them in some unified way even though
they're different. We can create a GeometricShape
class containing
a color
attribute and a render()
method. All the
geometric shapes would then inherit the interface from this class. Now you may
be thinking, "But the circle and square object would render differently!." Well,
polymorphism allows us to override the
render()
method in every subclass so it
will do what we want. The interface will be unified and we won't have
to think about which method to call to render different objects.
Polymorphism is often explained using animals. All having a
speak()
method in their interface, but each animal performs it
differently.
The essence of polymorphism is a method or methods, that all the descendants
have defined with the same heads, but with different method bodies. We'll use
polymorphism along with inheritance in the next lesson, Arena with a mage in Python (inheritance and polymorphism), on our warriors
in the arena. We'll add a mage who will inherit warrior's attributes and
behavior and override the warrior's attack()
method to use mana. We
won't be able to tell that he's not a pure warrior from the outside since he
will have the same interface. It'll be fun