Introduction to Python Programming. Section 16. Object-Oriented Programming III – Advanced Aspects

16 Object-Oriented Programming III - Advanced Aspects

In this section we will explore some more advanced aspects of OOP.

16.1 Objectives

You will learn about:

  • Object introspection:
    • How to check if an object is instance of a given class.
    • How to use the built-in function help to inspect classes.
    • How to obtain class name from a given object.
    • How to check if a class is subclass of another class.
    • How to check if an object has a given attribute or method, etc.
  • Polymorphism.
  • Multiple inheritance.

16.2 Why learn about polymorphism and multiple inheritance?

Polymorphism is probably the most beautiful aspect of object-oriented programming. You already know how to create subclasses. We will show you that it is possible to redefine the same method in each subclass to work differently. This opens the door to amazing algorithms where the code can do very different things based on the object which is used. As for multiple inheritance, we will show you why it is good to know about it, but also why one should be extremely cautious with it.

16.3 Inspecting objects with isinstance

Python has a built-in function isinstance which can be used to determine if an object is an instance of a given class or its superclass. Typing isinstance(a, C) will return True if object a is an instance of class C or its superclass, and False otherwise. This function is often used when checking the sanity of user data.

As you know, all types of variables such as bool, int, float, complex, str etc. are classes. For example, one can check whether a variable var is a Boolean as follows:

  var = 10 < 6
  flag = isinstance(var, bool)
  print(flag)

  True

Or, one can check if a variable t is a text string:

  t = 25
  flag = isinstance(t, str)
  print(flag)

  False

In the same way one can check instances of other built-in types including list, tuple, dict, set etc:

  L = [1, 2, 3]
  flag = isinstance(L, list)
  print(flag)

  True

16.4 Inspecting instances of custom classes with isinstance

One has to be a bit careful here. Let’s return for a moment to our classes Geometry, Circle, Polygon, Triangle, Quad, Rectangle and Square from Section 15 whose hierarchy was shown in Fig. 68:


PIC


Fig. 70: Hierarchy of geometry classes.


As we mentioned in the previous subsection, typing isinstance(a, C) will return True if object a is an instance of class C or its superclass, and False otherwise. The part "or its superclass" is important. If one takes the name of the function isinstance literally, one might be surprised by the fact that a square checks as an instance of the class Polygon:

  s = Square(2)
  flag = isinstance(s, Polygon)
  print(flag)

  True

When working with a custom hierarchy of classes, one might want the superclasses to be excluded. In the following subsection we will show you how.

16.5 Inspecting objects with type

If one needs to check whether an object a is an instance of a class C but not its superclass, one can use the built-in function type. The call type(a) returns the class this object is an instance of. Returning to the example from the previous subsection:

  s = Square(2)
  flag = type(a) == Polygon
  print(flag)

  False

But the same test passes for the class Square as it should:

  s = Square(2)
  flag = type(a) == Square
  print(flag)

  True

16.6 Inspecting classes with help

The built-in function help which you already know can be used to obtain very detailed information about any class, both built-in and custom. For illustration, let’s get help on the class list:

  help(list)

