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 aradius
With a
radius
, can calculate some values about theSphere
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 ofx
,y
, andz
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
andvolume
Measure the
distance_between_centres
of twoSphere
objectsMeasure the
distance_between_edges
of twoSphere
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 theSphere
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 typeSphere
— 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 aSphere
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 theSphere
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
onsphere_a
and passedsphere_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
andother
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 theself
Sphere
object referenceAnd
sphere_b
would be theother
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 methoddistance_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 calldistance_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 methoddistance_between_edges
20.1.2.1. Magic Methods
There exist a number of special, or magic methods within Python
What makes these methods magic is that you do not call them directly; you call them indirectly through some other syntax
In 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
, andz
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
andother
have all their attributes being the same
You may be tempted to then check equality by calling the
__eq__
method explicitlysphere_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 checksphere_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 beother
in theSphere
's equality method, does not have anx
,y
, orz
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 objectprint(some_sphere)
will automatically call itstr(some_sphere)
will call itrepr(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 theSphere
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 callprint
,str
, orrepr
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 theSphere
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)