21. Objects III — Interacting Objects

  • We completed the Sphere class in the previous topic, and it will work for our needs

  • However, 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 things

    • Location in three dimensional space

    • Distance between points

    • Equality on points

  • Although there is nothing wrong with the original Sphere implementation, let’s consider that

    • It will make our Sphere class simpler and easier to understand

    • It allows us to write the methods at a higher-level of abstraction since can deal with Point3D objects instead of individual coordinates

    • If we wanted to make other three dimensional shapes, they could use something like a Point3D class to represent their position

    • It 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 the Spheres independently

21.1. Point3D Class

  • The purpose of the Point3D class is to replace all the coordinate details from the Sphere class

  • The 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 class

    • A method for measuring the distance from another Point3D object

    • A method for checking if two Point3D objects are equal

    • A 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 equality

    • A __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 for Point3D objects will be if all their attributes match

  • The __repr__ will follow the same pattern as the Sphere — class name with the relevant attributes

21.1.3. Testing

  • Below is a series of assert tests verifying the Point3D class’ correctness

  • Like 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 the Sphere class to offload the relevant work

    • Keeping track of the location of the centre of the Sphere within the three dimensional space

    • Measuring 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, and z coordinates as explicit attributes for the Sphere

  • Instead, we make use of a Point3D object

  • And, as we know, those coordinate attributes exist within the Point3D class, which are entirely accessible from within the Sphere class

21.2.2. Methods

  • The below methods for calculating the diameter, surface_area, and volume do not change since they only make use of the Sphere object’s radius 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 the Point3D 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 another Point3D 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 and overlaps methods remain unchanged from the original implementation of the Sphere

    • 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 the centre_point attributes are the same instead of checking the x, y, and z explicitly

    • Remember, we defined the __eq__ within the Point3D 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 the x, y, and z attributes, we simply get the string version of the Point3D

    • 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 before

  • Before, 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 original Sphere implementation, those tests are becoming hard to manage

  • Our tests are now needing more setup before they can be run

    • For example, we now may need to make instances of Point3D objects and Sphere 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 the Sphere 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

21.3. For Next Class