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