18 Special (or "dunder") methods

In this unit we are going to review briefly the basics of constructing classes, while paying particular attention to the "special methods" mentioned above. We have already used extensively one of these, the initialiser method __init__() that featured in almost every example of class discussed so far. Python comes with a number of built-in method names for which you can provide your own implementation. This will become clear as we go along.

18.1 Introduction

By way of example we are going to consider a class that implements rational numbers (Python already has one of these, but we'll construct our own version). We can start with:

In [140]:
class RationalNumber(object):
    pass
    # the body of this class is empty

An instance of this class or, in other words, an object of the type 'RationalNumber' is created by a statement like this:

In [141]:
a = RationalNumber()

# We can check the type of this variable 'a' by using
type(a)
Out[141]:
__main__.RationalNumber
In [142]:
#which tells us that the variable 'a' is of the type 'RationalNumber'

We have generated an object of the type 'RationalNumber', but there is no data associated with this object yet. Furthermore, there are no functions inside this class to perform operations on the data.

A rational number is essentially a fraction, which has a numerator and a denominator. We can use these as the attributes (i.e., data) of our class:

In [25]:
# a simple-minded implementation of a class for rational numbers:
#

class RationalNumber(object):
    def __init__(self, numerator, denominator):
        self.numerator = numerator
        self.denominator = denominator
    
    def pprint(self):
        print("DATA STORED:")
        print("\tNumerator: {}".format(self.numerator))
        print("\tDenominator: {}".format(self.denominator))
        print("---------------------------")

The first function introduced above __init__() is the constructor of our class; it allows us to initialise objects that have specific values for their attributes. The name of this function is recognised by the Python interpreter because of the double underscores before and after the name 'init' (such methods are sometimes referred to as 'dunder methods'). The second function pprint is simply used to display the data contained in a particular object. To test this class we can use the following lines of code:

In [26]:
# Create two instances:
# Note that we pass specific numeric values to create our objects
a1 = RationalNumber(3, 4)
a2 = RationalNumber(7, 11)

# Display on the screen the data stored 
# in the objects a1 and a2:
a1.pprint() 
a2.pprint()
DATA STORED:
	Numerator: 3
	Denominator: 4
---------------------------
DATA STORED:
	Numerator: 7
	Denominator: 11
---------------------------

New objects of the type 'RationalNumber' are created by using the class name as if it was a function. The statement 'a1 = RationalNumber(3, 4)' does two things: (i) it first creates an empty object a1; (ii) and then applies the function __init__ to it, i.e. the statement a1.__init__(3, 4) is executed.

The first parameter of __init__() refers to the new object itself. On function call this first parameter is replaced by the object's instance. This rule applies to all methods of the class, not just to the special method __init__(). By convention, this first parameter is named 'self'.

In the above example the function __init__() defines two attributes of the new object, 'numerator' and 'denominator'. The two functions inside the class, '__init__()' and 'pprint()', are known as the methods of the instance (the first argument of such functions is always the variable 'self').

18.2 Special (or "Magic") Methods

In the above example we defined the method 'pprint' to display on the screen the values of the attributes stored in objects generated with our 'RationalNumber' class. While this is perfectly acceptable, it would be more natural if we can just type something like 'print(a1)' in order to produce the same effect (i.e., we would like to use the standard 'print' function). It turns out that in Python one can define and implement methods that will be called automatically when a standard Python syntax is invoked. This allows for a more natural use of objects than calling methods by name. Such special methods start and end with a double underscore (you should never do this to name your own methods).

The __str__() special method:

In [27]:
# a class for rational numbers with PRINTABLE objects:

class RationalNumber(object):
    def __init__(self, numerator, denominator):
        self.numerator = numerator
        self.denominator = denominator
    
    def __str__ (self):
        s = "\nThe fraction is: "
        s +=  "{}/{}".format(self.numerator, self.denominator)
        return s
    
# testing:

a3 = RationalNumber(2, 17)
print(a3)
The fraction is: 2/17

Note that there are no print statements inside the __str__() method; we simply return a string which contains the information that we want to be displayed on the screen. When we call the standard 'print' function on an object from our class, Python automatically passes the string returned by the __str__() method.

In [28]:
# unfortunately, this maight not be what we want....
# try, for example, typing the name of the object on the command line 
# and then press 'ENTER'

a3
Out[28]:
<__main__.RationalNumber at 0x22851c33208>

We can sort this out by using another special method:

the __repr__() special method:

In [29]:
class RationalNumber(object):
    def __init__(self, numerator, denominator):
        self.numerator = numerator
        self.denominator = denominator
    
    def __repr__(self):
        return "\n{}/{}".format(self.numerator, self.denominator)
    
# testing:

a4 = RationalNumber(21, 13)
a4   # we can get the fraction by simply typing its name
     # as it is typically the case with most variables in Python
Out[29]:
21/13

Next, let's say we want our class 'RationalNumber' to contain a method that would allow us to add two rational numbers. Recall that

$$ \frac{p_1}{q_1} + \frac{p_2}{q_2} = \frac{p_1q_2 + p_2q_1}{q_1q_2}\,, $$
which suggests that our method will have to return another object of the type 'RationalNumber' in which numerator $= p_1q_2 + p_2q_1$ and denominator $=q_1q_2$.

We would also like to be able to add a fraction to an integer; if we are to use the same formula, we'll have to set numerator = (our int) number and denominator $= 1$.

In [30]:
from __future__ import print_function

class RationalNumber(object):
    def __init__(self, numerator, denominator):
        self.numerator = numerator
        self.denominator = denominator
    
    def __repr__(self):
        return "{}/{}".format(self.numerator, self.denominator)

    def add(self, other):
        p1, q1 = self.numerator, self.denominator
        # Check first to see if the other number is an integer:
        if isinstance(other, int):
            p2, q2 = other, 1
        # If the other number is also a proper RationalNumber:    
        else:
            p2, q2 = other.numerator, other.denominator
        return RationalNumber(p1*q2 + p2*q1, q1*q2)
    
# testing
b1 = RationalNumber(2, 3)
b2 = RationalNumber(1, 6)
b3 = 2

res1 = b1.add(b2) # b1+b2
res2 = b1.add(b3) # b1+b3
print('\nb1 + b2 = ', res1); print('\nb1 + b3 = ', res2)
b1 + b2 =  15/18

b1 + b3 =  8/3

So, it seems that our code does the job, but it would be much nicer if we could just write $c_1+c_2$, where $c_1$ and $c_2$ are objects from 'RationalNumber' class. At the moment we can't do this because the plus sign is not defined for elements from our class. Python provides a special method that once it is included in our class (and properly implemented) will remove this limitation; this special method is called __add__(). We have already done all the hard work, the only thing left to do is change the name of the method.

The __add__() special method:

In [31]:
# now, the rational numbers can be added in the usual way
# thanks to the __add__ method that emulates the behaviour of built-in numbers 

class RationalNumber(object):
    def __init__(self, numerator, denominator):
        self.numerator = numerator
        self.denominator = denominator
    
    def __repr__(self):
        return "{}/{}".format(self.numerator, self.denominator)

    def __add__(self, other):
        p1, q1 = self.numerator, self.denominator
        # Check first to see if the other number is an integer:
        if isinstance(other, int):
            p2, q2 = other, 1
        # If the other number is also a proper RationalNumber:    
        else:
            p2, q2 = other.numerator, other.denominator
        return RationalNumber(p1*q2 + p2*q1, q1*q2)
    
# testing:
b1 = RationalNumber(2, 3)
b2 = RationalNumber(1, 6)
b3 = 2

res1 = b1 + b2 
res2 = b1 + b3
print('\nb1 + b2 = ', res1) 
print('\nb1 + b3 = ', res2)
b1 + b2 =  15/18

b1 + b3 =  8/3

The way this works: Python recognizes certain words enclosed by double underscores. So, in this case, Python translates b1+b2 into b1.__add__(b2) and b1+b3 into b1.__add__(b3); this is true for all the other methods discussed below. You can try using this syntax to convince yourself that there is no "magic" involved.

There are many other special methods that can be added to a class -- for a complete list see the official documentation.

Of course, not all the methods that you'll find there will be relevant to this particular class we are working with.

Another method that we can consider including in this class is for testing the equality of two rational numbers from the class 'RationalNumber'. Before we can test to see if two objects from our class are equal to each other, we must define what is meant by that:

$$ \dfrac{p_1}{q_1} = \dfrac{p_2}{q_2}\qquad\mbox{if}\qquad p_1q_2 = p_2q_1 $$

The __eq__() special method:

In [32]:
# extends the class immediately above
# includes a dunder method for testing equality

class RationalNumber(object):
    def __init__(self, numerator, denominator):
        self.numerator = numerator
        self.denominator = denominator
    
    def __repr__(self):
        return "{}/{}".format(self.numerator, self.denominator)

    def __add__(self, other):
        p1, q1 = self.numerator, self.denominator
        # Check first to see if the other number is an integer:
        if isinstance(other, int):
            p2, q2 = other, 1
        # If the other number is also a proper RationalNumber:    
        else:
            p2, q2 = other.numerator, other.denominator
        return RationalNumber(p1*q2 + p2*q1, q1*q2)
    
    def __eq__(self, other):
        return self.denominator*other.numerator ==\
            self.numerator*other.denominator

        
# testing:
p = RationalNumber(1, 2)
q = RationalNumber(2, 4)

if p == q:
    print('\nNumbers are equal')
else:
    print('\nNumbers are not equal')        
Numbers are equal

Although we have defined the addition of two objects from the class 'RationalNumber', sometimes we might want to add integers to objects from our class. That will not always work, as can be seen from the examples included below:

In [33]:
p + 5
Out[33]:
11/2
In [34]:
5 + p
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-34-f7fa6cdf0624> in <module>()
----> 1 5 + p

TypeError: unsupported operand type(s) for +: 'int' and 'RationalNumber'

What happened? By default the "$+$" operator invokes the left operand's method __add__(). We implemented it so that it allows both objects of type 'int' and objects of type 'RationaNumber'. In the statement $5+p$ the operands have swapped places and the __add__() method of the built-in type 'int' is invoked. This method returns an error as it does not know how to handle our rational numbers.

We can salvage the class by equipping it with the built-in method __radd__() (read 'reverse addition').

In [54]:
# extends the class immediately above
# now it can handle both: 
#           5 + p      and       p +5

class RationalNumber(object):
    def __init__(self, numerator, denominator):
        self.numerator = numerator
        self.denominator = denominator
    
    def __repr__(self):
        return "{}/{}".format(self.numerator, self.denominator)

    def __add__(self, other):
        p1, q1 = self.numerator, self.denominator
        # Check first to see if the other number is an integer:
        if isinstance(other, int):
            p2, q2 = other, 1
        # If the other number is also a proper RationalNumber:    
        else:
            p2, q2 = other.numerator, other.denominator
        return RationalNumber(p1*q2 + p2*q1, q1*q2)
    
    def __eq__(self, other):
        return self.denominator*other.numerator ==\
            self.numerator*other.denominator
    
    def __radd__(self, other):
        return self + other
    
# testing:
p = RationalNumber(1, 2)
In [21]:
5 + p
Out[21]:
11/2
In [22]:
p + 5
Out[22]:
11/2

Note that __radd__() interchanges the order of the arguments: 'self' is the object of type 'RationalNumber', while 'other' is the object that has to be converted.

In the table included below you'll find the special methods for those binary operators, such as $+$, $-$, or $*$. The implementation of those operators for a new class is called operator overloading (and is, of course, related to polymorphism). You will find a complete list at

https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types




18.3 A final example: the 'Vector2D' class

As you may recall from school, a vector in the plane is a quantity that is defined by a magnitude (a positive number) and a direction. If a Cartesian system of coordinates is considered, any vector $\boldsymbol{a}$ can be represented in terms of two unit vectors $\boldsymbol{i}$ and $\boldsymbol{j}$ that specify the directions of the x- and y-axes. That is,

$$ \boldsymbol{a} = a_1\boldsymbol{i} + a_2\,\boldsymbol{j}\,, $$

where the real numbers $a_1$ and $a_2$ are called the components of the vector $\boldsymbol{a}$. Thus, a vector in 2D can be represented as an ordered pair of real numbers $(a_1, a_2)$. The magnitude of such a vector is denoted by $|\boldsymbol{a}|$ and is calculated using the Pythagorean theorem:

$$ |\boldsymbol{a}| = \sqrt{a_1^2 + a_2^2}\,. $$

The so-called scalar product of two vectors $\boldsymbol{a}$ and $\boldsymbol{b}$ is by definition

$$ \boldsymbol{a}\cdot\boldsymbol{b} = |\boldsymbol{a}||\boldsymbol{b}|\cos\theta\,, $$

where $\theta$ is the angle between the two vectors. Using the component form of the two vectors this operation can be also expressed as

$$ \boldsymbol{a}\cdot\boldsymbol{b} = a_1b_1 + a_2b_2\,, $$

where $\boldsymbol{a} =(a_1, a_2)$ and $\boldsymbol{b} = (b_1, b_2)$.

Given two points in the plane, $A(x_A, y_A)$ and $B(x_B, y_B)$, the vector corresponding to the directed line segment $AB$, denoted by $\overrightarrow{AB}$, is going to have the representation:

$$ \overrightarrow{AB} = (x_B-x_A)\,\boldsymbol{i} + (y_B-y_A)\,\boldsymbol{j}\,. $$

The 'Vector2D' class included below implements the above concepts for planar (2D) vectors. The problem sheet for this week contains some questions that will require you to make use of this class.

In [51]:
# class for vectors in the plane

import math

class Vector2D(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y
        
    def __add__(self, other):
        a = self.x + other.x
        b = self.y + other.y
        return Vector2D(a, b)

    def __sub__(self, other):
        a = self.x - other.x
        b = self.y - other.y
        return Vector2D(a, b)
                
    def __mul__(self, other):
        return self.x*other.x + self.y*other.y 
    
    def __abs__(self):
        return math.sqrt(self.x**2 + self.y**2)
        
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y
        
    def __str__(self):
        return ('(%g, %g)' %(self.x, self.y))
    
    def __ne__(self, other):
        return not self.__eq__(other)        
In [44]:
# short demo for the above class

a = Vector2D(3, 4)
print('\nMagnitude of vector is: ', abs(a))
Magnitude of vector is:  5.0
In [47]:
b = Vector2D(-1, 6)
print('\nScalar product of \'a\' and \'b\' is: ', a*b)
Scalar product of 'a' and 'b' is:  21
In [48]:
c = Vector2D(-1, 6)
b == c
Out[48]:
True
In [53]:
sum = a + b
print('\nSum of the two vectors: ', sum)

diff = a - b
print('\nDifference of the two vectors: ', diff)
Sum of the two vectors:  (2, 10)

Difference of the two vectors:  (4, -2)