21. Objects III — Interacting Objects
We completed the
Sphereclass 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
Point3Dclass 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
Sphereimplementation, let’s consider thatIt will make our
Sphereclass simpler and easier to understandIt allows us to write the methods at a higher-level of abstraction since can deal with
Point3Dobjects instead of individual coordinatesIf we wanted to make other three dimensional shapes, they could use something like a
Point3Dclass 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
Point3Dclass’ functionality and theSpheres independently
21.1. Point3D Class
The purpose of the
Point3Dclass is to replace all the coordinate details from theSphereclassThe class will have three attributes that represent a position within some three dimensional space with a Cartesian coordinate system
xyz
The class will also have a few methods, including some that will replace functionality within the
SphereclassA method for measuring the distance from another
Point3DobjectA method for checking if two
Point3Dobjects 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
Point3Dclass, 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
Point3Dclass
21.1.2. Methods
The methods we want are
A way to measure the distance from another point —
distance_from_pointAn
__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
Sphereclass, 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 forPoint3Dobjects 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
asserttests verifying thePoint3Dclass’ correctnessLike before, these tests work, but we area pushing the limits of our simple
asserttests
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
Point3Dclass, we can now make use of it in theSphereclass to offload the relevant workKeeping track of the location of the centre of the
Spherewithin 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, andzcoordinates as explicit attributes for theSphereInstead, we make use of a
Point3DobjectAnd, as we know, those coordinate attributes exist within the
Point3Dclass, which are entirely accessible from within theSphereclass
21.2.2. Methods
The below methods for calculating the
diameter,surface_area, andvolumedo not change since they only make use of theSphereobject’sradiusattribute
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_centresis one that ends up being changed by offloading the Euclidean distance calculation to thePoint3Dobject
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
Point3Dobject how far away it is from anotherPoint3Dobject
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_edgesandoverlapsmethods remain unchanged from the original implementation of theSphereThey had already offloaded the Euclidean distance calculations to the
distance_between_centresmethod
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_pointattributes are the same instead of checking thex,y, andzexplicitlyRemember, we defined the
__eq__within thePoint3Dclass
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, andzattributes, we simply get the string version of thePoint3DWith f-strings, Python will automatically convert the
Point3Dobject to a string
The final string representation of the
Sphereclass 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
asserttests from the originalSphereimplementation, 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
Point3Dobjects andSphereobjects 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
Sphereobjects, but maybe we should test the distance if theSphereobjects 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 —
unittestThe next topic will cover details on how to start transitioning our tests to the
unittestframework for improved tests