The output is very long:

  Help on class list in module builtins:
  
  class list(object)
   |  list() -> new empty list
   |  list(iterable) -> new list initialized from iterable’s
      items
   |  Methods defined here:
   |
   |  __add__(self, value, /)
   |      Return self+value.
   |
   |  __contains__(self, key, /)
   |      Return key in self.
   |
   |  __delitem__(self, key, /)
   |      Delete self[key].
   |
   |  __eq__(self, value, /)
   |      Return self==value.
   |
   |  __ge__(self, value, /)
   |      Return self>=value.
   |
   |  __getattribute__(self, name, /)
   |      Return getattr(self, name).
   |
   |  __getitem__(...)
   |      x.__getitem__(y) <==> x[y]
   |
   |  __gt__(self, value, /)
   |      Return self>value.
   |
   |  __iadd__(self, value, /)
   |      Implement self+=value.
   |
   |  __imul__(self, value, /)
   |      Implement self*=value.

   |  __init__(self, /, *args, **kwargs)
   |      Initialize self.  See help(type(self)) for accurate
          signature.
   |
   |  __iter__(self, /)
   |      Implement iter(self).
   |
   |  __le__(self, value, /)
   |      Return self<=value.
   |
   |  __len__(self, /)
   |      Return len(self).
   |
   |  __lt__(self, value, /)
   |      Return self<value.
   |
   |  __mul__(self, value, /)
   |      Return self*value.n
   |
   |  __ne__(self, value, /)
   |      Return self!=value.
   |
   |  __new__(*args, **kwargs) from builtins.type
   |      Create and return a new object.  See help(type) for
          accurate signature.
   |
   |  __repr__(self, /)
   |      Return repr(self).
   |
   |  __reversed__(...)
   |      L.__reversed__() -- return a reverse iterator over the
          list
   |
   |  __rmul__(self, value, /)
   |      Return self*value.
   |
   |  __setitem__(self, key, value, /)
   |      Set self[key] to value.

   |  __sizeof__(...)
   |      L.__sizeof__() -- size of L in memory, in bytes
   |
   |  append(...)
   |      L.append(object) -> None -- append object to end
   |
   |  clear(...)
   |      L.clear() -> None -- remove all items from L
   |
   |  copy(...)
   |      L.copy() -> list -- a shallow copy of L
   |
   |  count(...)
   |      L.count(value) -> integer -- return number of
          occurrences of value
   |
   |  extend(...)
   |      L.extend(iterable) -> None -- extend list by appending
          elements from the iterable
   |
   |  index(...)
   |      L.index(value, [start, [stop]]) -> integer -- return
          first index of value.
   |      Raises ValueError if the value is not present.
   |
   |  insert(...)
   |      L.insert(index, object) -- insert object before index
   |
   |  pop(...)
   |      L.pop([index]) -> item -- remove and return item at
          index (default last).
   |      Raises IndexError if list is empty or index is out of
          range.
   |
   |  remove(...)
   |      L.remove(value) -> None -- remove first occurrence of
          value.
   |      Raises ValueError if the value is not present.

   |  reverse(...)
   |      L.reverse() -- reverse *IN PLACE*
   |
   |  sort(...)
   |      L.sort(key=None, reverse=False) -> None -- stable sort
          *IN PLACE*
   |
   |  -----------------------------------------------------------
   |  Data and other attributes defined here:
   |
   |  __hash__ = None

16.7 Obtaining class and class name from an instance

Every class in Python has a (hidden) attribute __class__ which makes it possible to retrieve the class C corresponding to a given instance a. Typing a.__class__ gives exactly the same result as typing type(a). The name of the class, as a text string, can be retrieved by typing a.__class__.__name__. This is illustrated in the following example which uses the class Circle which we defined in Subsection 14.8:

  a = Circle(1, 0, 0)
  c1 = type(a)
  c2 = a.__class__
  name = a.__class__.__name__
  print(c1)
  print(c1)
  print(name)

  <class ’Circle’>
  <class ’Circle’>
  Circle

16.8 Inspecting classes with issubclass

Python has a built-in function issubclass(A, B) which returns True if class A is subclass of the class B and False otherwise. The following example shows how this function can be used with our custom geometry classes from Section 15:

  flag1 = issubclass(Triangle, Polygon)
  flag2 = issubclass(Triangle, Circle)
  print(flag1)
  print(flag2)

  True
  False

However, in the code we may need to ask this question on the level of instances rather than classes. The solution is to first retrieve the class from each instance, and only then use issubclass:

  t1 = Triangle([0, 0], [1, 0], [0, 1])
  p1 = Polygon([[0, 1], [2, 0], [3, 2], [2, 3]])
  c1 = Circle(2, 1, 1)
  flag1 = issubclass(t1.__class__, p1.__class__)
  flag2 = issubclass(t1.__class__, c1.__class__)
  print(flag1)
  print(flag2)

  True
  False

