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 Point3Ds distance from the origin

    • A method for measuring the distance from another Point3D object

    • A method for finding the midpoint between two points

    • 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

 1class Point3D:
 2
 3    # init and/or other methods not shown for brevity
 4
 5    def distance_from_point(self, other: "Point3D") -> float:
 6        """
 7        Calculate the Euclidean distance from this Point3D (self) and the Point3D passed as a parameter.
 8
 9        :param other: A Point3D to find the the Euclidean distance to from the self Point3D
10        :type other: Point3D
11        :return: The Euclidean distance between the self Point3D and the parameter Point3D other
12        :rtype: float
13        """
14        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

 1class Point3D:
 2
 3    # init and/or other methods not shown for brevity
 4
 5    def __eq__(self, other) -> bool:
 6        """
 7        Check if the self Point3D is equal to the Point3D passed as a parameter. Points3D are considered equal if they
 8        have the same x, y, and z values.
 9
10        This is a "magic method" that can be used with `==`.
11
12        :param other: A Point3D to compare to the self point3D
13        :type other: Point3D
14        :return: A boolean indicating if the two Point3Ds are equivalent.
15        :rtype: boolean
16        """
17        if isinstance(other, Point3D):
18            return self.x == other.x and self.y == other.y and self.z == other.z
19        return False
20
21
22    def __repr__(self) -> str:
23        """
24        Generate and return a string representation of the Point3D object.
25
26        This os a "magic method" that can be used with `str(some_point3d)` or for printing.
27
28        :return: A string representation of the Point3D
29        :rtype: string
30        """
31        return f"Point3D({self.x}, {self.y}, {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.distance_from_origin()
 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(1, 1, 1) == point_origin.find_midpoint(Point3D(2, 2, 2))
 9assert Point3D(-1, -1, -1) == point_origin.find_midpoint(Point3D(-2, -2, -2))
10assert Point3D(0, 0, 0) == point_origin.find_midpoint(Point3D(0, 0, 0))
11assert "Point3D(0, 0, 0)" == str(point_origin)
12
13point = Point3D(-2, 7, 4)
14assert 0.001 > abs(point.distance_from_origin() - 8.306624)
15assert 0.001 > abs(point.distance_from_point(Point3D(0, 0, 0)) - 8.306624)
16assert 0.001 > abs(point.distance_from_point(Point3D(6, 3, 0)) - 9.797959)
17assert Point3D(5, 5.5, 3) == point.find_midpoint(Point3D(12, 4, 2))
18assert "Point3D(-2, 7, 4)"
19
20assert point != point_origin
21assert 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

 1class Sphere:
 2
 3    # init and/or other methods not shown for brevity
 4
 5    def diameter(self) -> float:
 6        return 2 * self.radius
 7
 8    def surface_area(self) -> float:
 9        return 4 * math.pi * self.radius**2
10
11    def volume(self) -> float:
12        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

 1class Sphere:
 2
 3    # init and/or other methods not shown for brevity
 4
 5    def distance_between_centres(self, other: "Sphere") -> float:
 6        """
 7        Calculate and return the distance between the centres of two Spheres.
 8
 9        :param other: Sphere whose centre to find the distance to from the self Sphere.
10        :type other: Sphere
11        :return: Distance between the Sphere centres.
12        :rtype: float
13        """
14        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

 1class Sphere:
 2
 3    # init and/or other methods not shown for brevity
 4
 5    def distance_between_edges(self, other: "Sphere") -> float:
 6        """
 7        Calculate and return the distance between the edges of two Spheres. If the value is negative, the two Spheres
 8        overlap.
 9
10        :param other: Sphere whose edge to find the distance to from the self Sphere.
11        :type other: Sphere
12        :return: Distance between the Sphere edges.
13        :rtype: float
14        """
15        return self.distance_between_centres(other) - self.radius - other.radius
16
17    def overlaps(self, other: "Sphere") -> bool:
18        """
19        Determine if two Sphere objects overlap within the 3D space. Two Spheres that are touching (distance of 0
20        between edges) are considered overlapping.
21
22        :param other: Sphere to check if it overlaps the self Sphere overlaps
23        :type other: Sphere
24        :return: Boolean indicating if the two Spheres overlap
25        :rtype: bool
26        """
27        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

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

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})"
  • 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