6.8 Designing with Classes

6.8 Designing with Classes

So far, we've concentrated on the OOP tool in Python ”the class. But OOP is also about design issues ”how to use classes to model useful objects. In this section, we're going to touch on a few OOP core ideas and look at some examples that are more realistic than the ones we've seen so far. Most of the design terms we throw out here require more explanation than we can provide; if this section sparks your curiosity , we suggest exploring a text on OOP design or design patterns as a next step.

6.8.1 Python and OOP

Python's implementation of OOP can be summarized by three ideas:

Inheritance

Is based on attribute lookup in Python (in X. name expressions).

Polymorphism

In X.method , the meaning of method depends on the type (class) of X .

Encapsulation

Methods and operators implement behavior; data hiding is a convention by default.

By now, you should have a good feel for what inheritance is all about in Python. Python's flavor of polymorphism flows from its lack of type declarations. Because attributes are always resolved at runtime, objects that implement the same interfaces are interchangeable; clients don't need to know what sort of object is implementing a method they call. [4] Encapsulation means packaging in Python, not privacy; privacy is an option, as we'll see later in this chapter.

[4] Some OOP languages also define polymorphism to mean overloading functions based on the type signatures of their arguments. Since there is no type declaration in Python, the concept doesn't really apply, but type-base selections can be always be coded using if tests and type(X) built-in functions (e.g., if type(X) is type(0): doIntegerCase() ).

6.8.2 OOP and Inheritance: "is-a"

We've talked about the mechanics of inheritance in depth already, but we'd like to show you an example of how it can be used to model real-world relationships. From a programmer's point of view, inheritance is kicked off by attribute qualifications and searches for a name in an instance, its class, and then its superclasses. From a designer's point of view, inheritance is a way to specify set membership. A class defines a set of properties that may be inherited by more specific sets (i.e., subclasses).

To illustrate , let's put that pizza-making robot we talked about at the start of the chapter to work. Suppose we've decided to explore alternative career paths and open a pizza restaurant. One of the first things we'll need to do is hire employees to service customers, make the pizza, and so on. Being engineers at heart, we've also decided to build a robot to make the pizzas; but being politically and cybernetically correct, we've also decided to make our robot a full-fledged employee, with a salary.

Our pizza shop team can be defined by the following classes in the example file employees.py . It defines four classes and some self-test code. The most general class, Employee , provides common behavior such as bumping up salaries ( giveRaise ) and printing (__ repr __ ). There are two kinds of employees, and so two subclasses of Employee--Chef and Server . Both override the inherited work method to print more specific messages. Finally, our pizza robot is modeled by an even more specific class: PizzaRobot is a kind of Chef , which is a kind of Employee . In OOP terms, we call these relationships "is-a" links: a robot is a chef, which is a(n) employee.

 class Employee:     def __init__(self, name, salary=0):         self.name   = name         self.salary = salary     def giveRaise(self, percent):         self.salary = self.salary + (self.salary * percent)     def work(self):         print self.name, "does stuff"     def __repr__(self):         return "<Employee: name=%s, salary=%s>" % (self.name, self.salary) class Chef(Employee):     def __init__(self, name):         Employee.__init__(self, name, 50000)     def work(self):         print self.name, "makes food" class Server(Employee):     def __init__(self, name):         Employee.__init__(self, name, 40000)     def work(self):         print self.name, "interfaces with customer" class PizzaRobot(Chef):     def __init__(self, name):         Chef.__init__(self, name)     def work(self):         print self.name, "makes pizza" if __name__ == "__main__":     bob = PizzaRobot('bob')       # make a robot named bob     print bob                     # runs inherited __repr__     bob.giveRaise(0.20)           # give bob a 20% raise     print bob; print     for klass in Employee, Chef, Server, PizzaRobot:         obj = klass(klass.__name__)         obj.work() 

