21. Objects III — Interacting Objects
We completed the
Sphere
class in the previous topic, and it will work for our needsHowever, there are a few things we could do to improve the design and implementation
One idea is to make a
Point3D
class to manage all the point related thingsLocation in three dimensional space
Distance between points
Equality on points
Although there is nothing wrong with the original
Sphere
implementation, let’s consider thatIt will make our
Sphere
class simpler and easier to understandIt allows us to write the methods at a higher-level of abstraction since can deal with
Point3D
objects instead of individual coordinatesIf we wanted to make other three dimensional shapes, they could use something like a
Point3D
class to represent their positionIt allows us to abstract and delegate some functionality to another class
It will help with testing since we can test the
Point3D
class’ functionality and theSphere
s independently
21.1. Point3D Class
The purpose of the
Point3D
class is to replace all the coordinate details from theSphere
classThe class will have three attributes that represent a position within some three dimensional space with a Cartesian coordinate system
x
y
z
The class will also have a few methods, including some that will replace functionality within the
Sphere
classA method for measuring the distance from another
Point3D
objectA method for checking if two
Point3D
objects are equalA method for generating a nice, human readable string representation of the object
21.1.1. Constructor and Attributes
Below is the start of the
Point3D
class, including the constructor and assignment of the attributes
1import math
2
3
4class Point3D:
5 """
6 A class for representing points in a three dimensional (3D) space.
7 """
8
9 def __init__(self, x: float, y: float, z: float):
10 self.x = x
11 self.y = y
12 self.z = z
That’s it — that’s all we need to get started with the
Point3D
class
21.1.2. Methods
The methods we want are
A way to measure the distance from another point —
distance_from_point
An
__eq__
method for equalityA
__repr__
for generating a string representation of the object
1import math
2
3
4class Point3D:
5
6 # init and/or other methods not shown for brevity
7
8 def distance_from_point(self, other: "Point3D") -> float:
9 """
10 Calculate the Euclidean distance from this Point3D (self) and the Point3D passed as a parameter.
11
12 :param other: A Point3D to find the the Euclidean distance to from the self Point3D
13 :type other: Point3D
14 :return: The Euclidean distance between the self Point3D and the parameter Point3D other
15 :rtype: float
16 """
17 return math.sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2 + (self.z - other.z) ** 2)
The above method is following the same pattern as before
Like within the
Sphere
class, we have a method making use of another class method
1import math
2
3
4class Point3D:
5
6 # init and/or other methods not shown for brevity
7
8 def __eq__(self, other) -> bool:
9 """
10 Check if the self Point3D is equal to the Point3D passed as a parameter. Points3D are considered equal if they
11 have the same x, y, and z values.
12
13 This is a "magic method" that can be used with `==`.
14
15 :param other: A Point3D to compare to the self point3D
16 :type other: Point3D
17 :return: A boolean indicating if the two Point3Ds are equivalent.
18 :rtype: boolean
19 """
20 if isinstance(other, Point3D):
21 return self.x == other.x and self.y == other.y and self.z == other.z
22 return False
23
24
25 def __repr__(self) -> str:
26 """
27 Generate and return a string representation of the Point3D object.
28
29 This os a "magic method" that can be used with `str(some_point3d)` or for printing.
30
31 :return: A string representation of the Point3D
32 :rtype: string
33 """
34 return f"Point3D(x={self.x}, y={self.y}, z={self.z})"
In the above
__eq__
method, equality forPoint3D
objects will be if all their attributes matchThe
__repr__
will follow the same pattern as theSphere
— class name with the relevant attributes
21.1.3. Testing
Below is a series of
assert
tests verifying thePoint3D
class’ correctnessLike before, these tests work, but we area pushing the limits of our simple
assert
tests
1point_origin = Point3D(0, 0, 0)
2assert 0 == point_origin.x
3assert 0 == point_origin.y
4assert 0 == point_origin.z
5assert 0 == point_origin.distance_from_point(Point3D(0, 0, 0))
6assert 0.001 > abs(point_origin.distance_from_point(Point3D(1, 1, 1)) - 1.732051)
7assert 0.001 > abs(point_origin.distance_from_point(Point3D(-1, -1, -1)) - 1.732051)
8assert "Point3D(x=0, y=0, z=0)" == str(point_origin)
9
10point = Point3D(-2, 7, 4)
11assert -2 == point.x
12assert 7 == point.y
13assert 4 == point.z
14assert 0.001 > abs(point.distance_from_point(Point3D(0, 0, 0)) - 8.306624)
15assert 0.001 > abs(point.distance_from_point(Point3D(6, 3, 0)) - 9.797959)
16assert "Point3D(x=-2, y=7, z=4)" == str(point)
17
18assert point != point_origin
19assert point_origin == Point3D(0, 0, 0)
21.2. Sphere Class
With our
Point3D
class, we can now make use of it in theSphere
class to offload the relevant workKeeping track of the location of the centre of the
Sphere
within the three dimensional spaceMeasuring distances between points
Checking equality between centre points in the space
21.2.1. Constructor and Attributes
1import math
2
3
4class Sphere:
5 """
6 Class for managing Spheres within a 3D space. This includes tracking it's centre point and radius. Additionally, it
7 allows for some basic geometry calculations, distance measurements between Spheres, and checking if two Spheres
8 overlap.
9 """
10
11 def __init__(self, centre_point: Point3D, radius: float):
12 self.centre_point = centre_point
13 self.radius = radius
From looking at the above code, we see that we have removed the
x
,y
, andz
coordinates as explicit attributes for theSphere
Instead, we make use of a
Point3D
objectAnd, as we know, those coordinate attributes exist within the
Point3D
class, which are entirely accessible from within theSphere
class
21.2.2. Methods
The below methods for calculating the
diameter
,surface_area
, andvolume
do not change since they only make use of theSphere
object’sradius
attribute
1import math
2
3
4class Sphere:
5
6 # init and/or other methods not shown for brevity
7
8 def diameter(self) -> float:
9 return 2 * self.radius
10
11 def surface_area(self) -> float:
12 return 4 * math.pi * self.radius**2
13
14 def volume(self) -> float:
15 return (4 / 3) * math.pi * self.radius**3
The method
distance_between_centres
is one that ends up being changed by offloading the Euclidean distance calculation to thePoint3D
object
1import math
2
3
4class Sphere:
5
6 # init and/or other methods not shown for brevity
7
8 def distance_between_centres(self, other: "Sphere") -> float:
9 """
10 Calculate and return the distance between the centres of two Spheres.
11
12 :param other: Sphere whose centre to find the distance to from the self Sphere.
13 :type other: Sphere
14 :return: Distance between the Sphere centres.
15 :rtype: float
16 """
17 return self.centre_point.distance_from_point(other.centre_point)
Notice how this updated method has no responsibility over calculating the distance
Instead, we simply ask the
Point3D
object how far away it is from anotherPoint3D
object
1import math
2
3
4class Sphere:
5
6 # init and/or other methods not shown for brevity
7
8 def distance_between_edges(self, other: "Sphere") -> float:
9 """
10 Calculate and return the distance between the edges of two Spheres. If the value is negative, the two Spheres
11 overlap.
12
13 :param other: Sphere whose edge to find the distance to from the self Sphere.
14 :type other: Sphere
15 :return: Distance between the Sphere edges.
16 :rtype: float
17 """
18 return self.distance_between_centres(other) - self.radius - other.radius
19
20 def overlaps(self, other: "Sphere") -> bool:
21 """
22 Determine if two Sphere objects overlap within the 3D space. Two Spheres that are touching (distance of 0
23 between edges) are considered overlapping.
24
25 :param other: Sphere to check if it overlaps the self Sphere overlaps
26 :type other: Sphere
27 :return: Boolean indicating if the two Spheres overlap
28 :rtype: bool
29 """
30 return self.distance_between_edges(other) <= 0
The above
distance_between_edges
andoverlaps
methods remain unchanged from the original implementation of theSphere
They had already offloaded the Euclidean distance calculations to the
distance_between_centres
method
And finally, the magic methods end up getting updated slightly
1import math
2
3
4class Sphere:
5
6 # init and/or other methods not shown for brevity
7
8 def __eq__(self, other) -> bool:
9 if isinstance(other, Sphere):
10 return self.radius == other.radius and self.centre_point == other.centre_point
11 return False
The above updated
__eq__
now checks if thecentre_point
attributes are the same instead of checking thex
,y
, andz
explicitlyRemember, we defined the
__eq__
within thePoint3D
class
1import math
2
3
4class Sphere:
5
6 # init and/or other methods not shown for brevity
7
8 def __repr__(self) -> str:
9 return f"Sphere(centre_point={self.centre_point}, radius={self.radius})"
And lastly, instead of having our
__repr__
extract thex
,y
, andz
attributes, we simply get the string version of thePoint3D
With f-strings, Python will automatically convert the
Point3D
object to a string
The final string representation of the
Sphere
class will not be slightly different from beforeBefore, we would see something like
Sphere(x=1, y=2, z=3, radius=4)
Now we would see something like
Sphere(centre_point=Point3D(x=1, y=2, z=3), radius=4)
21.2.3. Testing
Although we could adapt our simple
assert
tests from the originalSphere
implementation, those tests are becoming hard to manageOur tests are now needing more setup before they can be run
For example, we now may need to make instances of
Point3D
objects andSphere
objects before we can test anything
It’s also difficult to tell the tests apart as they feel a little jumbled together
It’s not easy to know what a test is doing just by looking at it anymore
For example,
assert 0.01 > abs(sphere.distance_between_edges(Sphere(0, 0, 0, 0)) - (-0.26))
We can piece it together, but it’s not immediately clear
It’s also hard to get a sense of how thorough the tests are
Sure we have tested the distance between
Sphere
objects, but maybe we should test the distance if theSphere
objects are in different octants (3D equivalent of quadrants)
Further, if we want to be more thorough in the distance tests, there is going to be a lot of duplicate code
Fortunately, Python provides us with a tool to help us manage our tests —
unittest
The next topic will cover details on how to start transitioning our tests to the
unittest
framework for improved tests