0% found this document useful (0 votes)
101 views9 pages

Object: "The Architects Themselves Came in To Explain The Advantages of Both Designs"

The document describes the design of two Python classes: 1) A Point class that represents a 2D point with x and y coordinates. It includes methods to access and modify the coordinates while hiding the internal representation. 2) A Ball class to model billiard balls on a table. It tracks position, speed, and direction, and includes methods to access and update the ball's state over time. Both classes balance encapsulation and a well-defined interface to separate implementation from use.
Copyright
© Attribution Non-Commercial (BY-NC)
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOC, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
101 views9 pages

Object: "The Architects Themselves Came in To Explain The Advantages of Both Designs"

The document describes the design of two Python classes: 1) A Point class that represents a 2D point with x and y coordinates. It includes methods to access and modify the coordinates while hiding the internal representation. 2) A Ball class to model billiard balls on a table. It tracks position, speed, and direction, and includes methods to access and update the ball's state over time. Both classes balance encapsulation and a well-defined interface to separate implementation from use.
Copyright
© Attribution Non-Commercial (BY-NC)
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOC, PDF, TXT or read online on Scribd
You are on page 1/ 9

Class Design "the architects themselves came in to explain the advantages of both designs" In the previous section we used

existing classes. In this section we design our own classes. We start with the design of a simple ADT for a 2D point (an x and y coordinate). A 2D Point Class The simplest class definition is as follows. class Point(object): pass For the moment, the syntax for a class definition is class classname(object): body where classname is the name of the class and body is the collection of definitions. In this example we use pass for the body - it does nothing (a 'noop'). A class definition requires something to be put in the body and so we use pass because we have nothing to say. The entire body of the class definition needs to be indented. As with function definitions, the body of the class definition ends when we return to the same indentation as the class keyword. We will discuss inheritance in the next section, but for now we simply note that a Point is an object (i.e. inherits from object) and that is why 'object' appears in brackets. Later we will see we can put the name of another class here to signify a different sort of inheritance. When we load this definition into the interpreter we can take an instance of the class as follows. >>> p = Point() >>> p <class __main__.Point object at 0x9947e8c> >>> At the moment p is an instance of the Point class but contains no data. Because Python is very dynamic we can add data 'onthe-fly' as follows. >>> p.x = 10 >>> p.y = 20 >>> p.x 10 >>> p.y 20 >>> Now p contains the instance variables x and y. We definitely don't consider this to be an ADT because we are exposing the data to the 'outside world'. At best this sort of class definition is simply a data structure. We need to be more disciplined when defining ADTs. Ideally we would like to hide the instance variables. In other OO languages this is done by using private instance variables. Python does not have private variables - we instead use a convention to 'hide' variables. The convention is to preceed the name of the variable with a single underscore. The following class definition uses this convention. This class, as with most classes, contain a combination of instance variables for storing data and methods for accessing and modifying data. Together the instance variables and methods are called class attributes. import math class Point(object): """A 2D point ADT. Constructor: Point(float, float) """ def __init__(self, x, y): self._x = x self._y = y def x(self): """The x coord.""" return self._x def y(self): """The y coord.""" return self._y def move(self, dx, dy): """ Move the point by (dx,dy).