16.9 Inspecting objects with hasattr

Python has a useful built-in function hasattr(a, name) which returns True if object a has attribute or method name, and False otherwise. Here, name must be a text string. For instance, let’s inquire about the class Circle:

  c1 = Circle(2, 1, 1)
  flag1 = hasattr(c1, ’R’)
  flag2 = hasattr(c1, ’ptsx’)
  flag3 = hasattr(c1, ’X’)
  flag4 = hasattr(c1, ’area’)
  flag5 = hasattr(c1, ’volume’)
  flag6 = hasattr(c1, ’draw’)
  print(flag1)
  print(flag2)
  print(flag3)
  print(flag4)
  print(flag5)
  print(flag6)

  True
  True
  False
  True
  False
  True

16.10 Polymorphism - Meet the Lemmings

Have you ever heard about the Lemmings? The Lemmings is a legendary 8-bit game created by Mike Dailly and David Jones in 1991 for Amiga computers. You can read all about it on Wikipedia, and you can even play it today in your web browser at https://www.elizium.nu/scripts/lemmings/if you can survive some ads.


PIC


Fig. 71: The Lemmings.


The reason why we mentioned the Lemmings is that this is a great way to explain polymorphism.

Formally speaking, polymorphism is an ability of derived classes to redefine the methods of the ancestor class. Hmm, that’s not very clear - is it? Fortunately, here come the Lemmings! Although all of them look alike, there are eight types: Digger, Miner, Basher, Builder, Blocker, Exploder, Floater, and Climber.

In the program below, each of them is an individual class, derived from the same base class Lemming. Notice that the base class defines a method work which is then redefined in each subclass to do something different:

  class Lemming:
      def work(self): print(~I do nothing.~)
  class Digger(Lemming):
      def work(self): print(~I dig!~)
  class Miner(Lemming):
      def work(self): print(~I mine!~)
  class Basher(Lemming):
      def work(self): print(~I bash!~)
  class Builder(Lemming):
      def work(self): print(~I build!~)
  class Blocker(Lemming):
      def work(self): print(~I block!~)
  class Exploder(Lemming):
      def work(self): print(~I explode!~)
  class Floater(Lemming):
      def work(self): print(~I float!~)
  class Climber(Lemming):
      def work(self): print(~I climb!~)

Now let’s create an instance of each subclass, and call the method work for each one:

  L = [Digger(), Miner(), Basher(), Builder(), Blocker(), \
       Exploder(), Floater(), Climber()]
  for l in L:
      l.work()

  I dig!
  I mine!
  I bash!
  I build!
  I block!
  I explode!
  I float!
  I climb!

As you can see, the outcomes of the same code l.work() are different. This is the true meaning of polymorphism.

16.11 Polymorphism II - Geometry classes

We have already used polymorphism before without mentioning it. In Section 15 we created a base class Geometry whose initializer was very simple:

      def __init__(self):
          ~~~
          Initializer creating empty lists of X and Y coordinates.
          ~~~
          self.ptsx = []
          self.ptsy = []

Then we derived the classes Polygon, Circle, Triangle, Quad, Rectangle and Square from it, redefining the initializer in each subclass. For example, in the subclass Polygon the initializer converted a list of points L into the arrays ptsx and ptsy:

      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])

In the subclass Square, the initializer took just one number a which was the size of the square, and called the initializer of the superclass Rectangle:

      def __init__(self, a):
          super().(self, a, a)

We will not list all the remaining initializers here, but you can find them in Section 15.

Finally, let us mention that Python makes polymorphism so easy, that one could easily miss how big deal this is. For comparison:

  • C++ introduces keyword "virtual" and special virtual methods to do the same thing.
  • Java does not use the keyword "virtual" but it has a special type of class (abstract class).

