21. Objects III — Interacting Objects
The
Sphereclass from the previous topic works, but there are a few design improvements worth makingOne idea is to introduce a
Point3Dclass to manage all the coordinate-related concernsLocation in three dimensional space
Distance between points
Equality on points
Doing so will make the
Sphereclass simpler, since it can delegate coordinate work toPoint3DIt also means any other three dimensional shape we write could reuse
Point3DAnd it makes testing easier — we can test
Point3DandSphereindependently
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 Euclidean distance to from the self Point3D
13 :return: The Euclidean distance between the self Point3D and the parameter Point3D other
14 """
15 return math.sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2 + (self.z - other.z) ** 2)
This follows the same pattern as the
distance_between_centresmethod from the previous topic — a method that takes another instance of the same class as a parameter
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 :return: A boolean indicating if the two Point3Ds are equivalent.
17 """
18 if isinstance(other, Point3D):
19 return self.x == other.x and self.y == other.y and self.z == other.z
20 return False
21
22
23 def __repr__(self) -> str:
24 """
25 Generate and return a string representation of the Point3D object.
26
27 This is a "magic method" that can be used with `str(some_point3d)` or for printing.
28
29 :return: A string representation of the Point3D
30 """
31 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 are 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 its 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
The
x,y, andzattributes are gone — replaced by a singlePoint3DobjectThe coordinate data still exists, just inside the
Point3D
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 :return: Distance between the Sphere centres.
14 """
15 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 :return: Distance between the Sphere edges.
15 """
16 return self.distance_between_centres(other) - self.radius - other.radius
17
18 def overlaps(self, other: "Sphere") -> bool:
19 """
20 Determine if two Sphere objects overlap within the 3D space. Two Spheres that are touching (distance of 0
21 between edges) are considered overlapping.
22
23 :param other: Sphere to check if it overlaps the self Sphere
24 :return: Boolean indicating if the two Spheres overlap
25 """
26 return self.distance_between_edges(other) <= 0
distance_between_edgesandoverlapsare unchanged — they already delegated todistance_between_centresThe magic methods are also updated slightly
1class Sphere:
2
3 # init and/or other methods not shown for brevity
4
5 def __eq__(self, other) -> bool:
6 if isinstance(other, Sphere):
7 return self.radius == other.radius and self.centre_point == other.centre_point
8 return False
__eq__now comparescentre_pointattributes instead ofx,y,zindividually — it uses the__eq__we defined onPoint3D
1class Sphere:
2
3 # init and/or other methods not shown for brevity
4
5 def __repr__(self) -> str:
6 return f"Sphere(centre_point={self.centre_point}, radius={self.radius})"
__repr__now embeds thePoint3Dstring directly — Python will automatically callPoint3D's__repr__inside the f-stringThe result changes from
Sphere(x=1, y=2, z=3, radius=4)toSphere(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(Point3D(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
21.3. For Next Topic
Download and look through the
Point3D classDownload and look through the
Sphere class