Chapter 15 Classes and objectsAt this point you know how to use functions to organize code and built-in types to organize data. The next step is to learn “object-oriented programming”, which uses programmer-defined types to organize both code and data. Object-oriented programming is a big topic; it will take a few chapters to get there. Code examples from this chapter are available from https://thinkpython.com/code/Point1.py; solutions to the exercises are available from https://thinkpython.com/code/Point1_soln.py. 15.1 Programmer-defined typesWe have used many of Python’s built-in types; now we are going to define a new type. As an example, we will create a type called Point that represents a point in two-dimensional space. 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 more complicated than the other options, but it has advantages that will be apparent soon. A programmer-defined type is also called a class. A class definition looks like this: class Point: """Represents a point in 2-D space.""" The header indicates that the new class is called Point. The body is a docstring that explains what the class is for. You can define variables and methods inside a class definition, but we will get back to that later. Defining a class named Point creates a class object. >>> Point <class '__main__.Point'>
Because Point is defined at the top level, its “full
name” is 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() >>> blank <__main__.Point object at 0xb7e9d3ac> The return value is a reference to a Point object, which we assign to blank. Creating a new object is called instantiation, and the object is an instance of the class. When you print an instance, Python tells you what class it belongs to and where it is stored in memory (the prefix 0x means that the following number is in hexadecimal). Every object is an instance of some class, so “object” and “instance” are interchangeable. But in this chapter I use “instance” to indicate that I am talking about a programmer-defined type. 15.2 AttributesYou 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. Figure 15.1 is a state diagram that shows the result of these assignments. A state diagram that shows an object and its attributes is called an object diagram. 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: >>> blank.y 4.0 >>> x = blank.x >>> x 3.0 The expression blank.x means, “Go to the object blank refers to and get the value of x.” In the example, 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: >>> '(%g, %g)' % (blank.x, blank.y) '(3.0, 4.0)' >>> distance = math.sqrt(blank.x**2 + blank.y**2) >>> distance 5.0 You can pass an instance as an argument in the usual way. For example: def print_point(p): print('(%g, %g)' % (p.x, p.y))
>>> print_point(blank) (3.0, 4.0) Inside the function, p is an alias for blank, so if the function modifies p, blank changes. As an exercise, write a function called 15.3 RectanglesSometimes 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:
At this point it is hard to say whether either is better than the other, so we’ll implement the first one, just as an example. Here is the class definition: class Rectangle: """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 valuesFunctions can return instances. For example, def find_center(rect): p = Point() p.x = rect.corner.x + rect.width/2 p.y = rect.corner.y + rect.height/2 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, 100) 15.5 Objects are mutableYou 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.height + 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: >>> box.width, box.height (150.0, 300.0) >>> grow_rectangle(box, 50, 100) >>> box.width, box.height (200.0, 400.0) Inside the function, rect is an alias for box, so when the function modifies rect, box changes. As an exercise, write a function named 15.6 CopyingAliasing 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, 4) >>> print_point(p2) (3, 4) >>> 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. That’s because for programmer-defined types, Python doesn’t know what should be considered equivalent. At least, not yet. If you use copy.copy to duplicate a Rectangle, you will find that it copies the Rectangle object but not the embedded Point. >>> 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,
invoking Fortunately, the copy module provides 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. As an exercise, write a version of 15.7 DebuggingWhen you start working with objects, you are likely to encounter some new exceptions. If you try to access an attribute that doesn’t exist, you get an AttributeError: >>> p = Point() >>> p.x = 3 >>> p.y = 4 >>> p.z AttributeError: Point instance has no attribute 'z' If you are not sure what type an object is, you can ask: >>> type(p) <class '__main__.Point'> You can also use isinstance to check whether an object is an instance of a class: >>> isinstance(p, Point) True If you are not sure whether an object has a particular attribute, you can use the built-in function hasattr: >>> 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. You can also use a try statement to see if the object has the attributes you need: try: x = p.x except AttributeError: x = 0 This approach can make it easier to write functions that work with different types; more on that topic is coming up in Section 17.9. 15.8 Glossary
15.9 ExercisesExercise 1 Write a definition for a class named Circle with attributes center and radius, where center is a Point object and radius is a number. Instantiate a Circle object that represents a circle with its center at (150, 100) and radius 75. Write a function named Write a function named Write a function named Solution: https://thinkpython.com/code/Circle.py. Exercise 2
Write a function called Write a function called Solution: https://thinkpython.com/code/draw.py. |
ContributeIf you would like to make a contribution to support my books, you can use the button below and pay with PayPal. Thank you!
Are you using one of our books in a class?We'd like to know about it. Please consider filling out this short survey.
|