class TriangleSSS:
"""Represents a geometric triangle based on its three side lengths.
Attributes:
a (float): The length of the first side.
b (float): The length of the second side.
c (float): The length of the third side.
"""
def __init__(self, a: float, b: float, c: float):
"""Initialises the triangle with the three side lengths.
Args:
a (float): Length of side a.
b (float): Length of side b.
c (float): Length of side c.
"""
self.a = a
self.b = b
self.c = c
def _get_semiperimeter(self):
"""Calculates the semiperimeter of the triangle.
Returns:
float: The semiperimeter of the triangle.
"""
return (self.a + self.b + self.c) / 2
def get_area(self):
"""Calculates the area of the triangle using Heron's formula.
Returns:
float: The area of the triangle.
"""
s = self._get_semiperimeter()
T = (s * (s - self.a) * (s - self.b) * (s - self.c)) ** (1/2)
return T
def get_altitude(self, base='a'):
"""Calculates the altitude of the triangle relative to a specific base side.
Args:
base (str, optional): The base side to which the altitude relates
('a', 'b', or 'c'). Defaults to 'a'.
Returns:
float: The altitude relative to the chosen base side.
"""
T = self.get_area()
if base == 'a':
h = (2 * T) / self.a
elif base == 'b':
h = (2 * T) / self.b
else:
h = (2 * T) / self.c
return h
def get_circumcircle_radius(self):
"""Calculates the radius of the circumcircle of the triangle.
Returns:
float: The radius of the circumcircle.
"""
T = self.get_area()
n = self.a * self.b * self.c
d = 4 * T
r = n / d
return r
def get_incircle_radius(self):
"""Calculates the radius of the incircle of the triangle.
Returns:
float: The radius of the incircle.
"""
T = self.get_area()
s = self._get_semiperimeter()
rho = T / s
return rho22 Classes: Triangle as Object
22.1 Introduction
Functions are perfect for avoiding repetitive code (DRY principle). But when you model real-world things—like a collection of triangles defined by their three side lengths—a Python class allows you to bundle both the data and the functions together efficiently.
In the following code cell, the functions from the previous chapter are integrated into a class named TriangleSSS.
To use the TriangleSSS class, we need to create (instantiate) a new triangle object. The following code cell shows how to do this.
When calling TriangleSSS(3, 4, 5), a very special function is automatically executed behind the scenes: the __init__() method (often referred to as the constructor).
The values 3, 4, and 5 are passed into the parameters a, b, and c. The self keyword then ensures that these values are bound to the newly created instance and stored in memory as attributes (self.a, self.b, and self.c). It is precisely through this step that the triangle object initialises and “remembers” its individual side lengths.
You can easily verify that these attributes have been successfully saved by accessing them directly using dot notation - for instance, running print(triangle.a) will output 3.
The newly instantiated object triangle allows us to access the methods of the TriangleSSS class. Methods are simply functions defined inside a class. They are called using dot notation, as shown in the example below.
Normally, we call a Python function by passing arguments inside the parentheses. However, thanks to the self argument in our method definitions, the method already knows the values of a, b, and c (by accessing self.a, self.b, and self.c). The variable self always refers to the specific instance of our object.
22.2 Enhanced Triangle Class
In geometry, a triangle is uniquely defined if we know specific combinations of its side lengths and angles. This is determined by the four classic congruence criteria:
SSS (Side-Side-Side): All three side lengths are known.
SAS (Side-Angle-Side): Two sides and the enclosed angle are known.
ASA / SAA (Angle-Side-Angle / Side-Angle-Angle): One side and two angles are known.
SsA (Side-Side-Angle): Two sides and an angle opposite the longer side are known.
While our initial TriangleSSS class worked perfectly when given three sides, a truly versatile geometric model should handle any valid combination. Ideally, we want a single, elegant Python class capable of dynamically initialising a triangle based on whichever of these four criteria the user provides.
The following implementation achieves exactly this by identifying the given parameters and validating them against mathematical rules before creating the object.
import math
class Triangle:
def __init__(self, a=None, b=None, c=None, alpha=None, beta=None, gamma=None):
"""Initialise a Triangle instance using valid congruence criteria.
Args:
a (float, optional): Length of side a. Defaults to None.
b (float, optional): Length of side b. Defaults to None.
c (float, optional): Length of side c. Defaults to None.
alpha (float, optional): Angle alpha opposite to side a. Defaults to None.
beta (float, optional): Angle beta opposite to side b. Defaults to None.
gamma (float, optional): Angle gamma opposite to side c. Defaults to None.
Raises:
ValueError: If the number of provided parameters is not exactly three,
if the parameters do not form a unique triangle, or if
mathematical rules (e.g. triangle inequality) are violated.
"""
# 1. Initialise all attributes to prevent subsequent AttributeErrors
self.a = a
self.b = b
self.c = c
self.alpha = alpha
self.beta = beta
self.gamma = gamma
# 2. Determine which values were actually passed to the constructor
given_sides = [k for k, v in [('a', a), ('b', b), ('c', c)] if v is not None]
given_angles = [k for k, v in [('alpha', alpha), ('beta', beta), ('gamma', gamma)] if v is not None]
num_sides = len(given_sides)
num_angles = len(given_angles)
# A triangle mathematically always requires exactly 3 parameters
if num_sides + num_angles != 3:
raise ValueError("A triangle must be defined by exactly 3 parameters.")
# 3. Validate mathematical congruence theorems
if num_sides == 3:
# --- SSS (Side-Side-Side) ---
if not (a + b > c and a + c > b and b + c > a):
raise ValueError("Triangle inequality violated: The sum of two sides must be greater than the third.")
elif num_sides == 2 and num_angles == 1:
# --- SAS or SsA (Side-Angle-Side / Side-Side-Angle) ---
angle_name = given_angles[0]
opposite_mapping = {'alpha': 'a', 'beta': 'b', 'gamma': 'c'}
opposite_side = opposite_mapping[angle_name]
if opposite_side in given_sides:
# Case: SsA (Angle is opposite one of the given sides)
other_side = [s for s in given_sides if s != opposite_side][0]
val_opposite = {'a': a, 'b': b, 'c': c}[opposite_side]
val_other = {'a': a, 'b': b, 'c': c}[other_side]
if val_opposite <= val_other:
raise ValueError(f"SsA error: Angle '{angle_name}' must be opposite the longer side.")
elif num_sides == 1 and num_angles == 2:
# --- ASA / SAA (Angle-Side-Angle / Side-Angle-Angle) ---
angle_values = [v for v in [alpha, beta, gamma] if v is not None]
if sum(angle_values) >= 180:
raise ValueError("The sum of the given angles must be less than 180 degrees.")
elif num_angles == 3:
# --- AAA (Angle-Angle-Angle) ---
raise ValueError("AAA (Three angles) does not define a unique triangle due to a lack of scaling.")
# 4. Mathematically solve the complete triangle to populate all missing sides and angles
self._solve_triangle()
def _solve_triangle(self):
"""Private helper method to calculate all missing sides and angles."""
def rad(deg): return math.radians(deg)
def deg(rad): return math.degrees(rad)
# Case 1: SSS
if self.a and self.b and self.c:
self.alpha = deg(math.acos((self.b**2 + self.c**2 - self.a**2) / (2 * self.b * self.c)))
self.beta = deg(math.acos((self.a**2 + self.c**2 - self.b**2) / (2 * self.a * self.c)))
self.gamma = 180.0 - self.alpha - self.beta
# Case 2: 2 sides, 1 angle (SAS or SsA)
elif sum(s is not None for s in [self.a, self.b, self.c]) == 2:
sides = {'a': self.a, 'b': self.b, 'c': self.c}
angles = {'alpha': self.alpha, 'beta': self.beta, 'gamma': self.gamma}
given_s = [k for k, v in sides.items() if v is not None]
given_a = [k for k, v in angles.items() if v is not None][0]
angle_val = angles[given_a]
enclosed_map = {('a', 'b'): 'gamma', ('b', 'a'): 'gamma',
('b', 'c'): 'alpha', ('c', 'b'): 'alpha',
('a', 'c'): 'beta', ('c', 'a'): 'beta'}
if given_a == enclosed_map[tuple(given_s)]:
# Sub-case: SAS
s1, s2 = given_s[0], given_s[1]
v1, v2 = sides[s1], sides[s2]
v3 = math.sqrt(v1**2 + v2**2 - 2 * v1 * v2 * math.cos(rad(angle_val)))
missing_s = [s for s in ['a', 'b', 'c'] if s not in given_s][0]
setattr(self, missing_s, v3)
# Recalculate angles as SSS
self.alpha = deg(math.acos((self.b**2 + self.c**2 - self.a**2) / (2 * self.b * self.c)))
self.beta = deg(math.acos((self.a**2 + self.c**2 - self.b**2) / (2 * self.a * self.c)))
self.gamma = 180.0 - self.alpha - self.beta
else:
# Sub-case: SsA
opposite_side = {'alpha': 'a', 'beta': 'b', 'gamma': 'c'}[given_a]
other_side = [s for s in given_s if s != opposite_side][0]
val_opp, val_oth = sides[opposite_side], sides[other_side]
sin_other = math.sin(rad(angle_val)) * val_oth / val_opp
other_angle_val = deg(math.asin(sin_other))
other_angle_name = {'a': 'alpha', 'b': 'beta', 'c': 'gamma'}[other_side]
setattr(self, other_angle_name, other_angle_val)
missing_a = [a for a in ['alpha', 'beta', 'gamma'] if getattr(self, a) is None][0]
setattr(self, missing_a, 180.0 - angle_val - other_angle_val)
missing_s = {'alpha': 'a', 'beta': 'b', 'gamma': 'c'}[missing_a]
missing_s_val = val_opp * math.sin(rad(getattr(self, missing_a))) / math.sin(rad(angle_val))
setattr(self, missing_s, missing_s_val)
# Case 3: 1 side, 2 angles (ASA / SAA)
elif sum(s is not None for s in [self.a, self.b, self.c]) == 1:
if self.alpha is None: self.alpha = 180.0 - self.beta - self.gamma
elif self.beta is None: self.beta = 180.0 - self.alpha - self.gamma
elif self.gamma is None: self.gamma = 180.0 - self.alpha - self.beta
known_s_name = [s for s in ['a', 'b', 'c'] if getattr(self, s) is not None][0]
known_s_val = getattr(self, known_s_name)
known_a_val = getattr(self, {'a': 'alpha', 'b': 'beta', 'c': 'gamma'}[known_s_name])
factor = known_s_val / math.sin(rad(known_a_val))
if self.a is None: self.a = factor * math.sin(rad(self.alpha))
if self.b is None: self.b = factor * math.sin(rad(self.beta))
if self.c is None: self.c = factor * math.sin(rad(self.gamma))
# ==================================================================
# Methods derived from the formulas in Durandi, Werner, Baoswan
# Dzung Wong, Markus Kriener, Hansruedi Künsch, Alfred Vogelsanger,
# Jörg Waldvogel, Samuel Byland, u. a. Formeln, Tabellen, Begriffe:
# Mathematik - Physik - Chemie. 8., Aktualisierte Auflage.
# Herausgegeben von Verein Schweizerischer Mathematik- und
# Physiklehrkräfte und Werner Durandi. Zürich: Orell Füssli Verlag,
# 2022; pp. 86.
# ==================================================================
def get_semiperimeter(self):
"""Calculate the semiperimeter (s) of the triangle.
Returns:
float: The semiperimeter value.
"""
# Formula: s = (a + b + c) / 2
return (self.a + self.b + self.c) / 2
def get_exterior_angle(self, vertex='alpha'):
"""Calculate the exterior angle for a given interior angle vertex.
Args:
vertex (str): The interior angle name ('alpha', 'beta', 'gamma'). Defaults to 'alpha'.
Returns:
float: The exterior angle in degrees.
"""
# Formula: alpha' = 180° - alpha
interior_angle = getattr(self, vertex)
return 180.0 - interior_angle
def get_area(self):
"""Calculate the area (A) of the triangle using Heron's formula.
Returns:
float: The area of the triangle.
"""
# Formula: A = sqrt(s * (s - a) * (s - b) * (s - c)) (HERON)
s = self.get_semiperimeter()
return math.sqrt(s * (s - self.a) * (s - self.b) * (s - self.c))
def get_altitude(self, base='a'):
"""Calculate the altitude (h) relative to a specific base side.
Args:
base (str): The base side identifier ('a', 'b', 'c'). Defaults to 'a'.
Returns:
float: The height (altitude) on the chosen base side.
"""
# Formula: h_a = c * sin(beta) or h_a = (2 / a) * Area
base_val = getattr(self, base)
return (2 * self.get_area()) / base_val
def get_median(self, side='a'):
"""Calculate the length of the median (Seitenhalbierende) for a side.
Args:
side (str): The side identifier ('a', 'b', 'c'). Defaults to 'a'.
Returns:
float: The length of the median line.
"""
# Formula: s_a = 0.5 * sqrt(2*b^2 + 2*c^2 - a^2)
if side == 'a':
return 0.5 * math.sqrt(2 * self.b**2 + 2 * self.c**2 - self.a**2)
elif side == 'b':
return 0.5 * math.sqrt(2 * self.a**2 + 2 * self.c**2 - self.b**2)
else:
return 0.5 * math.sqrt(2 * self.a**2 + 2 * self.b**2 - self.c**2)
def get_angle_bisector(self, angle='alpha'):
"""Calculate the length of the internal angle bisector (Winkelhalbierende).
Args:
angle (str): The angle identifier ('alpha', 'beta', 'gamma'). Defaults to 'alpha'.
Returns:
float: The length of the angle bisector line.
"""
# Formula: w_alpha = sqrt(b * c * [1 - (a / (b + c))^2])
if angle == 'alpha':
return math.sqrt(self.b * self.c * (1 - (self.a / (self.b + self.c))**2))
elif angle == 'beta':
return math.sqrt(self.a * self.c * (1 - (self.b / (self.a + self.c))**2))
else:
return math.sqrt(self.a * self.b * (1 - (self.c / (self.a + self.b))**2))
def get_incircle_radius(self):
"""Calculate the radius of the incircle (rho).
Returns:
float: The incircle radius.
"""
# Formula: rho = A / s
return self.get_area() / self.get_semiperimeter()
def get_excircle_radius(self, side='a'):
"""Calculate the radius of the excircle (Ankreisradius rho_i) for a side.
Args:
side (str): The side identifier ('a', 'b', 'c') where the circle touches. Defaults to 'a'.
Returns:
float: The excircle radius.
"""
# Formula: rho_a = A / (s - a)
s = self.get_semiperimeter()
side_val = getattr(self, side)
return self.get_area() / (s - side_val)
def get_circumcircle_radius(self):
"""Calculate the radius of the circumcircle (r).
Returns:
float: The circumcircle radius.
"""
# Formula: r = (a * b * c) / (4 * A)
return (self.a * self.b * self.c) / (4 * self.get_area())
def __repr__(self):
data = {k: round(v, 2) for k, v in self.__dict__.items() if not k.startswith('_')}
return f"{self.__class__.__name__}({data})"22.2.1 Inspecting the Triangle Instance
When printing a Python object or evaluating it directly inside a notebook cell, the default output is typically a generic string showing the object’s class name and its memory address. To provide a clear and meaningful summary of a triangle’s properties, we can implement the special __repr__ method.
The implementation above dynamically inspects the object’s internal attribute dictionary (self.__dict__). To ensure the output remains clean and legible, it automatically filters out any private helper attributes (those starting with an underscore) and rounds all public side lengths and angles to two decimal places. This guarantees that whenever a Triangle instance is invoked, it displays a neat, readable overview of its current geometric state.
To test this, in the cell below a Triangle triangle is instantiated and then printed to the command line.
22.3 Coding Task: Circle Class
After studying the Triangle class you should be able, to implement a Python class Circle on your own. To do so, you’ll find the mathematical properties of a circle below.
The radian measure \(\widehat{\varphi}\) of an angle is the ratio of the length of the arc intersected by the angle \(\varphi\) on a circle to the radius of the circle. It is also denoted by \(\operatorname{arc} \varphi\).
\[\widehat{\varphi} = \frac{b}{r} = \operatorname{arc} \varphi = \frac{\pi}{180^\circ} \varphi \iff \varphi = \frac{180^\circ}{\pi} \widehat{\varphi}\]
On the unit circle (\(r=1\)), we have: \(\widehat{\varphi} = b\)
The unit of radian measure is the radian (rad). An angle of 1 rad corresponds to a circular arc that has the same length as the radius.1
To define a circle, either its radius or its circumference is sufficient. With those data the following elements can be calculated:
- Circumference: \(c = 2\pi r\)
- Radius: \(r = \frac{c}{2 \pi}\)
- Area: \(A = \pi r^2\)
- Arc Length: \(a = \widehat{\beta} r = \frac{\pi}{180^\circ}\beta r\)
- Sector area: \(A = \frac{\widehat{\beta} r^2}{2} = \frac{\pi \beta r^2}{360^\circ} = \frac{ar}{2}\)
- Chord length: \(s = 2r \sin \frac{\alpha}{2}\)
- Segment height: \(h = r \left(1 - \cos \frac{\alpha}{2}\right)\)
- Segment area: \(A = \frac{r^2}{2} (\operatorname{arc} \alpha - \sin \alpha)\)
22.3.1 Implement Your own Circle Class
In the following cell you’ll find the skeleton of a Circle class. Implement the missing parts. You don’t need to implement your own __repr__ method.
Computers calculate angles in radians (see box above). To convert angles given in degrees use the function math.radians()
Circle class
import math
class Circle:
def __init__(self, r=None, c=None):
"""Initialises a Circle via either radius or circumference.
Raises:
ValueError: If neither or both parameters are provided.
"""
if r is not None and c is not None:
raise ValueError("Provide either radius 'r' OR circumference 'c', not both.")
if r == None and c == None:
raise ValueError("A circle must be initialised with either 'r' or 'c'.")
if r is not None:
if r < 0:
raise ValueError("Radius cannot be negative.")
c = 2 * math.pi * r
elif c is not None:
if c < 0:
raise ValueError("Circumference cannot be negative.")
r = c / (2 * math.pi)
self.r = r
self.c = c
def get_area(self):
return math.pi * self.r ** 2
def get_arc_length(self, beta):
"""Calculates arc length for angle beta in degrees."""
return (math.pi / 180) * beta * self.r
def get_sector_area(self, beta):
"""Calculates sector area for angle beta in degrees."""
a = self.get_arc_length(beta)
return (a * self.r) / 2
def get_chord_length(self, alpha):
"""Calculates chord length for angle alpha in degrees."""
alpha_rad = math.radians(alpha)
return 2 * self.r * math.sin(alpha_rad / 2)
def get_segment_height(self, alpha):
"""Calculates segment height for angle alpha in degrees."""
alpha_rad = math.radians(alpha)
return self.r * (1 - math.cos(alpha_rad / 2))
def get_segment_area(self, alpha):
"""Calculates segment area for angle alpha in degrees."""
alpha_rad = math.radians(alpha) # arc alpha is just the angle in radians!
return (self.r**2 / 2) * (alpha_rad - math.sin(alpha_rad))