When we run this module's self-test code, we create a pizza-making robot named bob , which inherits names from three classes: PizzaRobot , Chef , and Employee . For instance, printing bob runs the Employee. __ repr __ method, and giving bob a raise invokes Employee.giveRaise , because that's where inheritance finds it.

 C:\python\examples>  python employees.py  <Employee: name=bob, salary=50000> <Employee: name=bob, salary=60000.0> Employee does stuff Chef makes food Server interfaces with customer PizzaRobot makes pizza 

In a class hierarchy like this, you can usually make instances of any of the classes, not just the ones at the bottom. For instance, the for loop in this module's self-test code creates instances of all four classes; each responds differently when asked to work, because the work method is different in each. Really, these classes just simulate real world objects; work prints a message for the time being, but could be expanded to really work later.

6.8.3 OOP and Composition: "has-a"

We introduced the notion of composition at the start of this chapter. From a programmer's point of view, composition involves embedding other objects in a container object and activating them to implement container methods. To a designer, composition is another way to represent relationships in a problem domain. But rather than set membership, composition has to do with components ”parts of a whole. Composition also reflects the relationships between parts ; it's usually called a "has-a" relationship, when OOP people speak of such things.

Now that we've implemented our employees, let's throw them in the pizza shop and let them get busy. Our pizza shop is a composite object; it has an oven, and employees like servers and chefs. When a customer enters and places an order, the components of the shop spring into action ”the server takes an order, the chef makes the pizza, and so on. The following example simulates all the objects and relationships in this scenario:

 from employees import PizzaRobot, Server class Customer:     def __init__(self, name):         self.name = name     def order(self, server):         print self.name, "orders from", server     def pay(self, server):         print self.name, "pays for item to", server class Oven:     def bake(self):         print "oven bakes" class PizzaShop:     def __init__(self):         self.server = Server('Pat')         # embed other objects         self.chef   = PizzaRobot('Bob')     # a robot named bob         self.oven   = Oven()     def order(self, name):         customer = Customer(name)           # activate other objects         customer.order(self.server)         # customer orders from server         self.chef.work()         self.oven.bake()         customer.pay(self.server) if __name__ == "__main__":     scene = PizzaShop()                     # make the composite     scene.order('Homer')                    # simulate Homer's order     print '...'     scene.order('Shaggy')                   # simulate Shaggy's order 

The PizzaShop class is a container and controller; its constructor makes and embeds instances of the employee classes we wrote in the last section, as well as an Oven class defined here. When this module's self-test code calls the PizzaShop order method, the embedded objects are asked to carry out their actions in turn . Notice that we make a new Customer object for each order, and pass on the embedded Server object to Customer methods; customers come and go, but the server is part of the pizza shop composite. Also notice that employees are still involved in an inheritance relationship; composition and inheritance are complementary tools:

 C:\python\examples>  python pizzashop.py  Homer orders from <Employee: name=Pat, salary=40000> Bob makes pizza oven bakes Homer pays for item to <Employee: name=Pat, salary=40000> ... Shaggy orders from <Employee: name=Pat, salary=40000> Bob makes pizza oven bakes Shaggy pays for item to <Employee: name=Pat, salary=40000> 

When we run this module, our pizza shop handles two orders ”one from Homer, and then one from Shaggy. Again, this is mostly just a toy simulation; a real pizza shop would have more parts, and there's no real pizza to be had here. But the objects and interactions are representative of composites at work. As a rule of thumb, classes can represent just about any objects and relationships you can express in a sentence ; just replace nouns with classes and verbs with methods, and you have a first cut at a design.

Why You Will Care: Classes and Persistence

Besides allowing us to simulate real-world interactions, the pizza shop classes could also be used as the basis of a persistent restaurant database. As we'll see in Chapter 10, instances of classes can be stored away on disk in a single step using Python's pickle or shelve modules. The object pickling interface is remarkably easy to use:

 import pickle object = someClass() file   = open(filename, 'w')         # create external file pickle.dump(object, file)            # save object in file file   = open(filename, 'r') object = pickle.load(file)           # fetch it back later 

