[pythonvis] Re: Slice of Py: Class Inheritance

  • From: "Richard Dinger" <rrdinger@xxxxxxxxxx>
  • To: <pythonvis@xxxxxxxxxxxxx>
  • Date: Wed, 18 Feb 2015 13:43:57 -0800

Hi Jim,

Calls to instance methods can be done two ways.  The more usual way is called a 
bound method call, where you have an instance making the call such as:
self.doSomething() inside a class function def
or x.doSomething() inside some other scope where x is an instance variable

In the above the method is bound  to the instance

The other is an unbound method call where the method is bound to the class for 
example if the class name is Table
Table.doSomething(self, args) inside a method def for the class
Table.doSomething(x, args) inside some other scope where x is an instance 
variable

The unbound method is used because you specify the class name when calling a 
specific ancestor class.  Note that if the method is not in the called class an 
exception will be raised.

I am currently preparing a Slice on super, which is another approach to calling 
ancestors, but there are some significant differences to deal with.

Richard


From: Jim Snowbarger 
Sent: Wednesday, February 18, 2015 8:29 AM
To: pythonvis@xxxxxxxxxxxxx 
Subject: [pythonvis] Re: Slice of Py: Class Inheritance

Richard,

Fantastic and very helpful slice.  Thank you.

 

A question:

In the sample, discussing the fact that the new class must call the initializer 
in the parent class, as in:

Account.__init__(self, customerName, accountID, startBalance) 

 

You said:

“Note that the call to __init__ in the parent class is done as an unbound class 
method call.”

 

I don’t understand the significance of that aside.  The term “unbound method 
call” seems to escape me.  Can you clarify?

Thanks.

 

 

 

From: pythonvis-bounce@xxxxxxxxxxxxx [mailto:pythonvis-bounce@xxxxxxxxxxxxx] On 
Behalf Of Richard Dinger
Sent: Sunday, February 15, 2015 10:50 AM
To: pythonvis
Subject: [pythonvis] Slice of Py: Class Inheritance

 

The previous Slice article introduced classes as a first look at object 
oriented programming in Python and showed how to build simple classes that 
encapsulate some state and behavior into an object. In this Slice we will take 
a look at how to extend a class using what is referred to as inheritance. 

What is Inheritance? 

For this discussion, inheritance is the software mechanism of building a class 
by including the data and methods of an existing class into a new class. The 
new class may then use some of the inherited code unchanged, modify or enhance 
some of it or even hide some of the code. In addition the new class normally 
adds additional code to extend the features of the class beyond those of the 
inherited class. Inheritance is, therefore, a form of software reuse. 

But what exactly does it mean to include the data and methods of an existing 
class in a new class? Well, for example, lets say you want to build a deck of 
cards class . Since a deck of cards is a sort of ordered sequence, you decide 
to base your deck on the list class by having Deck inherit and extend list, so 
Deck is really a list of cards. If you write the following: 

>>>class Deck(list): 

... pass 

You now have a class called Deck that essentially is a clone of list. But, 
unlike the list class, you can modify Deck to include methods like shuffle or 
deal. Depending on the client code that will use your new Deck class, you may 
also wish to hide some of the list methods like sort or reverse so they cannot 
be accidentally called. 

The Deck is said to have an is-a relationship with list. That is a Deck is a 
kind of list. Deck extends the list type to a more specific kind of type. 
Conversely list is a more abstract type of class. 

Inheritance vs Composition 

As a quick aside, inheritance is not the only way to extend classes and build 
systems. You could also build a Deck class using composition wherein the Deck 
class contains a list instance. The list is then an internal container for the 
Deck. In this situation the design is a has-a relationship since the Deck has a 
list as an attribute. The Deck class design in this case eliminates client code 
from mistakenly accessing methods like sort or reverse in the Deck class. Which 
approach you use will depend on the problem that you are trying to solve and 
how you perceive that problem. In practice, though, most classes use a 
combination of both approaches. 

Class Diagrams and Nomenclature 

