See Test Your Knowledge: Part VI Exercises in Chapter 32 for the exercises.
Inheritance. Here’s the solution code for this exercise (file adder.py), along with some interactive tests. The __add__ overload has to appear only once, in the superclass, as it invokes type-specific add methods in subclasses:
class Adder:
def add(self, x, y):
print('not implemented!')
def __init__(self, start=[]):
self.data = start
def __add__(self, other): # Or in subclasses?
return self.add(self.data, other) # Or return type?
class ListAdder(Adder):
def add(self, x, y):
return x + y
class DictAdder(Adder):
def add(self, x, y):
new = {}
for k in x.keys(): new[k] = x[k]
for k in y.keys(): new[k] = y[k]
return new
% python
>>> from adder import *
>>> x = Adder()
>>> x.add(1, 2)
not implemented!
>>> x = ListAdder()
>>> x.add([1], [2])
[1, 2]
>>> x = DictAdder()
>>> x.add({1:1}, {2:2})
{1: 1, 2: 2}
>>> x = Adder([1])
>>> x + [2]
not implemented!
>>>
>>> x = ListAdder([1])
>>> x + [2]
[1, 2]
>>> [2] + x
In 3.3: TypeError: can only concatenate list (not "ListAdder") to list
Earlier: TypeError: __add__ nor __radd__ defined for these operands
Notice in the last test that you get an error for expressions where a class instance appears on the right of a +; if you want to fix this, use __radd__ methods, as described in “Operator Overloading” in Chapter 30.
If you are saving a value in the instance anyhow, you might as well rewrite the add method to take just one argument, in the spirit of other examples in this part of the book (this is adder2.py):
class Adder:
def __init__(self, start=[]):
self.data = start
def __add__(self, other): # Pass a single argument
return self.add(other) # The left side is in self
def add(self, y):
print('not implemented!')
class ListAdder(Adder):
def add(self, y):
return self.data + y
class DictAdder(Adder):
def add(self, y):
d = self.data.copy() # Change to use self.data instead of x
d.update(y) # Or "cheat" by using quicker built-ins
return d
x = ListAdder([1, 2, 3])
y = x + [4, 5, 6]
print(y) # Prints [1, 2, 3, 4, 5, 6]
z = DictAdder(dict(name='Bob')) + {'a':1}
print(z) # Prints {'name': 'Bob', 'a': 1}
Because values are attached to objects rather than passed around, this version is arguably more object-oriented. And, once you’ve gotten to this point, you’ll probably find that you can get rid of add altogether and simply define type-specific __add__ methods in the two subclasses.
Operator overloading. The solution code (file mylist.py) uses a handful of operator overloading methods we explored in Chapter 30. Copying the initial value in the constructor is important because it may be mutable; you don’t want to change or have a reference to an object that’s possibly shared somewhere outside the class. The __getattr__ method routes calls to the wrapped list. For hints on an easier way to code this in Python 2.2 and later, see Extending Types by Subclassing in Chapter 32:
class MyList:
def __init__(self, start):
#self.wrapped = start[:] # Copy start: no side effects
self.wrapped = list(start) # Make sure it's a list here
def __add__(self, other):
return MyList(self.wrapped + other)
def __mul__(self, time):
return MyList(self.wrapped * time)
def __getitem__(self, offset): # Also passed a slice in 3.X
return self.wrapped[offset] # For iteration if no __iter__
def __len__(self):
return len(self.wrapped)
def __getslice__(self, low, high): # Ignored in 3.X: uses __getitem__
return MyList(self.wrapped[low:high])
def append(self, node):
self.wrapped.append(node)
def __getattr__(self, name): # Other methods: sort/reverse/etc
return getattr(self.wrapped, name)
def __repr__(self): # Catchall display method
return repr(self.wrapped)
if __name__ == '__main__':
x = MyList('spam')
print(x)
print(x[2])
print(x[1:])
print(x + ['eggs'])
print(x * 3)
x.append('a')
x.sort()
print(' '.join(c for c in x))
c:\code> python mylist.py
['s', 'p', 'a', 'm']
a
['p', 'a', 'm']
['s', 'p', 'a', 'm', 'eggs']
['s', 'p', 'a', 'm', 's', 'p', 'a', 'm', 's', 'p', 'a', 'm']
a a m p s
Note that it’s important to copy the start value by calling list instead of slicing here, because otherwise the result may not be a true list and so will not respond to expected list methods, such as append (e.g., slicing a string returns another string, not a list). You would be able to copy a MyList start value by slicing because its class overloads the slicing operation and provides the expected list interface; however, you need to avoid slice-based copying for objects such as strings.
Subclassing. My solution (mysub.py) appears as follows. Your solution should be similar:
from mylist import MyList
class MyListSub(MyList):
calls = 0 # Shared by instances
def __init__(self, start):
self.adds = 0 # Varies in each instance
MyList.__init__(self, start)
def __add__(self, other):
print('add: ' + str(other))
MyListSub.calls += 1 # Class-wide counter
self.adds += 1 # Per-instance counts
return MyList.__add__(self, other)
def stats(self):
return self.calls, self.adds # All adds, my adds
if __name__ == '__main__':
x = MyListSub('spam')
y = MyListSub('foo')
print(x[2])
print(x[1:])
print(x + ['eggs'])
print(x + ['toast'])
print(y + ['bar'])
print(x.stats())
c:\code> python mysub.py
a
['p', 'a', 'm']
add: ['eggs']
['s', 'p', 'a', 'm', 'eggs']
add: ['toast']
['s', 'p', 'a', 'm', 'toast']
add: ['bar']
['f', 'o', 'o', 'bar']
(3, 2)
Attribute methods. I worked through this exercise as follows. Notice that in Python 2.X’s classic classes, operators try to fetch attributes through __getattr__, too; you need to return a value to make them work. As noted in Chapter 32 and elsewhere, __getattr__ is not called for built-in operations in Python 3.X (and in 2.X if new-style classes are used), so the expressions aren’t intercepted at all here; in new-style classes, a class like this must redefine __X__ operator overloading methods explicitly. More on this in Chapter 28, Chapter 31, Chapter 32, Chapter 38, and Chapter 39: it can impact much code!
c:\code>py −2>>>class Attrs:def __getattr__(self, name):print('get %s' % name)def __setattr__(self, name, value):print('set %s %s' % (name, value))>>>x = Attrs()>>>x.appendget append >>>x.spam = 'pork'set spam pork >>>x + 2get __coerce__ TypeError: 'NoneType' object is not callable >>>x[1]get __getitem__ TypeError: 'NoneType' object is not callable >>>x[1:5]get __getslice__ TypeError: 'NoneType' object is not callable c:\code>py −3>>>...same startup code...>>>x + 2TypeError: unsupported operand type(s) for +: 'Attrs' and 'int' >>>x[1]TypeError: 'Attrs' object does not support indexing >>>x[1:5]TypeError: 'Attrs' object is not subscriptable
Set objects. Here’s the sort of interaction you should get. Comments explain which methods are called. Also, note that sets are a built-in type in Python today, so this is largely just a coding exercise (see Chapter 5 for more on sets).
%python>>>from setwrapper import Set>>>x = Set([1, 2, 3, 4])# Runs __init__ >>>y = Set([3, 4, 5])>>>x & y# __and__, intersect, then __repr__ Set:[3, 4] >>>x | y# __or__, union, then __repr__ Set:[1, 2, 3, 4, 5] >>>z = Set("hello")# __init__ removes duplicates >>>z[0], z[-1], z[2:]# __getitem__ ('h', 'o', ['l', 'o']) >>>for c in z: print(c, end=' ')# __iter__ (else __getitem__) [3.X print] ... h e l o >>>''.join(c.upper() for c in z)# __iter__ (else __getitem__) 'HELO' >>>len(z), z# __len__, __repr__ (4, Set:['h', 'e', 'l', 'o']) >>>z & "mello", z | "mello"(Set:['e', 'l', 'o'], Set:['h', 'e', 'l', 'o', 'm'])
My solution to the multiple-operand extension subclass looks like the following class (file multiset.py). It needs to replace only two methods in the original set. The class’s documentation string explains how it works:
from setwrapper import Set
class MultiSet(Set):
"""
Inherits all Set names, but extends intersect and union to support
multiple operands; note that "self" is still the first argument
(stored in the *args argument now); also note that the inherited
& and | operators call the new methods here with 2 arguments, but
processing more than 2 requires a method call, not an expression;
intersect doesn't remove duplicates here: the Set constructor does;
"""
def intersect(self, *others):
res = []
for x in self: # Scan first sequence
for other in others: # For all other args
if x not in other: break # Item in each one?
else: # No: break out of loop
res.append(x) # Yes: add item to end
return Set(res)
def union(*args): # self is args[0]
res = []
for seq in args: # For all args
for x in seq: # For all nodes
if not x in res:
res.append(x) # Add new items to result
return Set(res)
Your interaction with the extension will look something like the following. Note that you can intersect by using & or calling intersect, but you must call intersect for three or more operands; & is a binary (two-sided) operator. Also, note that we could have called MultiSet simply Set to make this change more transparent if we used setwrapper.Set to refer to the original within multiset (the as clause in an import could rename the class too if desired):
>>>from multiset import *>>>x = MultiSet([1, 2, 3, 4])>>>y = MultiSet([3, 4, 5])>>>z = MultiSet([0, 1, 2])>>>x & y, x | y# Two operands (Set:[3, 4], Set:[1, 2, 3, 4, 5]) >>>x.intersect(y, z)# Three operands Set:[] >>>x.union(y, z)Set:[1, 2, 3, 4, 5, 0] >>>x.intersect([1,2,3], [2,3,4], [1,2,3])# Four operands Set:[2, 3] >>>x.union(range(10))# Non-MultiSets work, too Set:[1, 2, 3, 4, 0, 5, 6, 7, 8, 9] >>>w = MultiSet('spam')# String sets >>>wSet:['s', 'p', 'a', 'm'] >>>''.join(w | 'super')'spamuer' >>>(w | 'super') & MultiSet('slots')Set:['s']
Class tree links. Here is the way I changed the lister classes, and a rerun of the test to show its format. Do the same for the dir-based version, and also do this when formatting class objects in the tree climber variant:
class ListInstance:
def __attrnames(self):
...unchanged...
def __str__(self):
return '<Instance of %s(%s), address %s:\n%s>' % (
self.__class__.__name__, # My class's name
self.__supers(), # My class's own supers
id(self), # My address
self.__attrnames()) # name=value list
def __supers(self):
names = []
for super in self.__class__.__bases__: # One level up from class
names.append(super.__name__) # name, not str(super)
return ', '.join(names)
# Or: ', '.join(super.__name__ for super in self.__class__.__bases__)
c:\code> py listinstance-exercise.py
<Instance of Sub(Super, ListInstance), address 43671000:
data1=spam
data2=eggs
data3=42
>
Composition. My solution is as follows (file lunch.py), with comments from the description mixed in with the code. This is one case where it’s probably easier to express a problem in Python than it is in English:
class Lunch:
def __init__(self): # Make/embed Customer, Employee
self.cust = Customer()
self.empl = Employee()
def order(self, foodName): # Start Customer order simulation
self.cust.placeOrder(foodName, self.empl)
def result(self): # Ask the Customer about its Food
self.cust.printFood()
class Customer:
def __init__(self): # Initialize my food to None
self.food = None
def placeOrder(self, foodName, employee): # Place order with Employee
self.food = employee.takeOrder(foodName)
def printFood(self): # Print the name of my food
print(self.food.name)
class Employee:
def takeOrder(self, foodName): # Return Food, with desired name
return Food(foodName)
class Food:
def __init__(self, name): # Store food name
self.name = name
if __name__ == '__main__':
x = Lunch() # Self-test code
x.order('burritos') # If run, not imported
x.result()
x.order('pizza')
x.result()
% python lunch.py
burritos
pizza
Zoo animal hierarchy. Here is the way I coded the taxonomy in Python (file zoo.py); it’s artificial, but the general coding pattern applies to many real structures, from GUIs to employee databases to spacecraft. Notice that the self.speak reference in Animal triggers an independent inheritance search, which finds speak in a subclass. Test this interactively per the exercise description. Try extending this hierarchy with new classes, and making instances of various classes in the tree:
class Animal:
def reply(self): self.speak() # Back to subclass
def speak(self): print('spam') # Custom message
class Mammal(Animal):
def speak(self): print('huh?')
class Cat(Mammal):
def speak(self): print('meow')
class Dog(Mammal):
def speak(self): print('bark')
class Primate(Mammal):
def speak(self): print('Hello world!')
class Hacker(Primate): pass # Inherit from Primate
The Dead Parrot Sketch. Here’s how I implemented this one (file parrot.py). Notice how the line method in the Actor superclass works: by accessing self attributes twice, it sends Python back to the instance twice, and hence invokes two inheritance searches—self.name and self.says() find information in the specific subclasses:
class Actor:
def line(self): print(self.name + ':', repr(self.says()))
class Customer(Actor):
name = 'customer'
def says(self): return "that's one ex-bird!"
class Clerk(Actor):
name = 'clerk'
def says(self): return "no it isn't..."
class Parrot(Actor):
name = 'parrot'
def says(self): return None
class Scene:
def __init__(self):
self.clerk = Clerk() # Embed some instances
self.customer = Customer() # Scene is a composite
self.subject = Parrot()
def action(self):
self.customer.line() # Delegate to embedded
self.clerk.line()
self.subject.line()