Chapter 15 Classes and objects
15.1 User-defined types
In mathematical notation, points are often written in parentheses with a comma separating the coordinates. For example, (0,0) represents the origin, and (x,y) represents the point x units to the right and y units up from the origin.
There are several ways we might represent points in Python:
Creating a new type is (a little) more complicated than the other options, but it has advantages that will be apparent soon.
class Point(object): """Represents a point in 2-D space."""
Defining a class named Point creates a class object.
>>> print Point <class '__main__.Point'>
The class object is like a factory for creating objects. To create a Point, you call Point as if it were a function.
>>> blank = Point() >>> print blank <__main__.Point instance at 0xb7e9d3ac>
You can assign values to an instance using dot notation:
>>> blank.x = 3.0 >>> blank.y = 4.0
This syntax is similar to the syntax for selecting a variable from a module, such as math.pi or string.whitespace. In this case, though, we are assigning values to named elements of an object. These elements are called attributes.
As a noun, “AT-trib-ute” is pronounced with emphasis on the first syllable, as opposed to “a-TRIB-ute,” which is a verb.
The following diagram shows the result of these assignments. A state diagram that shows an object and its attributes is called an object diagram; see Figure 15.1.
The variable blank refers to a Point object, which contains two attributes. Each attribute refers to a floating-point number.
You can read the value of an attribute using the same syntax:
>>> print blank.y 4.0 >>> x = blank.x >>> print x 3.0
The expression blank.x means, “Go to the object blank refers to and get the value of x.” In this case, we assign that value to a variable named x. There is no conflict between the variable x and the attribute x.
You can use dot notation as part of any expression. For example:
>>> print '(%g, %g)' % (blank.x, blank.y) (3.0, 4.0) >>> distance = math.sqrt(blank.x**2 + blank.y**2) >>> print distance 5.0
def print_point(p): print '(%g, %g)' % (p.x, p.y)
>>> print_point(blank) (3.0, 4.0)
Write a function called
Sometimes it is obvious what the attributes of an object should be, but other times you have to make decisions. For example, imagine you are designing a class to represent rectangles. What attributes would you use to specify the location and size of a rectangle? You can ignore angle; to keep things simple, assume that the rectangle is either vertical or horizontal.
There are at least two possibilities:
Here is the class definition:
class Rectangle(object): """Represents a rectangle. attributes: width, height, corner. """
The docstring lists the attributes: width and height are numbers; corner is a Point object that specifies the lower-left corner.
To represent a rectangle, you have to instantiate a Rectangle object and assign values to the 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 expression box.corner.x means, “Go to the object box refers to and select the attribute named corner; then go to that object and select the attribute named x.”
Figure 15.2 shows the state of this object. An object that is an attribute of another object is embedded.
15.4 Instances as return values
Functions can return instances. For example,
def find_center(rect): p = Point() p.x = rect.corner.x + rect.width/2.0 p.y = rect.corner.y + rect.height/2.0 return p
Here is an example that passes box as an argument and assigns the resulting Point to center:
>>> center = find_center(box) >>> print_point(center) (50.0, 100.0)
15.5 Objects are mutable
You can change the state of an object by making an assignment to one of its attributes. For example, to change the size of a rectangle without changing its position, you can modify the values of width and height:
box.width = box.width + 50 box.height = box.width + 100
You can also write functions that modify objects. For example,
def grow_rectangle(rect, dwidth, dheight): rect.width += dwidth rect.height += dheight
Here is an example that demonstrates the effect:
>>> print box.width 100.0 >>> print box.height 200.0 >>> grow_rectangle(box, 50, 100) >>> print box.width 150.0 >>> print box.height 300.0
Inside the function, rect is an alias for box, so if the function modifies rect, box changes.
Write a function named
Aliasing can make a program difficult to read because changes in one place might have unexpected effects in another place. It is hard to keep track of all the variables that might refer to a given object.
Copying an object is often an alternative to aliasing. The copy module contains a function called copy that can duplicate any object:
>>> p1 = Point() >>> p1.x = 3.0 >>> p1.y = 4.0 >>> import copy >>> p2 = copy.copy(p1)
p1 and p2 contain the same data, but they are not the same Point.
>>> print_point(p1) (3.0, 4.0) >>> print_point(p2) (3.0, 4.0) >>> p1 is p2 False >>> p1 == p2 False
The is operator indicates that p1 and p2 are not the same object, which is what we expected. But you might have expected == to yield True because these points contain the same data. In that case, you will be disappointed to learn that for instances, the default behavior of the == operator is the same as the is operator; it checks object identity, not object equivalence. This behavior can be changed—we’ll see how later.
>>> box2 = copy.copy(box) >>> box2 is box False >>> box2.corner is box.corner True
Figure 15.3 shows what the object diagram looks like. This operation is called a shallow copy because it copies the object and any references it contains, but not the embedded objects.
For most applications, this is not what you want. In this example,
Fortunately, the copy module contains a method named deepcopy that copies not only the object but also the objects it refers to, and the objects they refer to, and so on. You will not be surprised to learn that this operation is called a deep copy.
>>> box3 = copy.deepcopy(box) >>> box3 is box False >>> box3.corner is box.corner False
box3 and box are completely separate objects.
Write a version of
>>> p = Point() >>> print p.z AttributeError: Point instance has no attribute 'z'
>>> type(p) <type '__main__.Point'>
>>> hasattr(p, 'x') True >>> hasattr(p, 'z') False
The first argument can be any object; the second argument is a string that contains the name of the attribute.
Swampy (see Chapter 4) provides a module named World, which defines a user-defined type also called World. You can import it like this:
from swampy.World import World
Or, depending on how you installed Swampy, like this:
from World import World
The following code creates a World object and calls the mainloop method, which waits for the user.
world = World() world.mainloop()
A window should appear with a title bar and an empty square.
We will use this window to draw Points,
Rectangles and other shapes.
Add the following lines before calling
canvas = world.ca(width=500, height=500, background='white') bbox = [[-150,-100], [150, 100]] canvas.rectangle(bbox, outline='black', width=2, fill='green4')
You should see a green rectangle with a black outline. The first line creates a Canvas, which appears in the window as a white square. The Canvas object provides methods like rectangle for drawing various shapes.
bbox is a list of lists that represents the “bounding box” of the rectangle. The first pair of coordinates is the lower-left corner of the rectangle; the second pair is the upper-right corner.
You can draw a circle like this:
canvas.circle([-25,0], 70, outline=None, fill='red')
The first parameter is the coordinate pair for the center of the circle; the second parameter is the radius.
If you add this line to the program, the result should resemble the national flag of Bangladesh (see http://en.wikipedia.org/wiki/Gallery_of_sovereign-state_flags).
I have written a small program that lists the available colors; you can download it from http://thinkpython.com/code/color_list.py.
Are you using one of our books in a class?We'd like to know about it. Please consider filling out this short survey.