Classes are organized by their inheritance patterns. Unfortunately these 
patterns are often displayed as a diagram of class boxes connected by 
relationship lines or arrows. So imagine the following simple inheritance 
diagram usually called a tree or a class tree. 

The tree has a box at the top called Shape. Below and to the left is a box 
labeled Triangle, which is connected to the Shape box by an arrow. Below and to 
the right is a box labeled Circle, which is also connected to the Shape box by 
an arrow. This diagram is just an organization chart for a family of classes. 

The boxes and arrows explain the inheritance of the classes. Each of the two 
lower boxes satisfy the 'is-a' relationship, that is a Circle is a Shape, but a 
Shape is not necessarily a Circle. A Shape may be either a circle or a 
triangle. 

Oddly enough, the top of a tree diagram is called its root and the top class in 
a class tree is often called the base class. A class immediately above another 
is called its parent and the class immediately below is its child class. Any 
class above is an ancestor while any below is a descendent class. The term 
super class is often used for ancestor and sub class for descendent. 

Multiple Inheritance 

One more complication, Python supports the notion of multiple inheritance. This 
simply means that a class can inherit from more than one super class. For 
example, a car class in an accounting system might inherit from the Asset class 
as well as from the Vehicle class. In this case a Car is an Asset and it is 
also a Vehicle. Since multiple inheritance adds complexity to your software, 
you should probably avoid it until you have a good understanding of Python. But 
if the problem fits a multiple inheritance design, there is nothing wrong with 
using the tool. 

The Account Class Revisited 

Recall in the previous Slice that a simple account class was developed for a 
debit card system. Now suppose that a new executive account type is being 
requested by bank management. Certain preferred customers should be able to 
make purchases even though their account balance is insufficient as long as a 
predefined overdraft limit is not exceeded. Each preferred account would have a 
unique limit established when the account is set up. Once overdrawn, additional 
purchase transactions are denied until a positive balance is restored, so only 
one overdraft at a time. A fee would, of course, be assessed for this service. 

In a traditional procedural programming environment, this would mean tearing up 
the code to rework it in several places. In the transaction processing code, 
for example, this type of problem is usually solved by conditional statements 
like: 

if customer is preferred: 

doPreferredProcessing() 

else: 

doStandardProcessing() 

The above idiom will appear everywhere account specific processing is performed 
such as where customers are created, where transactions are processed and where 
reports are printed. In an object oriented approach, that construct is usually 
needed less often, but I will explain that after we finish this new class. 

Starting the Executive Account Class 

After importing the original Account class in the file bank.py that we 
developed in the previous Slice article, we start the executive account class 
definition with a class statement just like any other class. 

# begin code indent 2 spaces 

from bank import Account 

class ExecutiveAccount(Account): 

" The ExecutiveAccount class" 

# Overdraft fee charged by the bank 

overFee = 30.0 

# suspend code 

The class name is in Camel case (first letter of each word a capital) and the 
parent class follows enclosed in parenthesis. The ExecutiveAccount 'inherits' 
from the Account class. In order to ensure that you are using the so called new 
classes, I recommend that you have all your classes inherit from either object 
or a class that inherits from object. 

We have something different in this class a class variable called overFee, 
which is the fee the bank charges for overdrawing the account. This class 
variable contains data that is shared by every instance of the class so it 
belongs here at the class level rather than being duplicated in every instance. 
Class variables can be initialized in the class definition or through code that 
has access to the class namespace. 

Initializing the ExecutiveAccount Class 

# resume code 

def __init__(self, customerName, accountID, startBalance, overLimit): 

" Initialize an ExecutiveAccount class object." 

# First, initialize the base Account class 

Account.__init__(self, customerName, accountID, startBalance) 

# and initialize overLimit not in base class 

self.overLimit = overLimit 

# suspend code 

A sub class almost always requires its own __init__ method to handle additional 
input parameters. The new ExecutiveAccount will need an additional attribute 
for holding the overdrawLimit in addition to the normal Account initializing 
data. Python does not automatically call the ancestor initialization methods, 
so that must be done explicitly in the sub class initialization method as 
shown. Note that the call to __init__ in the parent class is done as an unbound 
class method call. In addition to calling the parent initialization, any 
initializing required for the sub class is provided as well. 

Revising The show and str Methods 

# resume code 

def show(self): 

" Show current status of an ExecutiveAccount object." 

# if overdrawn, prepend amount over 

if self.balance < 0.0: 

print '*Overdrawn %.2f Limit %.2f*' % (-self.balance, self.overLimit), 

# then call parent class show method 

Account.show(self) 

def __str__(self): 

" Automatic string conversion method." 

# append limit to parent class details 

return '%s oLimit %.2f' % (Account.__str__(self), self.overLimit) 

# suspend code 

These display output methods are revised by using the inherited methods and 
extending them with the additional data contained in the new class. As in the 
initialization method, ancestor methods are called as unbound class methods. 

Revising the Processing Methods 

Here is the good news: most of the processing does not change from what is 
already in the Account class. The deposit and report methods in the Account 
class can handle the new ExecutiveAccount class, so only the purchase method 
must be revised for the new overdraft limit and fee. 

# resume code 

def purchase(self, purchaseID, vender, amount): 

" Process a valid purchase transaction or reject if not within bounds." 

# if amount less than balance, do normal processing 

if amount <= self.balance: 

Account.purchase(self, purchaseID, vender, amount) 

# purchase transaction fails if already overdrawn 

elif self.balance < 0.0: 

print '* Refused Already Overdrawn vender %s $%8.2f balance $%8.2f' % (vender, 
amount, self.balance) 

return 

# purchase transaction fail if current balance + overdraft limit exceeded 

elif amount > (self.balance + self.overLimit): 

print '* Refused Exceeds Overdraft Limit vender %s $%8.2f balance $%8.2f' % 
(vender, amount, self.balance) 

return 

# if balance exceeded, but not overLimit, do new processing 

elif amount > self.balance and amount <= (self.balance + self.overLimit): 

self.balance -= (amount + ExecutiveAccount.overFee) 

# make a transaction record 

record = 'Purchase ID %s Vender %s sd$%8.2f *balance $%8.2f*' % (purchaseID, 
vender, amount, self.balance) 

self.transactions.append(record) 

return 

# end code 

Now the sub class purchase method checks if the purchase amount is within the 
current account balance and if so, the sub class purchase method simply 
delegates the work to the parent class by calling the purchase method in the 
Account class as an unbound class method. 

And if the purchase amount exceeds the current balance, the special processing 
is done here in the ExecutiveAccount purchase method. Notice that the class 
variable overFee must b qualified with the class name. 

That completes the code for the ExecutiveAccount class. The complete code 
including some simple testing is also at the end of this Slice. 

How Classes Avoid Type Testing 

As promised earlier, now we can look at how object oriented design can simplify 
code in a system like this banking system where there are different types of 
accounts. 

For example, in a transaction processing kind of procedure you might have 
something like the following: 

# get account ID and purchase amount from transactions 

for ID, amount in transactionList: 

# lookup account type and current balance 

type, balance = lookupAccount(ID) 

# process account according to type 

if type is 'normal': 

doNormalProcessing(amount, balance) 

elif type is 'executive': 

doExecutive(amount, balance) 

and so on ... 

The above sort of pattern appears everywhere some form of type dependent 
processing takes place. Now consider our simple little system of accounts. We 
simply have accounts and each type of account 'knows' how to do its own 
processing. The processing process outlined above using the account objects 
looks like this: 

# get account ID and purchase amount from transactions 

for ID, amount in transactionList: 

# lookup account object 

account = lookupAccount(ID) 

# process account 

account.doProcessing(amount) 

and so on ... 

Now additional account types can always be included in the system with little 
impact on existing code. And the tedious task of searching through the code 
trying to find all those type dependent sections is no longer required. 

Method Resolution Order 

How classes and objects work in Python is largely determined by how the 
following expression is evaluated: 

object.attribute 

