The Rational Class#

Note

Source: Adapted from the C# edition (classes/rational.rst). Python’s dunder (double-underscore) methods replace C#’s operator+, operator*, and CompareTo syntax. Python’s math.gcd replaces the hand-coded GCD function.

A rational number is a ratio of two integers: 2/3, -5/4, 7. We build a Rational class that stores numerator and denominator in lowest terms and supports arithmetic with natural Python operators.

Class Definition and Normalisation#

import math

class Rational:
    def __init__(self, numerator, denominator=1):
        if denominator == 0:
            raise ValueError("Denominator cannot be zero")
        g = math.gcd(abs(numerator), abs(denominator))
        sign = -1 if denominator < 0 else 1
        self._num = sign * numerator // g
        self._denom = sign * denominator // g

    def numerator(self):
        return self._num

    def denominator(self):
        return self._denom

math.gcd reduces the fraction to lowest terms. We force the denominator to always be positive so -3/5 is stored as (-3, 5) rather than (3, -5).

String Representation#

def __str__(self):
    if self._denom == 1:
        return str(self._num)
    return f"{self._num}/{self._denom}"

def __repr__(self):
    return f"Rational({self._num}, {self._denom})"

Arithmetic Operators#

Python calls __add__ when + is used between two Rational objects:

def __add__(self, other):
    return Rational(
        self._num * other._denom + other._num * self._denom,
        self._denom * other._denom
    )

def __sub__(self, other):
    return Rational(
        self._num * other._denom - other._num * self._denom,
        self._denom * other._denom
    )

def __mul__(self, other):
    return Rational(self._num * other._num,
                    self._denom * other._denom)

def __truediv__(self, other):
    return Rational(self._num * other._denom,
                    self._denom * other._num)

Comparison Operators#

def __eq__(self, other):
    return self._num == other._num and self._denom == other._denom

def __lt__(self, other):
    return self._num * other._denom < other._num * self._denom

def __le__(self, other):
    return self == other or self < other

Conversion Methods#

def __float__(self):
    return self._num / self._denom

Putting It Together#

f = Rational(6, -10)
h = Rational(1, 2)

print(f)              # -3/5  (normalised automatically)
print(f + h)          # -1/10
print(f * h)          # -3/10
print(h > f)          # True
print(float(f))       # -0.6

Output:

-3/5
-1/10
-3/10
True
-0.6

Python dispatches f + h to f.__add__(h) and h > f to h.__gt__(f) (derived automatically from __lt__ and __eq__ when Python can’t find __gt__ directly).

Static Parse Method#

A class method acts on the class itself rather than an instance — Python’s replacement for C#’s static Parse:

@classmethod
def parse(cls, s):
    if "/" in s:
        num, denom = s.split("/")
        return cls(int(num), int(denom))
    if "." in s:
        digits_after = len(s.split(".")[1])
        value = int(s.replace(".", ""))
        return cls(value, 10 ** digits_after)
    return cls(int(s))
print(Rational.parse("-12/30"))   # -2/5
print(Rational.parse("1.125"))    # 9/8
print(Rational.parse("7"))        # 7