Introduction to Python Programming. Section 15. Object-Oriented Programming II – Class Inheritance

15 Object-Oriented Programming II - Class Inheritance

You already know from Section 14 that the main idea of Object-Oriented Programming (OOP) is encapsulation. A class is a container that keeps together some data and functions which work with the data. This brings a lot of structure and clarity into the software. The data are called attributes and the functions are called methods. You also know that a class is an abstract concept – such as a blueprint of a house. An object is then a concrete instance (materialization) of the class – such as a concrete house which is built using the blueprint.

15.1 Objectives

In this section we will venture further into OOP and explore class inheritance. You will:

  • Understand the benefits of class inheritance and when it should be used.
  • Learn the syntax of creating descendant classes (subclasses) in Python.
  • Learn how methods of the parent class are called from the descendant.
  • Learn how to correctly write the initializer of the subclass.
  • Learn how to design a good class hierarchy.

15.2 Why learn about class inheritance?

Class inheritance is an indivisible part of OOP. It is a powerful way to reuse functionality which can bring unparalleled structure and efficiency to your software projects.

15.3 Hierarchy of vehicles

Before we get to coding, let’s briefly talk about vehicles. All vehicles have some things in common, such as

  • weight,
  • maximum velocity,
  • maximum occupancy.

But then they differ in some aspects:

  • some vehicles move on the ground (bike, car, truck, bus...),
  • some move in the air (plane, rocket, ...),
  • some move on the water (jet ski, boat, ship, ...),
  • and some even move under water (submarine).

This is illustrated in Fig. 67.


PIC


Fig. 67: Hierarchy of vehicles.


All descendants (subclasses) of a class will automatically inherit all its attributes and methods. But moreover, they may add additional attributes and/or methods of their own.

So, the base class Vehicle should only have universal attributes which are applicable to all vehicles - weight, max_speed, max_occupancy, etc. It also should only have general methods such as accelerate() and decelerate() which apply to all vehicles.

Then, a subclass Wheeled_Vehicle of the class Vehicle will inherit the attributes weight, max_speed, max_occupancy. It will also inherit the methods accelerate() and decelerate(). These methods may need to be redefined in the subclass because different vehicles may accelerate and decelerate differently. This is called polymorphism and we will discuss it in Subsection 16.10. The subclass may need some new attributes including number_of_wheels and new methods such as change_tire().

A different subclass Vessel of the class Vehicle will also inherit the attributes weight, max_speed, max_occupancy. It will also inherit the methods accelerate() and decelerate() which may need to be redefined. But it will have neither the attribute number_of_wheels nor the method change_tire(). Instead, it may have a new attribute displacement (the volume of water it displaces) and a new method lift_anchor().

15.4 Class inheritance

In Subsections 14.8 and 14.9 we have defined a class Circle which allows us to easily create many different circles, calculate their areas and perimeters, and plot them with Matplotlib. Now we would like to create analogous classes also for polygons, triangles, quadrilaterals, rectangles and squares.

We surely could copy and paste the class Circle five more times, and rename and tweak each copy to work for the particular shape. But that would be a horrible way to do it! The same can be done extremely elegantly and efficiently using class inheritance.

First, like with the vehicles, we need to figure out what attributes and methods we should include with the base class. In this case, let’s call it Geometry. Hence, what will the classes Circle, Polygon, Triangle, Quad, Rectangle and Square have in common and where will they differ?

For sure, all these classes will need the arrays ptsx and ptsy of X and Y coordinates for Matplotlib. And in each class, the method draw will be identical to the one of the Circle. The other two methods area and perimeter will differ from class to class.

15.5 Base class Geometry

Based on the considerations from the previous subsection, the base class Geometry can be defined as follows:

  class Geometry:
      ~~~
      Base class for geometrical shapes.
      ~~~
      def __init__(self):
          ~~~
          Initializer creating empty lists of X and Y coordinates.
          ~~~
          self.ptsx = []
          self.ptsy = []
  
      def draw(self, label):
          ~~~
          Display the shape using Matplotlib.
          ~~~
          plt.axis(equal)
          plt.plot(self.ptsx, self.ptsy, label=label)
          plt.legend()

This base class really cannot do much, but it is fine. The class Geometry will not be instantiated. We will use it to derive several descendant classes from it. The benefit of class inheritance here is that if we decide in the future to change the plotting mechanism, we will just have to change it in one place – in the base class Geometry – and the change will propagate automatically to all its descendants.

15.6 Hierarchy of geometrical shapes

Let’s review some basic facts from geometry, because this is going to help us design a good hierarchy of our classes which is shown in Fig. 68.


PIC


Fig. 68: Hierarchy of geometry classes.


First, a (closed oriented) polygon is formed by a sequence of points in the XY plane called vertices which are connected with a polyline into a closed loop. So, a triangle is a special case of a polygon with three vertices, and a quadrilateral is a special case of a polygon with four vertices. Therefore, the classes Triangle and Quadrilateral should naturally be descendants of the class Polygon.

