# 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 `Sphere`s 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 `Point3D`s 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
```
• 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:
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        """
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):
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:
```
• 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