20. Objects II — More on Methods
The previous topic covered the basics of classes, constructors, attributes, and methods using a
CircleclassHere, we build on that by defining a
Sphereclass with more complex methods — including methods that take anotherSphereas a parameterThe goal is to define a
SphereclassLike the
Circleclass, it will know its radiusIt will also know its position within some three dimensional space
It will provide functionality to measure distances between
Sphereobjects and check if they overlap/collide
20.1. Sphere Class
In order to define a
Sphere, all we really need is aradiusWith a
radius, can calculate some values about theSphereDiameter
Surface Area
Volume
But we have an extra requirement — we need to know where the
Sphereis in a three dimensional coordinate spaceTherefore, in addition to a
radius, we need to keep track ofx,y, andzcoordinatesWith this information, we can start to perform some more sophisticated calculations
How far away are two
Sphereobjects from one another?Do two
Sphereobjects overlap/collide?
There is nothing stopping us from adding more functionality to our
Sphereclass, but we will keep it simple for now
20.1.1. Constructor and Attributes
Below is the start of the
Sphereclass, including the constructor and the assignment of attributesIt follows the same pattern as the
Circleclass discussed in the previous topicThe only differences here with the
Sphereare trivialAn
importto 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 its 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 all we need to get started with the
SphereclassLike before, the class won’t be very useful until we add methods
20.1.2. Methods
The methods we want are
Calculate the
diameter,surface_areaandvolumeMeasure the
distance_between_centresof twoSphereobjectsMeasure the
distance_between_edgesof twoSphereobjectsCheck if a
Sphereoverlapsanother in the three dimensional spaceA way to check if two
Sphereobjects 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 as the
Circlemethods from the previous topicThey are associated with an instance of a
SphereThey have a
selfparameter, which is a reference variable to theSphereinstanceAccessing any of the object’s attributes are done through the use of the
selfreference variable
Below is the
distance_between_centresmethod, which introduces something new
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 :return: Distance between the Sphere centres.
14 """
15 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
Sphereclass that takes an instance of aSphereas a parameterThis is OK since the intended functionality is to find the distance between two
SphereobjectsThe distance from the
Spherethe method was invoked on to theSpherethat 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_centresonsphere_aand passedsphere_bas the argumentIf we take a moment to analyze the code within the function, we may get a better sense of the
selfreference variableBelow is the relevant code from the
distance_between_centresmethod
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 —
selfandotherThis may be where
selfstarts to make a little more sense
Again, consider
sphere_a.distance_between_centres(sphere_b)In this context,
sphere_awould be theselfSphereobject referenceAnd
sphere_bwould be theotherreference
Note
You may have noticed that the type hint for other is the string "Sphere" rather than just Sphere.
This is because when the method is being defined, the Sphere class itself is not fully defined yet — it is
still being written. Using the string form tells Python to resolve the type later.
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 :return: Distance between the Sphere edges.
15 """
16 return self.distance_between_centres(other) - self.radius - other.radius
distance_between_edgescallsdistance_between_centresrather than duplicating the calculationLike attributes, calling a method on the same instance requires
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
14 :return: Boolean indicating if the two Spheres overlap
15 """
16 return self.distance_between_edges(other) <= 0
overlapsfollows the same idea, reusingdistance_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__
With numbers, strings, and booleans, Python already knows what equality means
With custom classes, Python has no way to know what equality means unless you define it
By default, Python falls back to checking if two reference variables point to literally the same object in memory (aliases)
For
Sphereobjects, a more useful equality check is whether they are the same size and in the same locationThat is, if the
radius,x,y, andzattributes are all 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 defines equality for
Sphere— if all attributes match, the two instances are equalRather than calling
sphere_a.__eq__(sphere_b)directly, use==as you would for any other typesphere_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
Circleinstance doesn’t havex,y, orzattributes, so the comparison crashesThe fix is to first check whether
otheris actually aSpherebefore comparing — usingisinstance
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
We could also add __eq__ to the Circle class. Below is an example.
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
Sphereclass, 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 is the class name and memory address — not particularly useful
To address this, we write another magic method —
__repr____repr__is called whenever Python needs a string representation of the object — viaprint,str(), orrepr()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
Listclass’__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 theSphereto 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, orrepron an instance of the class, I would see the values specified
Note
We could also add __repr__ to 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
asserttests for theSphereclassThough, 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(x=0, y=0, z=0, radius=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(x=1, y=2, z=3, radius=4)" == str(sphere)
20.2. For Next Topic
Download and look through the
Sphere class