Further, a rectangle is a special case of a quadrilateral whose adjacent edges have right angles between them. Hence the class Rectangle should be inherited from the class Quadrilateral.

And last, a square is a special case of a rectangle whose edges are equally long. Thus the class Square should be derived from the class Rectangle.

The decision where to place the class Circle is a bit more delicate. One could argue that in computer graphics, circles are just regular polygons with a large number of short edges. Based on this, one could derive class Circle from the class Polygon. Then, methods area and perimeter defined in the class Polygon would automatically work in the class Circle.

However, these methods would give imprecise results because a regular polygon approximating a circle has a slightly smaller area and perimeter than the real circle. And moreover, such class Circle would be different from the class Circle which was defined in Subsection 14.8. Therefore, we prefer to take the viewpoint that circles are geometrically genuinely different from polygons, and we will derive the class Circle directly from the base class Geometry.

Finally, notice that the less general class always is derived from a more general one, or in other words, that the process of class inheritance is making classes more specialized. In the following subsections we will explain each step of the inheritance scheme shown in Fig. 68 above.

15.7 Deriving class Polygon from class Geometry

Class Polygon can be inherited from the class Geometry by typing

  class Polygon(Geometry):

Its full definition is shown below. The attributes ptsx and ptsy inherited from the class Geometry are not repeated, nor is the method draw. If needed, the inherited method draw could be redefined in the subclass, but it is not necessary in our case. Redefining methods in subclasses is called polymorphism and we will discuss it in more detail in Subsection 16.10. Importantly, notice that the initializer of the superclass Geometry is called via the built-in function super() at the beginning of the body of the initializer of the subclass Polygon:

          super().__init__(self)

The purpose of this call is to initialize all attributes inherited from the superclass.

In order to calculate the area of a counter-clockwise (CCW) oriented polygon, we use a classical algorithm which constructs oriented trapezoids below the edges, and adds their (oriented) areas. This algorithm can be found easily online. The perimeter is calculated simply by adding up the lengths of all edges:

  class Polygon(Geometry):
      ~~~
      Class Polygon represents a general polygon
      with counter-clockwise (CCW) oriented boundary.
      ~~~
  
      def __init__(self, L):
          ~~~
          Initializer: here L is list of vertices. 
          ~~~
          super().__init__(self)
          # Convert L into plotting arrays:
          for pt in L:
              self.ptsx.append(pt[0])
              self.ptsy.append(pt[1])
          # To close the loop in plotting:
          pt = L[0]
          self.ptsx.append(pt[0])
          self.ptsy.append(pt[1])
  
      def area(self):
          ~~~
          Calculate the area of a general oriented polygon. 
          ~~~
          ymin = min(self.ptsy)
          # The area of an oriented polygon is the sum of 
          # areas of oriented trapezoids below its edges:
          m = len(self.ptsx) - 1   # Number of trapezoids
          s = 0
          for i in range(m):
              s -= (self.ptsx[i+1] - self.ptsx[i]) \
                   * (self.ptsy[i+1] + self.ptsy[i] \
                   - 2*ymin) / 2.
          return s
  

      def perimeter(self):
          ~~~
          Calculate the perimeter of a general oriented polygon. 
          ~~~
          l = 0
          m = len(self.ptsx) - 1
          for i in range(m):
              l += sqrt((self.ptsx[i+1] - self.ptsx[i])**2
                   + (self.ptsy[i+1] - self.ptsy[i])**2)
          return l

Finally, we already mentioned this, but notice that the definition of the class Polygon does not repeat the definition of the method draw – this method is inherited from the class Geometry and will work without any changes.

15.8 Deriving classes Triangle and Quad from class Polygon

The Polygon class defined above is very general. Of course we could use it directly to render triangles, quads, rectangles, and squares. But these geometries are simpler and thus we also want their instantiation to be simpler. Let us begin with the class Triangle.

This class takes three points a, b, c as parameters, forms a list containing these three points, and then passes the list to the initializer of the parent class Polygon. Notice the way the initializer of the Polygon class is called from its descendant Triangle via the function super():

  class Triangle(Polygon):
      ~~~
      General triangle with CCW oriented boundary.
      ~~~
      def __init__(self, a, b, c):
          L = [a, b, c]
          super().__init__(self, L)

The methods area, perimeter and draw do not have to be redefined - they are inherited from the class Polygon and will work correctly.

The class Quad can be created analogously:

  class Quad(Polygon):
      ~~~
      General quadrilateral with CCW oriented boundary.
      ~~~
      def __init__(self, a, b, c, d):
          L = [a, b, c, d]
          super().__init__(self, L)

Same as with the class Triangle, the methods area, perimeter and draw are inherited from the class Polygon and will work correctly without any changes.

15.9 Deriving class Rectangle from class Quad

