# 20. Objects II — More on Methods

The

`Circle`

class discussed in the previous topic introduced:How to write classes

How to write and make use of a constructor

Attributes, both how to set them and how to access them

How to write and use methods

How to make instances of the class

In this topic, we will create more classes containing more complex functionality

Additionally, we will see objects interacting with one another and how objects can provide a nice level of abstraction

The goal is to define a

`Sphere`

classLike the

`Circle`

class, it will know its radiusIt will also know its position within some three dimensional space

It will provide functionality to measure distances between

`Sphere`

objects and check if they overlap/collide

## 20.1. Sphere Class

In order to define a

`Sphere`

, all we really need is a`radius`

With a

`radius`

, can calculate some values about the`Sphere`

Diameter

Surface Area

Volume

But we have an extra requirement — we need to know

*where*the`Sphere`

is in a three dimensional coordinate spaceTherefore, in addition to a

`radius`

, we need to keep track of`x`

,`y`

, and`z`

coordinatesWith this information, we can start to perform some more sophisticated calculations

How far away are two

`Sphere`

objects from one another?Do two

`Sphere`

objects overlap/collide?

There is nothing stopping us from adding more functionality to our

`Sphere`

class, but we will keep it simple for now

### 20.1.1. Constructor and Attributes

Below is the start of the

`Sphere`

class, including the constructor and the assignment of attributesIt follows the same pattern as the

`Circle`

class discussed in the previous topicThe only differences here with the

`Sphere`

are trivialAn

`import`

to help with math calculationsThe name of the class is different

The parameters and attributes are for a

`Sphere`

```
1import math
2
3
4class Sphere:
5 """
6 Class for managing Spheres within a 3D space. This includes tracking it's location in three dimensional space and
7 radius. Additionally, it allows for some basic geometry calculations, distance measurements between Spheres, and
8 checking if two Spheres overlap.
9 """
10
11 def __init__(self, x: float, y: float, z: float, radius: float):
12 self.x = x
13 self.y = y
14 self.z = z
15 self.radius = radius
```

That’s it — that is all we need to get started with the

`Sphere`

classLike before, we can even start making instances of a

`Sphere`

However, like before, the class will not be particularly useful here without the needed functionality

### 20.1.2. Methods

The methods we want are

Calculate the

`diameter`

,`surface_area`

and`volume`

Measure the

`distance_between_centres`

of two`Sphere`

objectsMeasure the

`distance_between_edges`

of two`Sphere`

objectsCheck if a

`Sphere`

`overlaps`

another in the three dimensional spaceA way to check if two

`Sphere`

objects are equivalent (`__eq__`

)A way to generate a human readable string representation of a

`Sphere`

(`__repr__`

)

```
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 above three methods follow the same pattern we saw with the

`Circle`

These look like regular functions, but the difference is

They are associated with an instance of a

`Sphere`

They have a

`self`

parameter, which is a reference variable to the`Sphere`

instanceAccessing any of the object’s attributes are done through the use of the

`self`

reference variable

Below is the

`distance_between_centres`

method, where we see some things that may feel odd

```
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 math.sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2 + (self.z - other.z) ** 2)
```

The method takes a parameter,

`other`

, that should be of type`Sphere`

— the class we are writingBut this does not break any rules — we are writing a method that can be invoked on an instance of the

`Sphere`

class that takes an instance of a`Sphere`

as a parameterThis is OK since the intended functionality is to find the distance between two

`Sphere`

objectsThe distance from the

`Sphere`

the method was invoked on to the`Sphere`

that was passed as a parameter

If this still makes you uneasy, consider how we would use this method

```
1sphere_a = Sphere(1, 1, 1, 10)
2sphere_b = Sphere(2, 2, 2, 15)
3distance = sphere_a.distance_between_centres(sphere_b)
4print(distance) # Results in 1.732051
```

In the above example, I invoked the method

`distance_between_centres`

on`sphere_a`

and passed`sphere_b`

as the argumentIf we take a moment to analyze the code within the function, we may get a better sense of the

`self`

reference variableBelow is the relevant code from the

`distance_between_centres`

method

```
1def distance_between_centres(self, other: "Sphere") -> float:
2 return math.sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2 + (self.z - other.z) ** 2)
```

This code calculates the Euclidean distance between the centres in three dimensional space

But notice that we are making use of two reference variables —

`self`

and`other`

This may be where

`self`

starts to make a little more sense

Again, consider

`sphere_a.distance_between_centres(sphere_b)`

In this context,

`sphere_a`

would be the`self`

`Sphere`

object referenceAnd

`sphere_b`

would be the`other`

reference

Note

You may have also noticed how the type hint for `other`

is the *string* `"Sphere"`

, as opposed to just
`Sphere`

, like how the function’s return type hint is just `float`

instead of the string `"float"`

. This is
because the `Sphere`

class, as far as Python is concerned, is not defined yet as the method
`distance_between_centres`

is being defined within the class `Sphere`

that is currently being defined.

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

In

`distance_between_edges`

above, notice how the method makes a call to the method`distance_between_centres`

Since the

`distance_between_edges`

needs the distance between centres in order to complete it’s calculation, there is no need to re-write that code — just call`distance_between_centres`

But, like the attributes, if we want to access the instance’s methods, we must access them via the reference variable

`self`

```
1import math
2
3
4class Sphere:
5
6 # init and/or other methods not shown for brevity
7
8 def overlaps(self, other: "Sphere") -> bool:
9 """
10 Determine if two Sphere objects overlap within the 3D space. Two Spheres that are touching (distance of 0
11 between edges) are considered overlapping.
12
13 :param other: Sphere to check if it overlaps the self Sphere overlaps
14 :type other: Sphere
15 :return: Boolean indicating if the two Spheres overlap
16 :rtype: bool
17 """
18 return self.distance_between_edges(other) <= 0
```

Similarly, the

`overlaps`

method can be written by making use of the already existing method`distance_between_edges`

#### 20.1.2.1. Magic Methods

There exist a number of special, or

*magic*methods within PythonWhat makes these methods

*magic*is that you do not call them directly; you call them indirectly through some other syntaxIn fact, the

`__init__`

method, the constructor, is a magic methodYou never actually directly call

`__init__`

in your codeInstead, the constructor gets invoked when instantiating an instance of the class

`some_sphere = Sphere(1, 2, 3, 4)`

In addition to the constructor, we will focus on two very important ones here

`__eq__`

— a method for checking object equality`__repr__`

— a method for generating a human readable string representation of the object

##### 20.1.2.1.1. `__eq__`

It is common to want to check if two things are equal

For example, like numbers —

`if some_number == 10:`

With numbers, strings, booleans, and other types, Python already knows what equality is

However, with custom classes, Python will not know what it means for instances of that class to be equal, unless you tell it

In the context of the

`Sphere`

class, you may have a good idea of what it means for two instances of this object to be equalBut Python cannot read your mind; you need to tell Python what it means for two

`Sphere`

objects to be equalBy default, Python will try to be helpful if you ask it if two objects of a custom class are equal

The default equality check is checking if two reference variables are referencing literally the exact same object in memory (aliases)

A more reasonable equality check for

`Sphere`

objects would be if they are the same size and exist in the same location within the three dimensional spaceThat is, if the

`radius`

,`x`

,`y`

, and`z`

attributes are equal

```
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 return self.x == other.x and self.y == other.y and self.z == other.z and self.radius == other.radius
```

The above code showing the

`__eq__`

method is how we define our equals magic methodFor our needs, check if

`self`

and`other`

have all their attributes being the same

You may be tempted to then check equality by calling the

`__eq__`

method explicitly`sphere_a.__eq__(sphere_b)`

Although this will work, it is a little clunky and bad style

Instead, we will indirectly invoke the equality method by using

`==`

like we have used for every other equality check`sphere_a == sphere_b`

There is, however, one problem with the way we wrote our equality method

Consider the below example

```
1some_sphere = Sphere(1, 2, 3, 4)
2some_circle = Circle(10)
3
4print(some_sphere == some_circle)
```

Running this code results in

`AttributeError: 'Circle' object has no attribute 'x'`

The trouble is that the

`Circle`

instance, which would be`other`

in the`Sphere`

's equality method, does not have an`x`

,`y`

, or`z`

attributeA simple way to fix this is to check if the

`other`

reference variable is even referencing something that can be properly compared to

```
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.x == other.x and self.y == other.y and self.z == other.z and self.radius == other.radius
11 return False
```

Note

There was nothing stopping us from defining `__eq__`

for our `Circle`

class. In fact, it is arguably something
we should do. Below is an example of an equality check for the `Circle`

class.

```
1import math
2
3
4class Circle:
5
6 # init and/or other methods not shown for brevity
7
8 def __eq__(self, other) -> bool:
9 if isinstance(other, Circle):
10 return self.radius == other.radius
11 return False
```

##### 20.1.2.1.2. `__repr__`

It is nice to have a good, human readable representation of the values within our program

For example, think of the number of times you have printed the values of variables when doing some quick tests of your code

```
1some_list = ["a", "b", "c"]
2if condition:
3 some_list.append("x")
4print(some_list)
```

If you were to try this with our newly created

`Sphere`

class, we would get something not overly helpful

```
1sphere = Sphere(1, 2, 3, 4)
2print(sphere) # Results in <__main__.Sphere object at 0x7f99a2edac10>
```

The default behaviour will be the name of the class along with the memory address of where the object is

Chances are, this is not overly helpful to you

To address this, we write another magic method —

`__repr__`

`__repr__`

is the representation function, which is for getting a nice string representation of the instance of the classThis

`__repr__`

method is called whenever we need a string representation of our object`print(some_sphere)`

will automatically call it`str(some_sphere)`

will call it`repr(some_sphere)`

will call it too

Based on what the object is, there may be a very natural way one would want to represent the object as a string

For example, the

`List`

class’`__repr__`

returns a string of the form`["a", "b", "c", "d"]`

But sometimes, like with a

`Sphere`

, it may not be obvious and we just want to get enough information about the`Sphere`

to be helpful for usIf this is the case, a common representation is

`Sphere(x=1, y=2, z=3, radius=4)`

– class name, and then relevant attribute values within parentheses

```
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(x={self.x}, y={self.y}, z={self.z}, radius={self.radius})"
```

With the

`__repr__`

written, if I were to call`print`

,`str`

, or`repr`

on an instance of the class, I would see the values specified

Note

Like with the `__eq__`

method, we could go back and write a `__repr__`

for the `Circle`

class.

```
1import math
2
3
4class Circle:
5
6 # init and/or other methods not shown for brevity
7
8 def __repr__(self) -> str:
9 return f"Circle(radius={self.radius})"
```

### 20.1.3. Testing

Below is a collection of

`assert`

tests for the`Sphere`

classThough, you may feel that at this stage it’s getting harder to get a feel for how complete your tests are

```
1sphere_origin_0 = Sphere(0, 0, 0, 0)
2assert 0 == sphere_origin_0.x
3assert 0 == sphere_origin_0.y
4assert 0 == sphere_origin_0.z
5assert 0 == sphere_origin_0.radius
6assert 0 == sphere_origin_0.diameter()
7assert 0 == sphere_origin_0.surface_area()
8assert 0 == sphere_origin_0.volume()
9assert 0 == sphere_origin_0.distance_between_centres(Sphere(0, 0, 0, 0))
10assert 0 == sphere_origin_0.distance_between_edges(Sphere(0, 0, 0, 0))
11assert True == sphere_origin_0.overlaps(Sphere(0, 0, 0, 0))
12assert True == (sphere_origin_0 == Sphere(0, 0, 0, 0))
13assert False == (sphere_origin_0 == Sphere(0, 0, 0, 1))
14assert "Sphere(0, 0, 0, 0)" == str(sphere_origin_0)
15
16sphere = Sphere(1, 2, 3, 4)
17assert 1 == sphere.x
18assert 2 == sphere.y
19assert 3 == sphere.z
20assert 4 == sphere.radius
21assert 8 == sphere.diameter()
22assert 0.01 > abs(sphere.surface_area() - 201.06)
23assert 0.01 > abs(sphere.volume() - 268.08)
24assert 0.01 > abs(sphere.distance_between_centres(Sphere(0, 0, 0, 0)) - 3.74)
25assert 0.01 > abs(sphere.distance_between_edges(Sphere(0, 0, 0, 0)) - (-0.26))
26assert True == sphere.overlaps(Sphere(0, 0, 0, 0))
27assert False == (sphere == Sphere(0, 0, 0, 0))
28assert True == (sphere == Sphere(1, 2, 3, 4))
29assert "Sphere(1, 2, 3, 4)" == str(sphere)
```