# 21. Objects III — Interacting Objects

We completed the

`Sphere`

class in the previous topic, and it will work for our needsHowever, 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 thingsLocation in three dimensional space

Distance between points

Equality on points

Although there is nothing wrong with the original

`Sphere`

implementation, let’s consider thatIt will make our

`Sphere`

class simpler and easier to understandIt allows us to write the methods at a higher-level of abstraction since can deal with

`Point3D`

objects instead of individual coordinatesIf we wanted to make other three dimensional shapes, they could use something like a

`Point3D`

class to represent their positionIt 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`

classThe 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`

classA method for measuring the

`Point3D`

s distance from the originA method for measuring the distance from another

`Point3D`

objectA 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 equalityA

`__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 matchThe

`__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’ correctnessLike 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 workKeeping track of the location of the centre of the

`Sphere`

within 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 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`

objectAnd, 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`

explicitlyRemember, 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 beforeBefore, 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 manageOur 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