Exercises for Learning Python

Chapter 19 – Object Oriented Programming

1) Hierarchy of Classes

The objective of this exercise is to explore alternatives to the program structure shown in the Animals_2 example of this chapter.  Source code can be found at http://ece.arizona.edu/~edatools/Python/Animals.py   Re-write this program without using static methods.  What are the advantages and disadvantages of the new structure compared to the original?  Note:  The show() methods must provide essentially the same functionality as in the original program.  Each method provides a display of the characteristics of its class in a format unique to that class.  The methods must be callable from any instance of any class, or from no instance at all.  The displayed characteristics for any class must include the characteristics of its ancestors.

Solution a)

Animals_2b.py  In this solution, we have moved the show() methods to the module level, where they can be called as ordinary functions, without any binding to a particular instance.  To provide for calling from an instance, we have added one function show(self) which takes the instance as its argument, determines its class, and calls the correct function from that class.  Here are the comparative calls:

Original       Re-write

  Cat.show()     showCat()

  cat1.show()    show(cat1)

The advantage over the original form is that we avoid an extra line after each function that is to be used as a "static method" telling the interpreter not to expect an instance as the first argument of the function.

The disadvantages are 1)  The show() functions relating to each class must be maintained outside of that class.  This disruption of preferred modularity could be a problem in a large hierarchy if someone makes changes in the class and forgets to coordinate the changes with the external show() functions.  2)  An extra function with some advanced techniques is required to handle the call from an instance.

Solution b)

Animals_DC.py  The show() methods are replace by a generic show() function, which prints an a dictionary of (key,value) pairs, one for each animal.

The disadvantage of this solution is that it does not allow full customization of the display for each class.

2) Robust Programming

The Animals.py example in Exercise 1 needs to be modified to make it a production-quality program which can grow into a larger system, maintained by many programmers, and easily modified to do things not in the original plan.  What are the specific problems you see in the example, and what would you do to improve it.  Re-write the program to implement at least some of these suggestions.  What are the advantages and disadvantages of your implementation compared to the original?

Solution a) – James Moughan 5/10/04

Animals_JM.py  The entire class structure is replaced by functions which automatically generate new classes, provide counts, etc.

Claimed Advantages:

-- Robustness; no requirement to keep a count of instances or update the count of the super class by calling it's initializer.  This minimizes the chance of programmer error.

-- Avoids replication of data, eliminating OOS (out-of-sync) errors.  Totals are calculated each time from the counts of instances.

-- Maintainability; all of the code for counting is contained in a single module, so modifications will not require changes to multiple similar pieces of code throughout the class structure.

-- Agility; we can maintain more than one zoo in a program by creating new instances of the animal_farm class.  Class level data would limit us to data on a single zoo system.

-- Generality; the Collection class can be used for any problem of this type.

Disadvantages:

-- The new program is difficult to modify because generating classes automatically is more complex than the classes in Animals_2. 

The debate over robustness and maintainability here comes down to what modifications we expect.  In the new program, it is easy to add a new class with the anticipated characteristics of a class, but more difficult to add a completely different kind of class, or make radical changes in the behavior of the generated classes.

-- Like Exercise 1, solution b, this re-write assume an identical display format for each class.

Solution b) – David MacQuigg 5/19/04

Animals_2c.py     Problems and suggested solutions:

1) Class variables like numMammals that are not intended to be directly manipulated by the user should be made private variables, by adding a leading underscore.

    _numMammals = 0

2) Direct calls to a function in a parent class (super calls) might not get updated if a programmer adds a new class in the hierarchy.  Calls like Mammal.__init__(self) should be replaced by a general call to the __init__ function from whatever class is currently above in the method resolution order.

    super(Feline, self).__init__()

Note: Calls like Mammal.talk(self), which really are intended to use a specific class, should be left as is.

3) Variables like _numMammals which depend on variables in other classes could get out-of-sync with those other variables ( e.g. if a programmer adds a new class and forgets to call __init__).  These variables could be replaced by functions which re-calculate the dependent values from the original independent data.

Note: This is a trade-off of simplicity and efficiency vs robustness.

Discussions/Animals_2c.htm

Solution c) – anon5/30/04

Zoo2_DMQ.py

The major change in the first version is simply renaming numAnimals, numCats, etc. in each class to ‘count’. One benefit of this approach is that you can do something like:

for animal in Cat,Bovine,Animal,Frog:

    print animal.name, animal.count

Another benefit is that the work needed to track counts can be coded completely within the Animal base class. The built-in __mro__ class attribute makes it easy to loop over a class and its ancestor classes (even in cases of multiple inheritance) to increment the appropriate count attributes on initialization. For example, a Frog is-a Reptile is-a Animal. If a new Frog is added, Frog.count, Reptile.count, and Animal.count will be incremented.

Each type of animal added needs an attribute ‘count’.  Someone creating a new class must remember to add the attribute count = 0; otherwise count-tracking will break.

There is a __del__ method that decrements class counts in the event that someone deletes an instance of a class. It also uses __mro__ to decrement the appropriate class counts.

Another change is that the collection of showX() functions/methods has been eliminated, and replaced with a single function which handles the display of instance/class information for any class or instance in the hierarchy. The show() function takes as an argument either a class or an instance. If it receives a class, it prints a count for that class and all its ancestors (excluding object). For example, show(Frog) prints counts for Frog->Reptile->Animal. If show() receives an instance, it prints counts for the class of the instance passed in, and its ancestors, plus a list of that instance’s attributes and values.

Zoo2a_RK3.py

The primary purpose of zoo2a.py is to illustrate the use of classmethod. zoo2a.py uses a combination of a classmethod and a regular method to achieve the same result as the show() function in zoo2.py.

Zoo3_DMQ1.py

The major change in the third version is to use a metaclass, so that the programmer does not need to remember to add count = 0 inside a new class. See the metaclass example, pp.102-103, in Python in a Nutshell, by Alex Martelli.

For this specific example, __init__  and __del__ should not be replaced in subclasses; otherwise count-tracking will break. Extending the inherited __init__ and __del__ methods should work, but I did not test that operation in these examples.

3) Multiple Inheritance

Set up the following hierarchy of classes and methods.  All methods should print a message which identifies their class.

    Animal  ->  Mammal  ->  Feline  ->  Cat

      talk1()     talk1()                 talk1() 

    \ - - - ->  Mammal2 - - - - - - ->  /

                  talk2()

Call the talk1 and talk2 methods from an instance of Cat and from the various classes ( using the unbound method form Cat.talk1(cat1) ).  From which classes can you call talk2?  Look at the bases and ancestors of each class, using the attributes __bases__ and __mro__.   Call the methods talk1 and talk2 using the super-call function – super(Cat, cat1).talk1().  What is the advantage of this function over a call like Cat.__mro__[1].talk1(cat1)? 

Answer:  superCall.py  The super function will search the entire inheritance tree for the necessary method, not just the part of the tree above a particular class.

4) Automatic Binding of Methods and Functions

binding.py   Explain the error in this program and how to fix it.