[pythonvis] Re: Slice of Py: Class Inheritance

  • From: "Jeffrey Thompson" <jthomp@xxxxxxxxxxx>
  • To: <pythonvis@xxxxxxxxxxxxx>
  • Date: Sun, 15 Feb 2015 16:44:44 -0500

Richard,

Yes, indeed. Another short description provided by Richard Dinger.

I admire your skill and talent to produce something akin to what you do;

Yes, and even a little bit of envylurks around there as well as admiration.

Please keep up the good work.

 

                Jet (Jeff Thompson)

 

                P.S. And NO, Richard did not pay me to write this e-mail!

It is what I feel and think accurately and voluntarily.

 

 

 

From: pythonvis-bounce@xxxxxxxxxxxxx [mailto:pythonvis-bounce@xxxxxxxxxxxxx]
On Behalf Of Richard Dinger
Sent: Sunday, February 15, 2015 11: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: