I l@ve RuBoard |
If you've never been exposed to OOP in the past, classes can be somewhat complicated if taken in a single dose. To make classes easier to absorb, let's start off by taking a quick first look at classes in action here, to illustrate the three distinctions described previously. We'll expand on the details in a moment; but in their basic form, Python classes are easy to understand.
As we mentioned at the end of Chapter 5, classes are mostly just a namespace, much like modules. But unlike modules, classes also have support for multiple copies, namespace inheritance, and operator overloading. Let's look at the first of these extensions here.
To understand how the multiple copies idea works, you have to first understand that there are two kinds of objects in Python's OOP model—class objects and instance objects. Class objects provide default behavior and serve as generators for instance objects. Instance objects are the real objects your programs process; each is a namespace in its own right, but inherits (i.e., has access to) names in the class it was created from. Class objects come from statements and instances from calls; each time you call a class, you get a new instance. Now, pay attention, because we're about to summarize the bare essentials of Python OOP.
Like def, the Python class statement is an executable statement; when run, it generates a new class object and assigns it the name in the class header.
Like modules, assignments in a class statement generate attributes in a class object; class attributes are accessed by name qualification (object.name).
Attributes of a class object record state information and behavior, to be shared by all instances created from the class; function def statements inside a class generate methods, which process instances.
Each time a class is called, it generates and returns a new instance object.
Instance objects generated from classes are new namespaces; they start out empty, but inherit attributes that live in the class object they were generated from.
Inside class method functions, the first argument (called self by convention) references the instance object being processed; assignments to attributes of self create or change data in the instance, not the class.
Apart from a few details, that's all there is to OOP in Python. Let's turn to a real example to show how these ideas work in practice. First, let's define a class called FirstClass, using the Python class statement:
>>> class FirstClass: # define a class object ... def setdata(self, value): # define class methods ... self.data = value # self is the instance ... def display(self): ... print self.data # self.data: per instance
Like all compound statements, class starts with a header line that lists the class name, followed by a body of one or more nested and indented statements. Here, the nested statements are defs; they define functions that implement the behavior the class means to export. As we've seen, def is an assignment; here, it assigns to names in the class statement's scope and so generates attributes of the class. Functions inside a class are usually called method functions; they're normal defs, but the first argument automatically receives an implied instance object when called. We need a couple of instances to see how:
>>> x = FirstClass() # make two instances >>> y = FirstClass() # each is a new namespace
By calling the class as we do, we generate instance objects, which are just namespaces that get the class's attributes for free. Properly speaking, at this point we have three objects—two instances and a class; but really, we have three linked namespaces, as sketched in Figure 6.1. In OOP terms, we say that x is a FirstClass, as is y. The instances start empty, but have links back to the class; if we qualify an instance with the name of an attribute in the class object, Python fetches the name from the class (unless it also lives in the instance):
>>> x.setdata("King Arthur") # call methods: self is x or y >>> y.setdata(3.14159) # runs: FirstClass.setdata(y, 3.14159)
Neither x nor y has a setdata of its own; instead, Python follows the link from instance to class if an attribute doesn't exist in an instance. And that's about all there is to inheritance in Python: it happens at attribute qualification time, and just involves looking up names in linked objects (by following the is-a links in Figure 6.1).
In the setdata function in FirstClass, the value passed in is assigned to self.data; within a method, self automatically refers to the instance being processed (x or y), so the assignments store values in the instances' namespaces, not the class (that's how the data names in Figure 6.1 get created). Since classes generate multiple instances, methods must go through the self argument to get to the instance to be processed. When we call the class's display method to print self.data, we see that it's different in each instance; on the other hand, display is the same in x and y, since it comes (is inherited) from the class:
>>> x.display() # self.data differs in each King Arthur >>> y.display() 3.14159
Notice that we stored different object types in the data member (a string and a float). Like everything else in Python, there are no declarations for instance attributes (sometimes called members); they spring into existence the first time they are assigned a value, just like simple variables. In fact, we can change instance attributes either in the class itself by assigning to self in methods, or outside the class by assigning to an explicit instance object:
>>> x.data = "New value" # can get/set attributes >>> x.display() # outside the class too New value
Unlike modules, classes also allow us to make changes by introducing new components (subclasses ), instead of changing existing components in place. We've already seen that instance objects generated from a class inherit its attributes. Python also allows classes to inherit from other classes, and this opens the door to what are usually called frameworks —hierarchies of classes that specialize behavior by overriding attributes lower in the hierarchy. The key ideas behind this machinery are:
To inherit attributes from another class, just list the class in parentheses in a class statement's header. The class that inherits is called a subclass, and the class that is inherited from is its superclass.
Just like instances, a class gets all the names defined in its superclasses for free; they're found by Python automatically when qualified, if they don't exist in the subclass.
Instances get names from the class they are generated from, as well as all of the class's superclasses; when looking for a name, Python checks the instance, then its class, then all superclasses above.
By redefining superclass names in subclasses, subclasses override inherited behavior.
Our next example builds on the one before. Let's define a new class, SecondClass, which inherits all of FirstClass's names and provides one of its own:
>>> class SecondClass(FirstClass): # inherits setdata ... def display(self): # changes display ... print 'Current value = "%s"' % self.data
SecondClass redefines the display method to print with a different format. But because SecondClass defines an attribute of the same name, it replaces the display attribute in FirstClass. Inheritance works by searching up from instances, to subclasses, to superclasses, and stops at the first appearance of an attribute name it finds. Since it finds the display name in SecondClass before the one in FirstClass, we say that SecondClass overrides FirstClass's display. In other words, SecondClass specializes FirstClass, by changing the behavior of the display method. On the other hand, SecondClass (and instances created from it) still inherits the setdata method in FirstClass verbatim. Figure 6.2 sketches the namespaces involved; let's make an instance to demonstrate:
>>> z = SecondClass() >>> z.setdata(42) # setdata found in FirstClass >>> z.display() # finds overridden method in SecondClass Current value = "42"
As before, we make a SecondClass instance object by calling it. The setdata call still runs the version in FirstClass, but this time the display attribute comes from SecondClass and prints a different message. Now here's a very important thing to notice about OOP: the specialization introduced in SecondClass is completely external to FirstClass; it doesn't effect existing or future FirstClass objects, like x from the prior example:
>>> x.display() # x is still a FirstClass instance (old message) New value
Naturally, this is an artificial example, but as a rule, because changes can be made in external components (subclasses), classes often support extension and reuse better than functions or modules do.
Finally, let's take a quick look at the third major property of classes: operator overloading in action. In simple terms, operator overloading lets objects we implement with classes respond to operations we've already seen work on built-in types: addition, slicing, printing, qualification, and so on. Although we could implement all our objects' behavior as method functions, operator overloading lets our objects be more tightly integrated with Python's object model. Moreover, because operator overloading makes our own objects act like built-ins, it tends to foster object interfaces that are more consistent and easy to learn. The main ideas are:
Python operator overloading is implemented by providing specially named methods to intercept operations.
For instance, if an object inherits an _ _ add __ method, it is called when the object appears in a + expression.
There are dozens of special operator method names for catching nearly every built-in type operation.
By overloading type operations, user-defined objects implemented with classes act just like built-ins.
On to another example. This time, we define a subclass of SecondClass, which implements three special attributes: __ init _ _ is called when a new instance object is being constructed (self is the new ThirdClass object), and _ _ add __ and __ mul __ are called when a ThirdClass instance appears in + and * expressions, respectively:
>>> class ThirdClass(SecondClass): # is-a SecondClass ... def __init__(self, value): # on "ThirdClass(value)" ... self.data = value ... def __add__(self, other): # on "self + other" ... return ThirdClass(self.data + other) ... def __mul__(self, other): ... self.data = self.data * other # on "self * other" >>> a = ThirdClass("abc") # new __init__ called >>> a.display() # inherited method Current value = "abc" >>> b = a + 'xyz' # new __add__ called: makes a new instance >>> b.display() Current value = "abcxyz" >>> a * 3 # new __mul__ called: changes instance in-place >>> a.display() Current value = "abcabcabc"
ThirdClass is a SecondClass, so its instances inherit display from SecondClass. But ThirdClass generation calls pass an argument now ("abc"); it's passed to the value argument in the __ init __ constructor and assigned to self.data there. Further, ThirdClass objects can show up in + and * expressions; Python passes the instance object on the left to the self argument and the value on the right to other, as illustrated in Figure 6.3.
Special methods such as __ init __ and __ add __ are inherited by subclasses and instances, just like any other name assigned in a class statement. Notice that the _ _ add __ method makes a new object (by calling ThirdClass with the result value), but __ mul __ changes the current instance object in place (by reassigning a self attribute). The * operator makes a new object when applied to built-in types such as numbers and lists, but you can interpret it any way you like in class objects.[1]
[1] But you probably shouldn't (one reviewer went so far as to call this example "evil!"). Common practice dictates that overloaded operators should work the same way built-in operator implementations do. In this case, that means our __mul__ method should return a new object as its result, rather than changing the instance (self) in place; a mul method may be better style than a * overload here (e.g., a.mul(3) instead of a * 3). On the other hand, one person's common practice may be another person's arbitrary constraint.
I l@ve RuBoard |