move(float, float) """ self._x += dx self._y += dy def __str__(self): """ The 'informal' string representation of the point.""" return '(' + str(self._x) + ', ' + str(self._y) + ')' def __repr__(self): """ The 'official' string representation of the point.""" return 'Point'+self.__str__() def __add__(self, other): """ The vector addition of the points considered as vectors.""" return Point(self.x() + other.x(), self.y() + other.y()) def __eq__(self, other): """ Returns True iff self and other have the same x and y coords.""" return self.x() == other.x() and self.y() == other.y() We now look at this class in detail. Firstly note the indentation - all the method definitions are indented. Secondly, note that each of the methods have, as its first argument, the variable self. This variable is used as a reference to the object itself and could have any name, but the Python convention is to use self for its name. Typically, in methods of other OO languages, this reference to the object is an implicit argument and can be referred to in the body of methods with some keyword such as this. Methods whose names start and end with two underscores are methods of all Python object. Their definitions can be overridden in our class. The first one of these is __init__. This method is called when an instance of the class is created. As well as the self argument we pass in initial values for x and y. In the body of this method we assign the objects instance variables _x and _y to these values. Recall that because self is a reference to the object then self._x refers to the instance variable. The next two methods are accessors - they provide an interface to the instance variables. The method move is a mutator - it modifies the data stored in the object. The method __str__ overrides the default object method and produces a string representation of the object. This string is produced when using the function str on the object. The method __repr__ is like the above method except that it produces an 'official' string representation. The Python documentation recommends, where possible, using a string that the Python intereter can read back in. In our class, we made this the same as the constructor for the class - this means that if the Python interpreter evaluates this string it will construct a copy of the object. The interpreter uses this string when writing out the object as in the examples below. The second last method, __add__, gives a definition for addition. We have already seen examples of the +operator being overloaded - it is used for adding numbers and appending strings. Here it is used to add two points (as vectors). The final methods define what it means for two points to be equal. Without this definition, the default equality test would fail when comparing two copies of the same point (because they are not the same point). Here we take the view that two points are equal if and only if they have the same x and y coordinates. Here are some examples of the use of this class. >>> p1 = Point(2,3) >>> p1 Point(2, 3) >>> str(p1) '(2, 3)' >>> p2 = Point(1,5) >>> p1+p2 Point(3, 8) >>> p1.x() 2 >>> p2.y() 5 >>> p1._y 3 >>> type(p1) <type 'instance'> >>>

Note that in the second last example, we can refer to the instance variables directly but the naming convention reminds us that we are being 'naughty'. We now consider extending this ADT by providing accessor methods for getting the polor coordinates of the point. The polar coordinates specify a point by angle and distance rather than x and y coordinates. The new methods are given below. def r(self): return math.sqrt(self._x**2 + self._y**2) def theta(self): if self._x == 0: a = math.pi/2 else: a = math.atan(abs(self._y/self._x)) if self._x < 0: if self._y < 0: return math.pi + a else: return math.pi - a else: if self._y < 0: return 2*math.pi - a else: return a Let's now consider what we have just done. We have defined a class - in this case we are thinking of this class as an ADT. We have a constructor (to create an instance of the class) and a defined interface to the ADT. Imagine we now write some graphics programs that use this ADT. Later we come back and reconsider the implementation of the ADT and decide to use the polar coordinates rather than the x,y coordinates for the internal representation of the data. We make sure that the constructor and the interface behaves in the same way as before (have the same semantics). Now we go back to our graphics programs we wrote. Do we need to change anything? No! Because we did not change the interface, we do not need to change anything about our graphics programs. This is the key point about ADTs - we have completely separated the implementation from the use of the ADT via a well defined interface. We respect the abstraction barrier! Note that if our graphics program directly accessed the x and y coordinates instead of using the interface then we would be in trouble if we changed over to polar coordinates - we would have to rethink all our uses of Point objects! A Ball Class We now design another class that is similar to the one above in some ways, but we would probably not think of it as an ADT because it has more 'behaviour'. What we want to do here is to model a ball and its motion on something like a billiard table without pockets. To begin with we look at what assumptions we will make. Firstly, to simplify the physics, we assume no friction and no loss of energy when the ball bounces off the table edge. Secondly we assume a given radius of the ball and table dimensions. Lastly we assume a given positive time step and that time step is small enough to reasonably approximate the ball movement. For a first pass at this problem we will assume the following global variables: top, left, bottom, right and timestep to describe the edges of the table and the time step. We also assume all the balls have the same radius. Next we need to determine what is necessary to describe the state of the ball. We need to know its position, speed and direction. Finally we need to know what methods we will need - in other words what the ball interface will look like. We will need accessors to get the position, speed and direction and a mutator that modifies the balls state based on the given time step. We also add a test to determine if this ball is touching another ball. To do this we require some simple trigonomentry and physics so we will import the math module. Here is the class definition from ball.py. class Ball(object): """A class for managing the movement of a ball on a billiard table. Constructor: Ball(float, float, float, float) Class invariant: 0 <= _direction <= 2*pi and left + r <= _x <= right - r and top + r <= _y <= bottom - r and 0 <= _speed*timestep < r """ r = 0.1

def __init__(self, x, y, speed, direction): """Initialise a ball object with position speed and direction. Assumes the supplied values satisfy the class invariant. """ self._x = x self._y = y self._speed = speed self._direction = direction def getCentreX(self): """Get the x coord of the ball centre.""" return self._x def getCentreY(self): """Get the y coord of the ball centre.""" return self._y def getSpeed(self): """Get the speed of the ball.""" return self._speed def getDir(self): """Get the direction in which the ball is travelling.""" return self._direction def _reflectVertically(self): """Change the direction as the ball bounces off a vertical edge.""" self._direction = math.pi - self._direction if self._direction < 0: self._direction += 2*math.pi def _reflectHorizontally(self): """Change the direction as the ball bounces off a horizontal edge.""" self._direction = 2*math.pi - self._direction def step(self): """Advance time by timestep - moving the ball.""" self._x += timestep*self._speed*math.cos(self._direction) self._y += timestep*self._speed*math.sin(self._direction) if self._x < left + Ball.r: self._x = 2*(left + Ball.r) - self._x self._reflectVertically() elif self._x > right - Ball.r: self._x = 2*(right - Ball.r) - self._x self._reflectVertically() if self._y < top + Ball.r: self._y = 2*(top + Ball.r) - self._y self._reflectHorizontally() elif self._y > bottom - Ball.r: self._y = 2*(bottom - Ball.r) - self._y self._reflectHorizontally() def touching(self, other): """ Return True iff this ball is touching other. touching(Ball) -> bool

