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:
Or, one can check if a variable t is a text string:
In the same way one can check instances of other built-in types including list, tuple,
dict, set etc:
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:
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:
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:
But the same test passes for the class Square as it should:
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:
The output is very long:
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.
| 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.
| 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.
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:
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:
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:
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:
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.
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:
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:
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:
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:
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:
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
where A is the ancestor class, one types
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.
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.
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.
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
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()
Table of Contents
- Preface
- 1. Introduction
- 2. Using Python as a Scientific Calculator
- 3. Drawing, Plotting, and Data Visualization with Matplotlib
- 4. Working with Text Strings
- 5. Variables and Types
- 6. Boolean Values, Functions, Expressions, and Variables
- 7. Lists, Tuples, Dictionaries, and Sets
- 8. Functions
- 9. The ’For’ Loop
- 10. Conditions
- 11. The ’While’ Loop
- 12. Exceptions
- 13. File Operations
- 14. Object-Oriented Programming I – Introduction
- 15. Object-Oriented Programming II – Class Inheritance
- 16. Object-Oriented Programming III – Advanced Aspects
- 17. Recursion
- 18. Decorators
- 19. Selected Advanced Topics