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.[44]
[44] 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.