""" return (self.getCentreX() - other.getCentreX())**2 \ + (self.getCentreY() - other.getCentreY())**2 \ <= (2*Ball.r)**2 def __repr__(self): """ The ball's string representation.""" return 'Ball(%.2f, %.2f, %.2f, %.2f)' \ % (self._x, self._y, self._speed, self._direction) Let's look at this class in detail. Firstly, in the comments for the class itself we have included a class invariant. This is similar to the loop invariant we briefly discussed in an earlier section. The idea is that the class invariant is a property that should be true over the lifetime of each object of the class. In other words it should be true when the object is first constructed and after each method is called. This is typically a formula that interrelates the instance variables. (To shorten the formula we have elided the self. from the instance variables.) Even in a simple class like this, the class invariant can be a big help when it comes to writing methods. In particular, for the step method we can assume the class invariant is true when the method is called and given that, we need to guarantee the class invariant is true at the end of the method. The next part of the class is the assignment to the radius r. This is a class variable. Class variables are variables that are common to all instances of the class - all instances of the class share this variable and if any instance changes this variable all instances 'will see the change'. Since all the balls have the same r we make it a class variable. The constructor (the __init__ method) initialises the instance variables but note we assume that the supplied arguments to the constructor satisfy the class invariant. We will return to this point later. There are four accessor methods which return the values of the instance variables. The methods _reflectVertically and _reflectHorizontally are methods used as 'helpers' for the step method. Because we do not intend them to be called from outside we make them private (by prepending the names with an underscore). The step method is the mutator that defines the behaviour of a ball object and the touching method is a test that determines if this ball is touching another ball. We also define the __repr__ method - this is helpful for testing the class as can be seen below. >>> b = Ball(0.51, 0.51, 1.0, math.pi/4) >>> for i in range(50): b.step() print b Ball(0.58, 0.58, 1.00, 0.79) Ball(0.65, 0.65, 1.00, 0.79) Ball(0.72, 0.72, 1.00, 0.79) Ball(0.79, 0.79, 1.00, 0.79) Ball(0.86, 0.86, 1.00, 0.79) Ball(0.93, 0.93, 1.00, 0.79) Ball(1.00, 1.00, 1.00, 0.79) Ball(1.08, 1.08, 1.00, 0.79) Ball(1.15, 1.15, 1.00, 0.79) Ball(1.22, 1.22, 1.00, 0.79) Ball(1.29, 1.29, 1.00, 0.79) Ball(1.36, 1.36, 1.00, 0.79) Ball(1.43, 1.43, 1.00, 0.79) Ball(1.50, 1.50, 1.00, 0.79) Ball(1.57, 1.57, 1.00, 0.79) Ball(1.64, 1.64, 1.00, 0.79) Ball(1.71, 1.71, 1.00, 0.79) Ball(1.78, 1.78, 1.00, 0.79) Ball(1.85, 1.85, 1.00, 0.79) Ball(1.92, 1.88, 1.00, 5.50) Ball(1.99, 1.81, 1.00, 5.50) Ball(2.07, 1.73, 1.00, 5.50) Ball(2.14, 1.66, 1.00, 5.50) Ball(2.21, 1.59, 1.00, 5.50) Ball(2.28, 1.52, 1.00, 5.50) Ball(2.35, 1.45, 1.00, 5.50) Ball(2.42, 1.38, 1.00, 5.50) Ball(2.49, 1.31, 1.00, 5.50) Ball(2.56, 1.24, 1.00, 5.50)

