Chapter 15 · pp. 169–176
Classes and Objects
Up to now, functions have organized your code and built-in types have organized your data. This chapter introduces classes, which let you define your own types and bundle data together. You will build Point and Rectangle types, see how objects can be changed inside functions, and learn how copying and debugging work with objects. (p. 169)
Defining a class and making instances
A class is a programmer-defined type. To make one, you write the keyword class, a name, and an indented body. A short docstring explaining the class is a good body to start with:
class Point:
"""Represents a point in 2-D space."""
This definition creates a class object named Point. If you type Point in the interpreter, it reports something like <class '__main__.Point'>. The class object acts like a factory: calling Point() as if it were a function builds a brand-new object. Why bother? A point could be stored as two loose variables or as a tuple, but a class lets you give the data a name and a structure of its own. (pp. 169–170)
Making a new object from a class is called instantiation, and the result is an instance of the class. For example:
blank = Point()
The variable blank now refers to a new Point instance. Printing it shows the class name plus the memory location in hexadecimal (the 0x prefix marks hexadecimal), such as <__main__.Point object at 0xb7e9d3ac>. Every object is an instance of some class. (p. 170)
Attributes and object diagrams
An instance can hold named values called attributes. You set them with dot notation:
blank.x = 3.0
blank.y = 4.0
You read them the same way. The expression blank.x means "go to the object blank refers to and get its x." An attribute named x and an ordinary variable named x are separate things, so x = blank.x causes no conflict. Dot notation works inside any expression, for instance math.sqrt(blank.x**2 + blank.y**2) gives 5.0 for this point. (pp. 170–171)
An object diagram is a state diagram that shows an object, its attributes, and what each attribute refers to. For blank, you draw a box labeled Point with arrows from x and y to the floats 3.0 and 4.0. Drawing such diagrams is a good way to keep track of what refers to what. (p. 170)
Objects in functions
You can pass an instance to a function like any other argument. A function such as print_point(p) can print (%g, %g) using p.x and p.y. Calling print_point(blank) displays (3.0, 4.0). (p. 171)
Functions can also build and return instances. The function find_center(rect) creates a new Point p, sets p.x to rect.corner.x + rect.width/2 and p.y to rect.corner.y + rect.height/2, and returns p. (p. 172) The center is half the width and half the height away from the corner. For a rectangle with corner (0, 0), width 100 and height 200, the center is (50, 100). (p. 172)
Embedded objects: rectangles
Sometimes you must decide which attributes best describe a thing. A rectangle (assumed to be vertical or horizontal) could be described by one corner plus width and height, or by two opposite corners. The book picks the first design. (p. 171)
To build one, create a Rectangle instance and fill in its attributes:
box = Rectangle()
box.width = 100.0
box.height = 200.0
box.corner = Point()
box.corner.x = 0.0
box.corner.y = 0.0
The Point stored in box.corner is an embedded object, meaning an object that is an attribute of another object. To reach its coordinates you chain dots. The expression box.corner.x means: take box, select its corner, then select x from that corner. (p. 172)
Objects are mutable
You can change an object by assigning to its attributes, for example box.width = box.width + 50. (p. 173)
Functions can change objects as well. In
def grow_rectangle(rect, dwidth, dheight):
rect.width += dwidth
rect.height += dheight
the parameter rect is an alias for the argument, so both names refer to the same object. If box is 150 by 300, then grow_rectangle(box, 50, 100) makes it 200 by 400. The change is visible outside the function because no copy was made. (p. 173)
Copying objects
Aliasing can make programs hard to follow, because a change made through one name shows up through another. Copying is often a safer option. The copy module has a function copy.copy that duplicates an object. (p. 173)
If p2 = copy.copy(p1), the two points hold the same data, but p1 is p2 is False, as you would expect. Many people also expect p1 == p2 to be True, but it is False. For instances of your own classes, the default == works like is: it tests identity (same object), not equivalence (same contents). Python cannot know what "equivalent" should mean for a new type unless you tell it. (pp. 173–174)
Copying gets trickier with embedded objects. box2 = copy.copy(box) makes a new Rectangle, so box2 is box is False. But the corner is not copied, so box2.corner is box.corner is True. This is a shallow copy: it copies the object and the references inside it, not the objects those references point to. The result is confusing, because moving one rectangle's corner would move both, even though growing one would not affect the other. (p. 174)
A deep copy, made with copy.deepcopy, copies the object and everything it refers to, and so on down. After box3 = copy.deepcopy(box), both box3 is box and box3.corner is box.corner are False, so the two rectangles are completely separate. When objects contain other objects, deep copy is usually what you want. (p. 174)
Debugging with objects
If you ask for an attribute that does not exist, such as p.z on a Point that only has x and y, Python raises an AttributeError. Several tools help you check what you have: (pp. 174–175)
type(p) tells you the class of an object.
isinstance(p, Point) returns True if p is an instance of Point.
hasattr(p, 'z') returns True or False depending on whether the attribute exists. The second argument must be a string holding the attribute's name.
- A
try statement can attempt x = p.x and, in an except AttributeError: clause, set a default such as x = 0. This lets one function cope with objects that have different attributes.
(p. 175)
Check yourself
- What happens when you write
class Point: and then call Point()? Name the two things that are created.
- Given
box.corner.x, explain step by step what Python does to evaluate it.
- A function receives a Rectangle named
rect and runs rect.height += 10. Does the caller's rectangle change? Why?
- After
p2 = copy.copy(p1) for two Points, what do p1 is p2 and p1 == p2 return, and why?
- How does a shallow copy of a Rectangle differ from a deep copy, and which would you use before moving one rectangle without affecting the other?