Learning Python - Mark Lutz [359]
We’ve also changed our self-test code to print objects directly, instead of printing individual attributes. When run, the output is more coherent and meaningful now; the “[...]” lines are returned by our new __str__, run automatically by print operations:
[Person: Bob Smith, 0]
[Person: Sue Jones, 100000]
Smith Jones
[Person: Sue Jones, 110000]
Here’s a subtle point: as we’ll learn in the next chapter, a related overloading method, __repr__, provides an as-code low-level display of an object when present. Sometimes classes provide both a __str__ for user-friendly displays and a __repr__ with extra details for developers to view. Because printing runs __str__ and the interactive prompt echoes results with __repr__, this can provide both target audiences with an appropriate display. Since we’re not interested in displaying an as-code format, __str__ is sufficient for our class.
Step 4: Customizing Behavior by Subclassing
At this point, our class captures much of the OOP machinery in Python: it makes instances, provides behavior in methods, and even does a bit of operator overloading now to intercept print operations in __str__. It effectively packages our data and logic together into a single, self-contained software component, making it easy to locate code and straightforward to change it in the future. By allowing us to encapsulate behavior, it also allows us to factor that code to avoid redundancy and its associated maintenance headaches.
The only major OOP concept it does not yet capture is customization by inheritance. In some sense, we’re already doing inheritance, because instances inherit methods from their classes. To demonstrate the real power of OOP, though, we need to define a superclass/subclass relationship that allows us to extend our software and replace bits of inherited behavior. That’s the main idea behind OOP, after all; by fostering a coding model based upon customization of work already done, it can dramatically cut development time.
Coding Subclasses
As a next step, then, let’s put OOP’s methodology to use and customize our Person class by extending our software hierarchy. For the purpose of this tutorial, we’ll define a subclass of Person called Manager that replaces the inherited giveRaise method with a more specialized version. Our new class begins as follows:
class Manager(Person): # Define a subclass of Person
This code means that we’re defining a new class named Manager, which inherits from and may add customizations to the superclass Person. In plain terms, a Manager is almost like a Person (admittedly, a very long journey for a very small joke...), but Manager has a custom way to give raises.
For the sake of argument, let’s assume that when a Manager gets a raise, it receives the passed-in percentage as usual, but also gets an extra bonus that defaults to 10%. For instance, if a Manager’s raise is specified as 10%, it will really get 20%. (Any relation to Persons living or dead is, of course, strictly coincidental.) Our new method begins as follows; because this redefinition of giveRaise will be closer in the class tree to Manager instances than the original version in Person, it effectively replaces, and thereby customizes, the operation. Recall that according to the inheritance search rules, the lowest version of the name wins:
class Manager(Person): # Inherit Person attrs
def giveRaise(self, percent, bonus=.10): # Redefine to customize
Augmenting Methods: The Bad Way
Now, there are two ways we might code this Manager customization: a good way and a bad way. Let’s start with the bad way, since it might be a bit easier to understand. The bad way is to cut and paste the code of giveRaise in Person and modify it for Manager, like this:
class Manager(Person):
def giveRaise(self, percent, bonus=.10):
self.pay = int(self.pay * (1 + percent + bonus)) # Bad: cut-and-paste
This works as advertised—when we later call the giveRaise method of a Manager instance, it will