Part VI, Classes and OOP

See Test Your Knowledge: Part VI Exercises in Chapter 31 for the exercises.

 

广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元

 
  1. 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
    Traceback (innermost last):
      File "<stdin>", line 1, in ?
    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 29.
    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:

    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):
            pass                               # Change to use self.data instead of x

    x = ListAdder([1, 2, 3])
    y = x + [4, 5, 6]
    print(y)                                   # Prints [1, 2, 3, 4, 5, 6]

    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.
  2. Operator overloading. The solution code (file mylist.py) uses a few operator overloading methods that the text didn’t say much about, but they should be straightforward to understand. 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 31:

    class MyList:
        def __init__(self, start):
            #self.wrapped = start[:]       # Copy start: no side effects
            self.wrapped = []              # Make sure it's a list here
            for x in start: self.wrapped.append(x)
        def __add__(self, other):
            return MyList(self.wrapped + other)
        def __mul__(self, time):
            return MyList(self.wrapped * time)
        def __getitem__(self, offset):
            return self.wrapped[offset]
        def __len__(self):
            return len(self.wrapped)
        def __getslice__(self, low, high):
            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):
            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()
        for c in x: print(c, end=' ')

    % 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 appending 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. 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).
  3. Subclassing. My solution (mysub.py) appears below. 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):
            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())

    % python mysub.py
    a
    ['p', 'a', 'm']
    ['s', 'p', 'a', 'm', 'eggs']
    ['s', 'p', 'a', 'm', 'toast']
    ['f', 'o', 'o', 'bar']
    (3, 2)

  4. Attribute methods. I worked through this exercise as follows. Notice that in Python 2.6, operators try to fetch attributes through __getattr__, too; you need to return a value to make them work. Caveat: as noted in Chapter 30, __getattr__ is not called for built-in operations in Python 3.0, so the following expression won’t work as shown; in 3.0, a class like this must redefine __X__ operator overloading methods explicitly. More on this in Chapters 30, 37, and 38.

    >>> class Meta:
    ...     def __getattr__(self, name):
    ...         print('get', name)
    ...     def __setattr__(self, name, value):
    ...         print('set', name, value)
    ...
    >>> x = Meta()
    >>> x.append
    get append
    >>> x.spam = "pork"
    set spam pork
    >>>
    >>> x + 2
    get __coerce__
    Traceback (innermost last):
      File "<stdin>", line 1, in ?
    TypeError: call of non-function
    >>>
    >>> x[1]
    get __getitem__
    Traceback (innermost last):
      File "<stdin>", line 1, in ?
    TypeError: call of non-function

    >>> x[1:5]
    get __len__
    Traceback (innermost last):
      File "<stdin>", line 1, in ?
    TypeError: call of non-function

  5. Set objects. Here’s the sort of interaction you should get. Comments explain which methods are called:

    % 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]                    # __getitem__
    ('h', 'o')

    >>> for c in z: print(c, end=' ')  # __getitem__
    ...
    h e l o
    >>> 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 only needs to replace 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:
        """

        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:

    >>> 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]

  6. 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 __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 __attrnames(self):
            ...unchanged...
        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)

    C:\misc> python testmixin.py
    <Instance of Sub(Super, ListInstance), address 7841200:
            name data1=spam
            name data2=eggs
            name data3=42
    >

  7. Composition. My solution is below (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

  8. 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. 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

  9. 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()