Ball(2.63, 1.17, 1.00, 5.50) Ball(2.70, 1.10, 1.00, 5.50) Ball(2.77, 1.03, 1.00, 5.50) Ball(2.84, 0.96, 1.00, 5.50) Ball(2.91, 0.89, 1.00, 5.50) Ball(2.98, 0.82, 1.00, 5.50) Ball(3.06, 0.74, 1.00, 5.50) Ball(3.13, 0.67, 1.00, 5.50) Ball(3.20, 0.60, 1.00, 5.50) Ball(3.27, 0.53, 1.00, 5.50) Ball(3.34, 0.46, 1.00, 5.50) Ball(3.41, 0.39, 1.00, 5.50) Ball(3.48, 0.32, 1.00, 5.50) Ball(3.55, 0.25, 1.00, 5.50) Ball(3.62, 0.18, 1.00, 5.50) Ball(3.69, 0.11, 1.00, 5.50) Ball(3.76, 0.16, 1.00, 0.79) Ball(3.83, 0.23, 1.00, 0.79) Ball(3.90, 0.30, 1.00, 2.36) Ball(3.83, 0.37, 1.00, 2.36) Ball(3.75, 0.45, 1.00, 2.36) The Ball Class Invariant We now look in more depth at the class invariant and prove that the step method maintains the class invariant. In the example above the class invariant says that the direction should be be bounded between 0 and 2*pi, that the ball should be on the table, that the ball should move forward and that the movement should not be 'too jittery'. First note that the class invariant tells us that right - left >= 2*r and bottom-top >= 2*r. In other words the ball can fit on the table - this is really a constraint that our global variables must satisfy in order that ANY ball objects (that satisfy the class invariant) can be constructed. Now let's see if we can prove that the step method maintains the class invariant - i.e. if the invariant is true before calling the step method then it will be true after the call. In other words, if the ball is on the table before the call then it will be on the table after the call. Note that proving this does not prove we have implemented the physics correctly. First we prove that the direction property is maintained. That is, if 0 <= _direction <= 2*pi is true before the method call then we need to prove that it is true after the call. The only place this instance variable is changed is in the helper methods and so it is enough to prove this part of the invariant is maintained by each helper. We start with the easier one - _reflectHorizontally. Let d0 and d1 be the initial and final values of self._direction. We want to show that if 0 <= d0 <= 2*pi then 0 <= d1 <= 2*pi where d1 == 2*pi - d0 (We use => for 'implies' below) 0 <= d0 <= 2*pi => 0 >= -d0 >= -2*pi (multiplying by -1) => 2*pi >= 2*pi - d0 >= 0 (adding 2*pi) QED We now prove the property is true for _reflectVertically. Here we let d0 be the initial value of self._direction, d1 be the value after the first assignment and d2 be the final value. In this case there is an if statement involved and so we have to consider two cases: d1 >= 0 and d1 < 0. The first case. We want to show that if 0 <= d0 <= 2*pi and d1 >= 0 then 0 <= d2 <= 2*pi In this case the body of the if statement is not executed and so d2 == d1 == pi - d0

0 <= d0 <= 2*pi => 0 >= -d0 >= -2*pi => pi >= pi - d0 >= -pi => pi >= pi - d0 >= 0 QED The second case.

(multiplying by -1) (adding pi) (d1 >= 0 i.e. pi - d0 >= 0)

