Let’s say that we want a class to represent a rectangle which is located somewhere in the XY plane. The question is, what information do we have to provide in order to specify such a rectangle? To keep things simple, assume that the rectangle is oriented either vertically or horizontally, never at an angle.
There are a few possibilities: we could specify the center of the rectangle (two coordinates) and its size (width and height); or we could specify one of the corners and the size; or we could specify two opposing corners. A conventional choice is to specify the upper-left corner of the rectangle and the size.
Again, we’ll define a new class, and provide it with an initializer and a string converter method:
class Rectangle:
""" A class to manufacture rectangle objects """
def __init__(self, posn, w, h):
""" Initialize a rectangle at posn, with width w and height h """
self.corner = posn
self.width = w
self.height = h
def __str__(self):
return "({0}, {1}, {2})".format(self.corner, self.width, self.height)
box = Rectangle(Point(0, 0), 100, 200)
bomb = Rectangle(Point(100, 80), 5, 10) # in my video game
print("box: ", box)
print("bomb: ", bomb)
To specify the upper-left corner, we have embedded an object within an object! We create two new Rectangle objects, and then print them producing:
box: ((0, 0), 100, 200)
bomb: ((100, 80), 5, 10)
The dot operator composes. The expression box.corner.x means, Go to the object that box refers to and select its attribute named corner; then go to that object and select its attribute named x.
The figure shows the state of this object:
We can change the state of an object by making an assignment to one of its attributes. For example, to grow the size of a rectangle without changing its position, we could modify the values of width and height:
box.width += 50
box.height += 100
Of course, we’d probably like to provide a method to encapsulate this inside the class. We will also provide another method to move the position of the rectangle elsewhere:
class Rectangle:
# ...
def grow(self, delta_width, delta_height):
""" Grow (or shrink) this object by the deltas """
self.width += delta_width
self.height += delta_height
def move(self, dx, dy):
""" Move this object by the deltas """
self.corner.x += dx
self.corner.y += dy
Let us try this:
>>> r = Rectangle(Point(10,5), 100, 50)
>>> print(r)
((10, 5), 100, 50)
>>> r.grow(25, -10)
>>> print(r)
((10, 5), 125, 40)
>>> r.move(-10, 10)
print(r)
((0, 15), 125, 40)
The meaning of the word same seems perfectly clear until you give it some thought, and then you realize there is more to it than you expected.
For example, if you say, Chris and I have the same car, you mean that his car and yours are the same make and model, but that they are two different cars. If you say, Chris and I have the same mother, you mean that his mother and yours are the same person.
When you talk about objects, there is a similar ambiguity. For example, if two Points are the same, does that mean they contain the same data (coordinates) or that they are actually the same object?
We’ve already seen the is operator in the chapter on lists, where we talked about aliases: it allows us to find out if two references refer to the same object:
>>> p1 = Point(3, 4)
>>> p2 = Point(3, 4)
>>> p1 is p2
False
Even though p1 and p2 contain the same coordinates, they are not the same object. If we assign p1 to p3, then the two variables are aliases of the same object:
>>> p3 = p1
>>> p1 is p3
True
This type of equality is called shallow equality because it compares only the references, not the contents of the objects.
To compare the contents of the objects — deep equality — we can write a function called same_coordinates:
def same_coordinates(p1, p2):
return (p1.x == p2.x) and (p1.y == p2.y)
Now if we create two different objects that contain the same data, we can use same_point to find out if they represent points with the same coordinates.
>>> p1 = Point(3, 4)
>>> p2 = Point(3, 4)
>>> same_coordinates(p1, p2)
True
Of course, if the two variables refer to the same object, they have both shallow and deep equality.
Beware of ==
“When I use a word,” Humpty Dumpty said, in a rather scornful tone, “it means just what I choose it to mean — neither more nor less.” Alice in Wonderland
Python has a powerful feature that allows a designer of a class to decide what an operation like == or < should mean. (We’ve just shown how we can control how our own objects are converted to strings, so we’ve already made a start!) We’ll cover more detail later. But sometimes the implementors will attach shallow equality semantics, and sometimes deep equality, as shown in this little experiment:
p = Point(4, 2)
s = Point(4, 2)
print("== on Points returns", p == s) # by default, == does a shallow equality test here
a = [2,3]
b = [2,3]
print("== on lists returns", a == b) # by default, == does a deep equality test on lists
This outputs:
== on Points returns False
== on lists returns True
So we conclude that even though the two lists (or tuples, etc.) are distinct objects with different memory addresses, in one case the == operator tests for deep equality, while in the case of points it makes a shallow test.
..index:: copy, copy; deep, copy; shallow
Aliasing can make a program difficult to read because changes made 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:
>>> import copy
>>> p1 = Point(3, 4)
>>> p2 = copy.copy(p1) # ugly! <module_name>.<function_name> are identical!
>>> p1 is p2
False
>>> same_coordinates(p1, p2)
True
Once we import the copy module, we can use the copy function to make a new Point. p1 and p2 are not the same point, but they contain the same data.
To copy a simple object like a Point, which doesn’t contain any embedded objects, copy is sufficient. This is called shallow copying.
For something like a Rectangle, which contains a reference to a Point, copy doesn’t do quite the right thing. It copies the reference to the Point object, so both the old Rectangle and the new one refer to a single Point.
If we create a box, b1, in the usual way and then make a copy, b2, using copy, the resulting state diagram looks like this:
This is almost certainly not what we want. In this case, invoking grow on one of the Rectangles would not affect the other, but invoking move on either would affect both! This behavior is confusing and error-prone. The shallow copy has created an alias to the Point that represents the corner.
Fortunately, the copy module contains a function named deepcopy that copies not only the object but also any embedded objects. You will not be surprised to learn that this operation is called a deep copy.
>>> b2 = copy.deepcopy(b1)
Now b1 and b2 are completely separate objects.
Add a method area to the Rectangle class that returns the area of any instance:
r = Rectangle(Point(0, 0), 10, 5)
test(r.area(), 50)
Write a perimeter method in the Rectangle class so that we can find the perimeter of any rectangle instance:
r = Rectangle(Point(0, 0), 10, 5)
test(r.perimeter(), 30)
Write a transpose method in the Rectangle class that swaps the width and the height of any rectangle instance:
r = Rectangle(Point(100, 50), 10, 5)
test(r.width, 10)
test(r.height, 5)
r.flip()
test(r.width, 5)
test(r.height, 10)
Write a new method in the Rectangle class to test if a Point falls within the rectangle. For this exercise, assume that a rectangle at (0,0) with width 10 and height 5 has open upper bounds on the width and height, i.e. it stretches in the x direction from [0 to 10), where 0 is included but 10 is excluded, and from [0 to 5) in the y direction. So it does not contain the point (10, 2). These tests should pass:
r = Rectangle(Point(0, 0), 10, 5)
test(r.contains(Point(0, 0)), True)
test(r.contains(Point(3, 3)), True)
test(r.contains(Point(3, 7)), False)
test(r.contains(Point(3, 5)), False)
test(r.contains(Point(3, 4.99999)), True)
test(r.contains(Point(-3, -3)), False)
In games, we often put a rectangular “bounding box” around our sprites in the game. We can then do collision detection between, say, bombs and spaceships, by comparing whether their rectangles overlap anywhere.
Write a function to determine whether two rectangles collide. Hint: this might be quite a tough exercise! Think carefully about all the cases before you code.