Shelves are similar, but they automatically pickle objects to an access-by-key database:

 import shelve object = someClass() dbase  = shelve.open('filename') dbase['key'] = object                 # save under key object = dbase['key']                 # fetch it back later 

(Pickling converts objects to serialized byte streams, which may be stored in files, sent across a network, and so on.) In our example, using classes to model employees means we can get a simple database of employees and shops for free: pickling such instance objects to a file makes them persistent across Python program executions. See Chapter 10 for more details on pickling.

6.8.4 OOP and Delegation

Object-oriented programmers often talk about something called delegation too, which usually implies controller objects that embed other objects, to which they pass off operation requests . The controllers can take care of administrative activities such as keeping track of accesses and so on. In Python, delegation is often implemented with the __ getattr __ method hook; because it intercepts accesses to nonexistent attributes, a wrapper class can use __ getattr __ to route arbitrary accesses to a wrapped object. For instance:

 class wrapper:     def __init__(self, object):         self.wrapped = object                        # save object     def __getattr__(self, attrname):         print 'Trace:', attrname                     # trace fetch         return getattr(self.wrapped, attrname)       # delegate fetch 

You can use this module's wrapper class to control any object with attributes ”lists, dictionaries, and even classes and instances. Here, the class simply prints a trace message on each attribute access:

 >>>  from trace import wrapper  >>>  x = wrapper([1,2,3])  # wrap a list >>>  x.append(4)  # delegate to list method Trace: append >>>  x.wrapped  # print my member [1, 2, 3, 4] >>>  x = wrapper({"a": 1, "b": 2})  # wrap a dictionary >>>  x.keys()  # delegate to dictionary method Trace: keys ['a', 'b'] 

6.8.5 Extending Built-in Object Types

Classes are also commonly used to extend the functionality of Python's built-in types, to support more exotic data structures. For instance, to add queue insert and delete methods to lists, you can code classes that wrap (embed) a list object, and export insert and delete methods that process the list.

Remember those set functions we wrote in Chapter 4? Here's what they look like brought back to life as a Python class. The following example implements a new set object type, by moving some of the set functions we saw earlier in the book to methods, and adding some basic operator overloading. For the most part, this class just wraps a Python list with extra set operations, but because it's a class, it also supports multiple instances and customization by inheritance in subclasses.

 class Set:    def __init__(self, value = []):    # constructor        self.data = []                 # manages a list        self.concat(value)    def intersect(self, other):        # other is any sequence        res = []                       # self is the subject        for x in self.data:            if x in other:             # pick common items                res.append(x)        return Set(res)                # return a new Set    def union(self, other):            # other is any sequence        res = self.data[:]             # copy of my list        for x in other:                # add items in other            if not x in res:                res.append(x)        return Set(res)    def concat(self, value):           # value: list, Set...        for x in value:                # removes duplicates           if not x in self.data:                self.data.append(x)    def __len__(self):          return len(self.data)        # on len(self)    def __getitem__(self, key): return self.data[key]        # on self[i]    def __and__(self, other):   return self.intersect(other) # on self & other    def __or__(self, other):    return self.union(other)     # on self  other    def __repr__(self):         return 'Set:' + `self.data`  # on print 

By overloading indexing, our set class can often masquerade as a real list. Since we're going to ask you to interact with and extend this class in an exercise at the end of this chapter, we won't say much more about this code until Appendix C.

6.8.6 Multiple Inheritance

When we discussed details of the class statement, we mentioned that more than one superclass can be listed in parentheses in the header line. When you do this, you use something called multiple inheritance; the class and its instances inherit names from all listed superclasses. When searching for an attribute, Python searches superclasses in the class header from left to right until a match is found. Technically, the search proceeds depth-first, and then left to right, since any of the superclasses may have superclasses of its own.