We want to show that if 0 <= d0 <= 2*pi and d1 < 0 then 0 <= d2 <= 2*pi In this case the body of the if statement is executed and so d2 == 2*pi + d1 and d1 == pi - d0 and so d2 == 3*pi - d0 d1 < 0 => pi - d0 < 0 => 3*pi -d0 < 2*pi (adding 2*pi) and d0 <= 2*pi => -d0 >= -2*pi (multiplying by -1) => 3*pi - d0 >= pi (adding 3*pi) and so pi <= 3*pi - d0 <= 2*pi QED Now we look at the hardest part of the proof - that the ball stays on the table. The method has four if statements and below we will only consider the case when the first test is satisfied - the other cases follow in a similar manner. We let x0 be the initial value of self._x, x1 be the value after the first assignment and x2 be the final value. We also let s be self._speed, d be self._direction and r be Ball.r. So we can assume left+r <= x0 <= right-r and 0 <= s*timestep < r and x1 < left + r (the test in the first if statement is true) and we want to show left+r <= x2 <= right-r We have x1 == x0 + s*timestep*cos(d) and x2 == 2*(left+r) - x1 Now x1 < left + r => -x1 >= -left - r (multiplying by -1) => 2*(left+r) - x1 >= left+r (adding 2*(left+r)) => x2 >= left+r (one half of the required inequality) We now need to show 2*(left+r) - x0 - s*timestep*cos(d) <= right-r left+r <= x0 =>

left+r - x0 <= 0 => 2*(left+r) - x0 + r <= left + 2*r (adding left + 2*r) => 2*(left+r) - x0 + s*timestamp <= left + 2*r (r >= s*timestamp) => 2*(left+r) - x0 - s*timestamp*cos(d) <= left + 2*r (1 >= -cos(d) and s*timestamp >= 0) => x2 <= left + 2*r So provided left+2*r <= right - r (i.e. right - left >= 3*r) then the required property is true. This means that, if we insist that the table is at least one and a half balls long and wide then the step method will maintain the class invariant. The point of this exercise is to show that it is possible to prove useful properties about the class and therefore of any object of the class. Here we showed that, provided the global variables satisfy some reasonable constraints, any ball from the Ball class (that initially satisfies the class invariant) will stay on the table. Creating Instances of the Ball Class We now return to the Ball class itself. Where things get really interesting is when we create several instances of the class. Below is an example to show the power of OO programming - once we have defined the class we can create as many instances as we want! >>> balls = [Ball(1.0, 1.0, 1.0, 0), Ball(1.2, 1.2, 1.0, 1.0), Ball(1.4, 1.4, 1.0, 2.0)] >>> balls [Ball(1.00, 1.00, 1.00, 0.00), Ball(1.20, 1.20, 1.00, 1.00), Ball(1.40, 1.40, 1.00, 2.00)] >>> for b in balls: b.step() >>> balls [Ball(1.10, 1.00, 1.00, 0.00), Ball(1.25, 1.28, 1.00, 1.00), Ball(1.36, 1.49, 1.00, 2.00)] >>> def some_touch(balls): for b1 in balls: for b2 in balls: if b1 != b2 and b1.touching(b2): return True return False >>> while not some_touch(balls): for b in balls: b.step() >>> balls [Ball(1.20, 1.00, 1.00, 3.14), Ball(3.57, 1.49, 1.00, 4.14), Ball(1.13, 0.91, 1.00, 5.14)] The next step would, of course, be to program the interaction between the balls. We could do this either by writing a collection of functions to manage the interaction of the balls and the motion or we could define a Table class (for example) which would contain a colection of balls and a step method for the table which would involve stepping each ball and defining how the balls bounce off each other. Is defining a Table class worthwhile? It could be argued either way. If there was only ever going to be one table then it could be argued that creating a class would be overkill. On the other hand, collecting all the information and behaviour of the table into one place (a class) might be a good idea. Summary In this section we have introduced the idea of class design. Once we have defined a class we can take any number of instances of the class. This is a simple, but powerful form of reuse. Things to consider when designing classes: What assumptions am I making? What data do I need to store? o Create instance variables to capture properties associated with individual objects. o Create class variables to capture shared properties Are the values of variables interrelated or constrained? - Add a class invariant to the class comments.

What should the interface look like? - What are the 'public' methods? Name the class after the objects it produces. Name variables after their roles and make instance variables private. What information does the constructor need to create an object? Add parameters to the __init__ method and give them meaningful names. Name each method to suggest its role. Comment each method before writing any code! Don't 'over complicate' methods - methods should, where possible, perform just one task. What helper methods do I need? - make them private. For testing purposes write the __repr__ method.

You might also like