As we explained in Subsection 15.6, rectangle is a special case of a quadrilateral. We will define it using just two numbers a, b for the edge lengths, as a CCW oriented quadrilateral with four points (0, 0), (a, 0), (a, b) and (0, b):

  class Rectangle(Quad):
      ~~~
      Rectangle with dimensions (0, a) x (0, b).
      ~~~
      def __init__(self, a, b):
          super().__init__(self, [0, 0], [a, 0], [a, b], [0, b])

The methods area, perimeter and draw are inherited from the class Quad and will just work without any changes.

15.10 Deriving class Square from class Rectangle

Square is a special case of rectangle with equally-long edges. Therefore we can derive class Square from the class Rectangle by just redefining the initializer:

  class Square(Rectangle):
      ~~~
      Square with dimensions (0, a) x (0, a).
      ~~~
      def __init__(self, a):
          super().(self, a, a)

Again, the methods area, perimeter and draw are inherited from the class Rectangle and will work without any changes.

15.11 Deriving class Circle from class Geometry

As we explained in Subsection 15.6, circle is a non-polygonal shape which therefore should not be derived from the class Polygon but rather directly from the class Geometry. This time we will add new attributes R (radius) and Cx, Cy (coordinates of the center point). Since you already know the class Circle from Subsection 14.8, there is no need to explain the initializer __init__ or the methods area and perimeter again:

  class Circle(Geometry):
      ~~~
      Circle with given radius R and center point (Cx, Cy).
      Default plotting subdivision: 100 linear edges.
      ~~~
  
      def __init__(self, r, cx, cy, n = 100):
          ~~~
          The initializer adds and initializes the radius R,
          and the center point coordinates Cx, Cy.
          It also creates the arrays of X and Y coordinates.
          ~~~
          super().__init__(self)
          self.R = r
          self.Cx = cx
          self.Cy = cy
          # Now define the arrays of X and Y coordinates:
          self.ptsx = []
          self.ptsy = []
          da = 2*np.pi/self.n
          for i in range(n):
              self.ptsx.append(self.Cx + self.R * np.cos(i * da))
              self.ptsy.append(self.Cy + self.R * np.sin(i * da))
          # Close the polyline by adding the 1st point again:
          self.ptsx.append(self.Cx + self.R)
          self.ptsy.append(self.Cy + 0)

      def area(self):
          ~~~
          Calculates and returns the area.
          ~~~
          return np.pi * self.R**2
  
      def perimeter(self):
          ~~~
          Calculates and returns the perimeter.
          ~~~
          return 2 * np.pi * self.R

Notice that this time we also added the methods area and perimeter because only the method draw is inherited from the class Geometry.

15.12 Sample application

Finally, let us put all the classes to work! We will create sample instances, inquire about their areas and perimeters, and ask them to display themselves:

  # Create a triangle:
  T = Triangle([-2, -0.5], [0, -0.5], [-1, 2])
  print(~Area and perimeter of the triangle:~, \
  T.area(), T.perimeter())
  
  # Create a quad:
  Q = Quad([-3, -1], [0, -1], [-1, 1], [-2, 1])
  print(~Area and perimeter of the quad:~, \
  Q.area(), Q.perimeter())
  
  # Create a rectangle:
  R = Rectangle(3, 1)
  print(~Area and perimeter of the rectangle:~, \
  R.area(), R.perimeter())
  
  # Create a square:
  S = Square(1.5)
  print(~Area and perimeter of the square:~, \
  S.area(), S.perimeter())

  # Create a circle:
  C = Circle(2.5, 0, 0.5)
  print(~Area and perimeter of the circle:~, \
  C.area(), C.perimeter())
  
  # Plot the geometries:
  plt.clf()
  T.draw(~Triangle~)
  Q.draw(~Quad~)
  R.draw(~Rectangle~)
  S.draw(~Square~)
  C.draw(~Circle~)
  plt.ylim(-3, 4)
  plt.legend()
  plt.show()

The graphical output is shown in Fig. 69.


PIC


Fig. 69: Visualization of the instances created above.


And finally, here is the text output:

Area and perimeter of the triangle: 2.5 7.38516480713  
Area and perimeter of the quad: 4.0 8.472135955  
Area and perimeter of the rectangle: 3.0 8.0  
Area and perimeter of the square: 2.25 6.0  
Area and perimeter of the circle: 19.6349540849 15.7079632679

At this point you have a solid theoretical understanding of the principles of OOP and class inheritance. Some additional, more advanced aspects will be discussed in the following section. But most importantly, you need more training. Therefore we recommend that you take the Python II course in NCLab which will guide you through two additional projects – you will create your own object-oriented version of the Python Turtle (famous drawing program based on the educational programming language Logo) and build an object-oriented Geometry Editor. Visit NCLab’s home page http:// nclab.com to learn more!


Table of Contents

Created on August 6, 2018 in Python I,   Python II.
Add Comment
0 Comment(s)

Your Comment

By posting your comment, you agree to the privacy policy and terms of service.