I l@ve RuBoard Previous Section Next Section

6.2 Class Basics

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.

6.2.1 Classes Generate Multiple Instance Objects

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.

6.2.1.1 Class objects provide default behavior
The class statement creates a class object and assigns it a name

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.

Assignments inside class statements make class attributes

Like modules, assignments in a class statement generate attributes in a class object; class attributes are accessed by name qualification (object.name).

Class attributes export object state and behavior

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.

6.2.1.2 Instance objects are generated from classes
Calling a class object like a function makes a new instance object

Each time a class is called, it generates and returns a new instance object.

Each instance object inherits class attributes and gets its own namespace

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.

Assignments to self in methods make per-instance attributes

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.

6.2.1.3 An example

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
Figure 6.1. Classes and instances are linked namespace objects
figs/lpy_0601.gif

6.2.2 Classes Are Specialized by I nheritance

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:

S uperclasses are listed in parentheses in a class header

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.

Classes inherit attributes from their superclasses

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 inherit attributes from all accessible classes

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.

Logic changes are made by subclassing, not by changing superclasses

By redefining superclass names in subclasses, subclasses override inherited behavior.

6.2.2.1 An example

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.

Figure 6.2. Specialization by overriding inherited names
figs/lpy_0602.gif

6.2.3 Classes Can Intercept Python Operators

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:

Methods with names such as __ X __ are special hooks

Python operator overloading is implemented by providing specially named methods to intercept operations.

Such methods are called automatically when Python evaluates operators

For instance, if an object inherits an _ _ add __ method, it is called when the object appears in a + expression.

Classes may override most built-in type operations

There are dozens of special operator method names for catching nearly every built-in type operation.

Operators allow classes to integrate with Python's object model

By overloading type operations, user-defined objects implemented with classes act just like built-ins.

6.2.3.1 An example

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.

Figure 6.3. Operators map to special methods
figs/lpy_0603.gif

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 Previous Section Next Section