16.12 Multiple inheritance I - Introduction

By multiple inheritance we mean that a descendant class (say B) is derived from two or more ancestor classes (say A1, A2, ...), inheriting the attributes and methods from both/all of them. The syntax is what you would expect - instead of typing

  class B(A):

where A is the ancestor class, one types

  class B(A1, A2):

where A1, A2 are two ancestor classes in this case.

Although multiple inheritance sounds like a cool thing, its practical usefulness is extremely limited. It is good to know that it exists, but experienced software developers do not recommend it. Let’s explain why.

Most of the time, multiple inheritance is used without proper understanding. It may seem like an elegant way to easily aggregate attributes and/or methods from various classes together. Basically, to "add classes together". But this is fundamentally wrong.

It is OK to think about a Car as being a composition of various parts including Engine, Wheels, Carburetor, AirFilter, SparkPlugs, Exhaust, etc. But then one should just create class Car and have instances of all these other classes in it - this is not a case for multiple inheritance from the classes Engine, Wheels, Carburetor, etc.

A more justified case for multiple inheritance would be a class FlyingCar which would have the classes Car and Airplane as ancestors, attributes Wheels and Wings, and methods drive() and fly(). But you can already see problems - for example, what about Engine and Wheels? Will they be coming from the Car class or from the Airplane class?

In general, a good justification for multiple inheritance is extremely hard to find.

On top of this, the next subsection presents the so-called Diamond of Dread, a classical problem of multiple inheritance you should know about. But to finish on a positive note, Subsection 16.12 will present a working example of multiple inheritance where you will be able to see exactly how it’s done.

16.13 Multiple inheritance II - The Diamond of Dread

The so-called Diamond of Dread is the classical problem of multiple inheritance.


PIC


Fig. 72: Diamond of Dread.


In short - if one creates two descendants B, C of the same base class A, and then defines a new class D from B, C using multiple inheritance, there will be conflicts between attributes and/or methods coming from the classes B and C.

How to avoid the Diamond of Dread problem:

When creating a new class D using multiple inheritance from the classes B and C, make sure that the ancestors B and C are completely independent classes which do not have a common ancestor themselves. However, even if the classes B, C do not have a common ancestor, but have attributes or methods with the same names, there is going to be a conflict on the level of the class D.

16.14 Multiple inheritance III - Leonardo da Vinci

You certainly heard about Leonardo da Vinci, a universal Renaissance genius whose areas of interest included engineering, science, painting, music, architecture, and many others.


PIC


Fig. 73: Leonardo da Vinci.


In order to illustrate multiple inheritance, we allowed ourselves to create just two base classes:

  • Engineer who has an attribute invention and method invent,
  • Painter who has an attribute painting and method paint.

The program below creates a new class Genius as a descendant of these two classes. This class will have both attributes invention and painting, and both methods invent and paint. In particular, pay attention to the complicated-looking constructor which uses *args and **kwargs. You do not need to learn it, just remember that it’s part of the story. Recall that the usage of *args and **kwargs was explained in Section 8.

  class Engineer:
      def __init__(self, *args, **kwargs):
          super(Engineer, self).__init__(*args, **kwargs)
          self.invention = kwargs[’invention’]
      def invent(self):
          return self.invention
  
  class Painter:
      def __init__(self, *args, **kwargs):
          super(Painter, self).__init__()
          self.painting = kwargs[’painting’]
      def paint(self):
          return self.painting
  

  class Genius(Engineer, Painter):
      def __init__(self, *args, **kwargs):
          super(Genius, self).__init__(*args, **kwargs)
      def create(self):
          print(~I am inventing ~ + self.invent() + ~.~)
          print(~I am painting ~ + self.paint() + ~.~)
  
  # Main program:
  Leonardo = Genius(invention=~an airplane~, painting=~Mona Lisa~)
  Leonardo.create()

  I am inventing an airplane.
  I am painting Mona Lisa.


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.