23.5 Part VI Exercises
These exercises ask you to write a few classes and experiment with
some existing code. Of course, the problem with existing code is that
it must be existing. To work with the set class in exercise 5, either
pull down the class source code off the Internet (see Preface) or type it up by hand (it's
fairly small). These programs are starting to get more sophisticated,
so be sure to check the solutions at the end of the book for
pointers.
See Section B.6 for
the solutions.
Inheritance. Write a class called
Adder that exports a method add(self, x,
y) that prints a "Not
Implemented" message. Then define two subclasses of
Adder that implement the add
method:
- ListAdder
-
With an add method that returns the concatenation
of its two list arguments
- DictAdder
-
With an add method that returns a new dictionary
with the items in both its two dictionary arguments (any definition
of addition will do)
Experiment by making instances of all three of your classes
interactively and calling their add methods. Now, extend your Adder superclass to save an
object in the instance with a constructor (e.g., assign
self.data a list or a dictionary) and overload the
+ operator with an __add__ to
automatically dispatch to your add methods (e.g.,
X+Y triggers X.add(X.data,Y)).
Where is the best place to put the constructors and operator overload
methods (i.e., in which classes)? What sorts of objects can you add
to your class instances? In practice, you might find it easier to code your
add methods to accept just one real argument
(e.g., add(self,y)), and add that one argument to
the instance's current data (e.g.,
self.data+y). Does this make more sense than
passing two arguments to add? Would you say this
makes your classes more "object-oriented"? Operator overloading. Write a class called
Mylist that shadows
("wraps") a Python list: it should
overload most list operators and operations including
+, indexing, iteration, slicing, and list methods
such as append and sort. See
the Python reference manual for a list of all possible methods to
support. Also, provide a constructor for your class that takes an
existing list (or a Mylist instance) and copies
its components into an instance member. Experiment with your class
interactively. Things to explore: Why is copying the initial value important here? Can you use an empty slice (e.g., start[:]) to
copy the initial value if it's a
Mylist instance? Is there a general way to route list method calls to the wrapped
list? Can you add a Mylist and a regular list? How about
a list and a Mylist instance? What type of object should operations like + and
slicing return; how about indexing? If you are working with a more recent Python release (Version 2.2 or
later), you may implement this sort of wrapper class either by
embedding a real list in a standalone class, or by extending the
built-in list type with a subclass. Which is easier and why?
Subclassing. Make a subclass of
Mylist from Exercise 2 called
MylistSub, which extends Mylist
to print a message to stdout before each
overloaded operation is called and counts the number of calls.
MylistSub should inherit basic method behavior
from Mylist. Adding a sequence to a
MylistSub should print a message, increment the
counter for + calls, and perform the
superclass's method. Also, introduce a new method
that displays the operation counters to stdout and
experiment with your class interactively. Do your counters count
calls per instance, or per class (for all instances of the class)?
How would you program both of these? (Hint: it depends on which
object the count members are assigned to: class members are shared by
instances, self members are per-instance data.) Metaclass methods. Write a class called
Meta with methods that intercept every attribute
qualification (both fetches and assignments) and prints a message
with their arguments to stdout. Create a
Meta instance and experiment with qualifying it
interactively. What happens when you try to use the instance in
expressions? Try adding, indexing, and slicing the instance of your
class. Set objects. Experiment with the set class
described in Section 23.1.1. Run commands to do the
following sorts of operations: Create two sets of integers, and compute their intersection and union
by using & and | operator
expressions. Create a set from a string, and experiment with indexing your set;
which methods in the class are called? Try iterating through the items in your string set using a
for loop; which methods run this time? Try computing the intersection and union of your string set and a
simple Python string; does it work? Now, extend your set by subclassing to handle arbitrarily many
operands using a *args argument form (Hint: see
the function versions of these algorithms in Chapter 13). Compute intersections and unions of multiple
operands with your set subclass. How can you intersect three or more
sets, given that & has only two sides? How would you go about emulating other list operations in the set
class? (Hints: __add__ can catch concatenation,
and __getattr__ can pass most list method calls
off to the wrapped list.)
Class tree links. In Section 21.5 in
Chapter 21, and in Section 22.6 in
Chapter 22, we mentioned that classes have a
__bases__ attribute that returns a tuple of the
class's superclass objects (the ones in parentheses
in the class header). Use __bases__ to extend
the Lister mixin class (see Chapter 22), so that it prints the names of the immediate
superclasses of the instance's class. When
you're done, the first line of the string
representation should look like this (your address may vary): <Instance of Sub(Super, Lister), address 7841200: How would you go about listing inherited class attributes too? (Hint:
classes have a __dict__.) Try extending your
Lister class to display all accessible
superclasses and their attributes as well; see Chapter 21s
classtree.py example for hints on climbing class
trees, and the Lister footnote about using
dir and getattr in Python 2.2
for hints on climbing trees. Composition. Simulate a fast-food ordering
scenario by defining four classes:
- Lunch
-
A container and controller class
- Customer
-
The actor that buys food
- Employee
-
The actor that a customer orders from
- Food
-
What the customer buys
To get you started, here are the classes and methods
you'll be defining: class Lunch:
def __init__(self) # Make/embed Customer and Employee.
def order(self, foodName) # Start a Customer order simulation.
def result(self) # Ask the Customer what kind of Food it has.
class Customer:
def __init__(self) # Initialize my food to None.
def placeOrder(self, foodName, employee) # Place order with an Employee.
def printFood(self) # Print the name of my food.
class Employee:
def takeOrder(self, foodName) # Return a Food, with requested name.
class Food:
def __init__(self, name) # Store food name. The order simulation works as follows: The Lunch class's constructor
should make and embed an instance of Customer and
Employee, and export a method called
order. When called, this order
method should ask the Customer to place an order,
by calling its placeOrder method. The
Customer's
placeOrder method should in turn ask the
Employee object for a new Food
object, by calling the Employee's
takeOrder method. Food objects should store a food name string (e.g.,
"burritos"), passed down from
Lunch.order to
Customer.placeOrder, to
Employee.takeOrder, and finally to
Food's constructor. The top-level
Lunch class should also export a method called
result, which asks the customer to print the name
of the food it received from the Employee via the
order (this can be used to test your simulation).
Note that Lunch needs to either pass the
Employee to the Customer, or
pass itself to the Customer, in order to allow the
Customer to call Employee
methods. Experiment with your classes interactively by importing the
Lunch class, calling its order method to run an
interaction, and then calling its result method to verify that the
Customer got what he or she ordered. If you
prefer, you can also simply code test cases as self-test code in the
file where your classes are defined, using the module __name__ trick in Chapter 18. In this simulation, the
Customer is the active agent; how would your
classes change if Employee were the object that
initiated customer/ employee interaction instead? Zoo Animal Hierarchy: Consider the class tree
shown in Figure 23-1. Code a set of six
class statements to model this taxonomy with
Python inheritance. Then, add a speak method to
each of your classes that prints a unique message, and a
reply method in your top-level
Animal superclass that simply calls
self.speak to invoke the category-specific message
printer in a subclass below (this will kick off an independent
inheritance search from self). Finally, remove the
speak method from your Hacker
class, so that it picks up the default above it. When
you're finished, your classes should work this way: % python
>>> from zoo import Cat, Hacker
>>> spot = Cat( )
>>> spot.reply( ) # Animal.reply; calls Cat.speak
meow
>>> data = Hacker( ) # Animal.reply; calls Primate.speak
>>> data.reply( )
Hello world!
The Dead Parrot Sketch: Consider the object
embedding structure captured in Figure 23-2. Code a
set of Python classes to implement this structure with composition.
Code your Scene object to define an
action method, and embed instances of
Customer, Clerk, and
Parrot classes—all three of which should
define a line method that prints a unique message.
The embedded objects may either inherit from a common superclass that
defines line and simply provide message text, or define
line themselves. In the end, your classes should
operate like this: % python
>>> import parrot
>>> parrot.Scene( ).action( ) # Activate nested objects.
customer: "that's one ex-bird!"
clerk: "no it isn't..."
parrot: None
|