The Rational Class#

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#

    def __init__(self, numerator: int, denominator: int = 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) -> int:
        return self._num

    def denominator(self) -> int:
        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) -> str:
        if self._denom == 1:
            return str(self._num)
        return f"{self._num}/{self._denom}"

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

Arithmetic Operators#

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

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

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

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

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

Comparison Operators#

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Rational):
            return NotImplemented
        return self._num == other._num and self._denom == other._denom

    def __lt__(self, other: "Rational") -> bool:
        return self._num * other._denom < other._num * self._denom

    def __le__(self, other: "Rational") -> bool:
        return self == other or self < other

Conversion Methods#

    def __float__(self) -> float:
        return self._num / self._denom

Putting It Together#

Here we create two Rational values and exercise the arithmetic and display methods. This cell folds in the full class so you can run it and edit the numbers:

>>> import math
>>> class Rational:
...     def __init__(self, numerator: int, denominator: int = 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 __str__(self) -> str:
...         if self._denom == 1:
...             return str(self._num)
...         return f"{self._num}/{self._denom}"
...     def __add__(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 __eq__(self, other):
...         if not isinstance(other, Rational):
...             return NotImplemented
...         return self._num == other._num and self._denom == other._denom
...     def __lt__(self, other):
...         return self._num * other._denom < other._num * self._denom
...     def __float__(self) -> float:
...         return self._num / self._denom
...
>>> f = Rational(6, -10)
>>> h = Rational(1, 2)
>>> print(f)              # -3/5  (normalised automatically)
-3/5
>>> print(f + h)          # -1/10
-1/10
>>> print(f * h)          # -3/10
-3/10
>>> print(h > f)          # True
True
>>> print(float(f))       # -0.6
-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 for parsing a string acts on the class itself rather than an instance:

    @classmethod
    def parse(cls, s: str) -> "Rational":
        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