In theory, multiple inheritance is good for modeling objects which belong to more than one set. For instance, a person may be an engineer, a writer, a musician, and so on, and inherit properties from all such sets. In practice, though, multiple inheritance is an advanced tool and can become complicated if used too much; we'll revisit this as a gotcha at the end of the chapter. But like everything else in programming, it's a useful tool when applied well.

One of the most common ways multiple inheritance is used is to "mix in" general-purpose methods from superclasses. Such superclasses are usually called mixin classes; they provide methods you add to application classes by inheritance. For instance, Python's default way to print a class instance object isn't incredibly useful:

 >>>  class Spam:  ...  def __init__(self):  # no __repr__ ...  self.data1 = "food"  ... >>>  X = Spam()  >>>  print X  # default format: class, address <Spam instance at 87f1b0> 

As seen in the previous section on operator overloading, you can provide a __ repr _ _ method to implement a custom string representation of your own. But rather than code a __ repr __ in each and every class you wish to print, why not code it once in a general-purpose tool class, and inherit it in all classes?

That's what mixins are for. The following code defines a mixin class called Lister that overloads the _ _ repr __ method for each class that includes Lister in its header line. It simply scans the instance's attribute dictionary (remember, it's exported in __ dict __ ) to build up a string showing the names and values of all instance attributes. Since classes are objects, Lister 's formatting logic can be used for instances of any subclass; it's a generic tool.

Lister uses two special tricks to extract the instance's classname and address. Instances have a built-in __ class __ attribute that references the class the instance was created from, and classes have a __ name __ that is the name in the header, so self. __ class _ _ . __ name __ fetches the name of an instance's class. You get the instance's memory address by calling the built-in id function, which returns any object's address:

 # Lister can be mixed-in to any class, to # provide a formatted print of instances # via inheritance of __repr__ coded here; # self is the instance of the lowest class; class Lister:    def __repr__(self):        return ("<Instance of %s, address %s:\n%s>" %                          (self.__class__.__name__,      # my class's name                           id(self),                     # my address                           self.attrnames()) )           # name=value list    def attrnames(self):        result = ''        for attr in self.__dict__.keys():      # scan instance namespace dict            if attr[:2] == '__':                result = result + "\tname %s=<built-in>\n" % attr            else:                result = result + "\tname %s=%s\n" % (attr, self.__dict__[attr])        return result 

Now, the Lister class is useful for any class you write ”even classes that already have a superclass. This is where multiple inheritance comes in handy: by adding Lister to the list of superclasses in a class header, you get its __ repr _ _ for free, while still inheriting from the existing superclass:

 from mytools import Lister            # get tool class class Super:     def __init__(self):               # superclass __init__         self.data1 = "spam" class Sub(Super, Lister):             # mix-in a __repr__     def __init__(self):               # Lister has access to self         Super.__init__(self)         self.data2 = "eggs"           # more instance attrs         self.data3 = 42 if __name__ == "__main__":     X = Sub()     print X                           # mixed-in repr 

Here, Sub inherits names from both Super and Lister ; it's a composite of its own names and names in both its superclasses. When you make a Sub instance and print it, you get the custom representation mixed in from Lister :

 C:\python\examples>  python testmixin.py  <Instance of Sub, address 7833392:         name data3=42         name data2=eggs         name data1=spam > 

Lister works in any class it's mixed into, because self refers to an instance of the subclass that pulls Lister in, whatever that may be. If you later decide to extend Lister 's __ repr __ to also print class attributes an instance inherits, you're safe; because it's an inherited method, changing Lister 's __ repr __ updates each subclass that mixes it in. [5] In some sense, mixin classes are the class equivalent of modules. Here is Lister working in single-inheritance mode, on a different class's instances; like we said, OOP is about code reuse:

[5] For the curious reader, classes also have a built-in attribute called __ bases __ , which is a tuple of the class's superclass objects. A general-purpose class hierarchy lister or browser can traverse from an instance's __ class __ to its class, and then from the class's __ bases __ to all superclasses recursively. We'll revisit this idea in an exercise, but see other books or Python's manuals for more details on special object attributes.

 >>>  from mytools import Lister  >>>  class x(Lister):  ...  pass  ... >>>  t = x()  >>>  t.a = 1; t.b = 2; t.c = 3  >>>  t  <Instance of x, address 7797696:         name b=2         name a=1         name c=3 > 

6.8.7 Classes Are Objects: Generic Object Factories

Because classes are objects, it's easy to pass them around a program, store them in data structures, and so on. You can also pass classes to functions that generate arbitrary kinds of objects; such functions are sometimes called factories in OOP design circles. They are a major undertaking in a strongly typed language such as C++, but almost trivial in Python: the apply function we met in Chapter 4 can call any class with any argument in one step, to generate any sort of instance: [6]

[6] Actually, apply can call any callable object; that includes functions, classes, and methods. The factory function here can run any callable, not just a class (despite the argument name).

 def factory(aClass, *args):                 # varargs tuple     return apply(aClass, args)              # call aClass class Spam:     def doit(self, message):         print message class Person:     def __init__(self, name, job):         self.name = name         self.job  = job object1 = factory(Spam)                      # make a Spam object2 = factory(Person, "Guido", "guru")   # make a Person 

In this code, we define an object generator function, called factory . It expects to be passed a class object (any class will do), along with one or more arguments for the class's constructor. The function uses apply to call the function and return an instance. The rest of the example simply defines two classes and generates instances of both by passing them to the factory function. And that's the only factory function you ever need write in Python; it works for any class and any constructor arguments. The only possible improvement worth noting: to support keyword arguments in constructor calls, the factory can collect them with a **args argument and pass them as a third argument to apply :

 def factory(aClass, *args, **kwargs):        # +kwargs dict     return apply(aClass, args, kwargs)       # call aClass 

By now, you should know that everything is an "object" in Python; even things like classes, which are just compiler input in languages like C++. However, only objects derived from classes are OOP objects in Python; you can't do inheritance with nonclass-based objects such as lists and numbers , unless you wrap them in classes.

6.8.8 Methods Are Objects: Bound or Unbound

Speaking of objects, it turns out that methods are a kind of object too, much like functions. Because class methods can be accessed from either an instance or a class, they actually come in two flavors in Python:

Unbound class methods: no self

Accessing a class's function attribute by qualifying a class returns an unbound method object . To call it, you must provide an instance object explicitly as its first argument.

Bound instance methods: self + function pairs

Accessing a class's function attribute by qualifying an instance returns a bound method object . Python automatically packages the instance with the function in the bound method object, so we don't need to pass an instance to call the method.

Both kinds of methods are full-fledged objects; they can be passed around, stored in lists, and so on. Both also require an instance in their first argument when run (i.e., a value for self ), but Python provides one for you automatically when calling a bound method through an instance. For example, suppose we define the following class:

 class Spam:     def doit(self, message):         print message 

Now, we can make an instance, and fetch a bound method without actually calling it. An object.name qualification is an object expression; here, it returns a bound method object that packages the instance ( object1 ) with the method function ( Spam.doit ). We can assign the bound method to another name and call it as though it were a simple function:

 object1 = Spam() x = object1.doit        # bound method object x('hello world')        # instance is implied 

On the other hand, if we qualify the class to get to doit , we get back an unbound method object, which is simply a reference to the function object. To call this type of method, pass in an instance in the leftmost argument:

 t = Spam.doit           # unbound method object t(object1, 'howdy')     # pass in instance 

Most of the time, you call methods immediately after fetching them with qualification (e.g., self.attr(args) ), so you don't always notice the method object along the way. But if you start writing code that calls objects generically, you need to be careful to treat unbound methods specially; they require an explicit object.



Learning Python
Learning Python: Powerful Object-Oriented Programming
ISBN: 0596158068
EAN: 2147483647
Year: 1999
Pages: 156
Authors: Mark Lutz

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net