We have been using this sort of expression from our earliest programming 
attempts to access both data attributes and function attributes within objects. 
If object is not a user defined class type object, the attribute is fetched 
directly from the object. If object is a user defined class type, however, the 
search for attribute starts with object and then the class tree is searched 
starting with object's class proceeding from bottom to top and from left to 
right returning the first occurrence of attribute found. This search is called 
the Method Resolution Order (MRO), although it applies to data attributes as 
well as methods. 

Python classes are essentially trees of namespaces that are searched during 
program execution to fetch attributes. Since the first occurrence found of an 
attribute is returned from the search, attributes located lower down in the 
tree 'hide' attributes with the same name located higher in the tree. 

When you use a unbound class method call, you force the method in the ancestor 
class to be called giving you the ability to override the MRO when needed. 

Sub Classes Override Super Classes 

This is really the heart of object oriented programming and inheritance. As we 
saw earlier with Deck inheriting from list, when creating a new class that 
inherits from a super class, the new class will use attributes from the super 
class unless they are redefined in the descendent. So a sub class extends the 
capabilities of its ancestor classes. 

Now two major features of classes and the object oriented story have been 
introduced; encapsulation and inheritance. With these tools alone many problems 
can be solved. But classes and objects have more features to learn, like 
operator overloading, that will be discussed in future slices.

 

The file bank2.py with the code for the ExecutiveAccount class follows.

 

# bank2.py bank classes 2 spaces each indent

 

# begin code indent 2 spaces

from bank import Account

 

class ExecutiveAccount(Account):

  " The ExecutiveAccount class"

 

  # Overdraft fee charged by the bank

  overFee = 30.0

# suspend code

 

# resume code

  def __init__(self, customerName, accountID, startBalance, overLimit):

    " Initialize an ExecutiveAccount class object."

 

    # First, initialize the base Account class

    Account.__init__(self, customerName, accountID, startBalance)

 

    # and initialize overLimit not in base class

    self.overLimit = overLimit

# suspend code

 

# resume code

  def show(self):

    " Show current status of an ExecutiveAccount object."

 

    # if overdrawn, prepend amount over

    if self.balance < 0.0:

      print '*Overdrawn %.2f Limit %.2f*' % (-self.balance, self.overLimit),

 

    # then call parent class show method

    Account.show(self)

 

  def __str__(self):

    " Automatic string conversion method."

 

    # append limit to parent class details

    return '%s oLimit %.2f' % (Account.__str__(self), self.overLimit)

# suspend code

 

  # resume code

  def purchase(self, purchaseID, vender, amount):

    " Process a valid purchase transaction or reject if not within bounds."

 

    # if amount less than balance, do normal processing

    if amount <= self.balance:

      Account.purchase(self, purchaseID, vender, amount)

 

    # purchase transaction fails if already overdrawn

    elif self.balance < 0.0:

      print '* Refused Already Overdrawn vender %s $%8.2f balance $%8.2f' % 
(vender, amount, self.balance)

      return

    

    # purchase transaction fail if current balance + overdraft limit exceeded

    elif amount > (self.balance + self.overLimit):

      print '* Refused Exceeds Overdraft Limit vender %s $%8.2f balance $%8.2f' 
% (vender, amount, self.balance)

      return

    # if balance exceeded, but not overLimit, do new processing

    elif amount > self.balance and amount <= (self.balance + self.overLimit):

      self.balance -= (amount + ExecutiveAccount.overFee)

 

      # make a transaction record

      record = 'Purchase ID %s Vender %s sd$%8.2f *balance $%8.2f*' % 
(purchaseID, vender, amount, self.balance)

      self.transactions.append(record)

      return

# end code

 

 

# Testing

if __name__ == '__main__':

  a = ExecutiveAccount('Smith, John', 'A1234', 200, 100)

  a.show()

  a.deposit('D12345', 100.00)

  a.show()

  a.purchase('C0001', 'Supermarket', 100.00)

  a.purchase('C0002', 'Drug Store', 100.00)

  a.show()

  a.purchase('C0003', 'Computer Shop', 180.00)

  a.show()

  a.purchase('C0002', 'Drug Store', 1.00)

  a.report()

  print '__str__ function output  is:\n', a

 

Other related posts: