Special methods

Arithmetical operations

Let’s go back to our Fraction class:

class Fraction:
    def __init__(self, numer, denom):
        self.numer = numer
        self.denom = denom

    def mul(self, other):
        return Fraction(self.numer * other.numer, self.denom * other.denom)

Such formulation allows us to multiply two fractions in an ugly way c = a.mul(b). It would be much more clear if we were able to write c = a * b. In fact we can. Let’s rename the method mul to __mul__ (two underscored in the beginning and the end, like in __init__):

class Fraction:
    def __init__(self, numer, denom):
        self.numer = numer
        self.denom = denom

    def __mul__(self, other):
        return Fraction(self.numer * other.numer, self.denom * other.denom)

Now we can write:

x = Fraction(1, 2)
y = Fraction(5, 6)

z = x * y

Methods with two underscores in the beginning and the end are special methods. Generally you never invoke them directly. For example, you never call the __init__ method explicitly. It is automatically invoked on instance creation. __mul__ is another such method and it is called when two objects are multiplied.

There are many such methods. You can find the list of all of them in the documentation. Here are the most basic ones used for arithmetics:

You want… So you write… And the special function is…
addition x + y x.__add__(y)
subtraction x - y x.__sub__(y)
multiplication x * y x.__mul__(y)
division x / y x.__truediv__(y)
modulo (remainder) x % y x.__mod__(y)
raise to power x ** y x.__pow__(y)

Please implement __add__, __sub__ and __truediv__ for the Fraction class.

Pretty-printing objects

Other special methods allow to access “elements” of our class using square brackets (like for lists or dictionaries) (__getitem__ and __setitem__), getting the “length” of our custom sequence (__len__), etc.

An important special method you should always implement is __str__. Consider the following code:

class Fraction:
    def __init__(self, numer, denom):
        self.numer = numer
        self.denom = denom

frac = Fraction(1, 2)

print(frac)

If you run it, the output will be something like:

<__main__.Fraction object at 0x7f61ad175b80>

This is not very useful. Luckily, the __str__ method can be used to convert the class into a string that can be printed:

class Fraction:
    def __init__(self, numer, denom):
        self.numer = numer
        self.denom = denom

    def __str__(self):
        # This method must RETURN a string. Do not try to print anything!
        return f"{self.numer}/{self.denom}"

frac = Fraction(1, 2)

print("Our fraction is:", frac)

This time the output looks much better:

Our fraction is: 1/2

Object comparison

Some other special functions that are very useful are comparisons. Fractions can be equal (if both numerators and denominators equal) or one can be larger than another one. Special functions doing comparisons should always return True or False. Their names are as follow:

Comparison Special function
x < y x.__lt__(self, y)
x <= y x.__le__(self, y)
x == y x.__eq__(self, y)
x != y x.__ne__(self, y)
x > y x.__gt__(self, y)
x >= y x.__ge__(self, y)

Note: Fraction reduction

When you implement arithmetic operation on fractions, you may end up with a reducible fraction, e.g. 2/3 × 3/5 = 6/15. Naturally, this fraction is equal to 2/5.

In order to have your class behave elegantly and for comparisons to work, you should always reduce the fraction by dividing both the numerator and denominator by their greatest common divisor, which can be found using the function gcd in the math module. In which method you should do it? If you do this in every special method responsible for mathematical operation, you will need to do it several times. Furthermore, if someone creates an instance of you class e.g. as Fraction(4, 6), the fraction will not be reduced. However if you make the reduction in the constructor, this will cover all use cases:

from math import gcd

class Fraction:
    def __init__(self, numer, denom):
        r = gcd(numer, denom)
        self.numer = numer // r      # we use // for integer division
        self.denom = denom // r

    def __str__(self):
        return f"{self.numer}/{self.denom}"

    def __mul__(self, other):
        return Fraction(self.numer * other.numer, self.denom * other.denom)

Also in the constructor you can do more sanitizations, like checking if the denominator is not 0 (and raising ValueError in such case), making sure that the denominator is always positive (and the sign of the numerator is adjusted accordingly), etc.


Published under Creative Commons Attribution-NonCommercial-ShareAlike license.