# Using dunder methods to refine your data model

## Introduction

Practically everyone who has ever used Python came across at least one of the so-called Python *magic* methods.
**Dunder** methods, as they also called that way, are Python’s special functions that allow users to **hook into** some specific actions being performed.
Probably the most frequently encountered one is the `__init__`

method.
It is called when instantiating a new object from a class and by overriding it, we can gain control over that process.

However, this post is **not** going to take you through a full list of these.

Instead, we will show how you can effectively use this great Python feature by telling of a short *story*.
We will use *quaternions* as an example to explain the proces of creating of our **data model**
that is easy to handle for other developers, especially those less enthusiastic about advanced algebra.
Most importantly, we will explain **the decision process** and argue why it **makes sense** to even bother.

## Simple object

A quaternion is an algebraic concept often used for describing *rotations* and widely applied in 3D modeling and gaming.
Conceptually, quaternions can be thought of as an extension of *complex* numbers body, having not one, but three imaginary parts.
Depending on the application, they are can also be understood as quotients of three-dimensional vectors or four-dimensional objects or scalar-vector pairs.

OK, but how do we code this thing?

### Instantiation

From the programming point of view, we do not need to focus that deep into math.
At this stage, all we need to know is that one quaternion is defined by *four real numbers*.

#### __int__

1
2
3
4
5
6

class Quaternion:
def __init__(self, w, x, y, z):
self.w = w
self.x = x
self.y = y
self.z = z

We model our mathematical “being” as an object, and we have our first dunder method.
All this code does is to tell Python: “look, when you create a new object of class *Quaternion*, I will need four numbers from you to instantiate it.
Since every quaternion is different, it makes sense to define `w`

, `x`

, `y`

and `z`

as object attributes instead of class properties.

### Representation

Let’s create our first quaternion.

Our quaternion is an object, but it looks pretty ugly. By default, we see an address of where that object lives in memory, but that description tells us nothing about the qualities we are interested in.

#### __repr__, __str__

1
2
3
4
5
6
7

def __repr__(self):
return "Quaternion({}, {}, {}, {})".format(
self.w, self.x, self.y, self.z)
def __str__(self):
return "Q = {:.2f} + {:.2f}i + {:.2f}j + {:.2f}k".format(
self.w, self.x, self.y, self.z)

Here, we have defined two more methods.
The `__repr__`

method is an “official” representation of the object,
and here with this quality that `eval(repr(obj)) == obj`

.

Good.
The `__repr__`

method returns a string that is descriptive enough.
However, we can further enhance our representation with `__str__`

.
The output will be as follows:

## Performing algebraic operations

You may wonder, at this point, why not using a *list* or a *dictionary*?
It is certainly less code and we can easily see the elements.

Well, we indeed need something more than just a “bag of numbers”. There are two main arguments against it:

- We don’t want to
**rely on convention**. Is`w`

always going to be named “w” and used as the first argument? What if someone breaks it? - We define this object to
**reflect upon the mathematical properties it is designed to represent.**

Pretty tough, right?
Apart from 1., quaternions are *additive*.
Try adding dictionaries or lists together… one will result in `TypeError`

, while the other will extend the number of elements, thus breaking our definition.
There is another way.

### Addition

#### __add__

1
2
3
4
5
6

def __add__(self, other):
w = self.w + other.w
x = self.x + other.x
y = self.y + other.y
z = self.z + other.z
return Quaternion(w, x, y, z)

There we have it.
We have just overridden the `+`

operator, making the addition of quaternions defined.

### Subtraction

#### __sub__

The same we can do with subtracting. This time we will be fancy and do it in just one line of code.

1
2

def __sub__(self, other):
return Quaternion(*list(map(lambda i, j: i - j, self.__dict__.values(), other.__dict__.values())))

Although that was unnecessary, it also shows another convenient dunder method.
The `__dict__`

method collects all the attributes of an object and returns them as a dictionary.

### Multiplication

If you still think that overriding of operations is boring, now it is time for fun.

#### __matmul__

The easiest of all is the *dot product* (see this gist for more methods).

Represented with `@`

, since Python 3.5, it invokes `__matmul__`

method, which for quaternions, is defined as a simple element-wise multiplication.

The “normal” multiplication is harder though.
First, the algebra distinguishes between quaternion times quaternion multiplication and quaternion times scalar multiplication.
Secondly, quaternion-by-quaternion multiplication is **not commutative**, meaning that .

#### __mul__

1
2
3
4
5
6
7
8
9
10
11

def __mul__(self, other):
if isinstance(other, Quaternion):
w = self.w * other.w - self.x * other.x - self.y * other.y - self.z * other.z
x = self.w * other.x + self.x * other.w + self.y * other.z - self.z * other.y
y = self.w * other.y + self.y * other.w + self.z * other.x - self.x * other.z
z = self.w * other.z + self.z * other.w + self.x * other.y - self.y * other.x
return Quaternion(w, x, y, z)
elif isinstance(other, (int, float)):
return Quaternion(*[other * i for i in self.__dict__.values()])
else:
raise TypeError("Operation undefined.")

Here, if the `other`

is a quaternion, we compute the so-called Hamilton product and return a new object.
If the `other`

is a scalar (a number), we multiply each of the quaternion’s coordinates with that number.
Finally, anything else raises an exception.

As mentioned earlier, the multiplication of quaternions is not commutative.
However, that is only when multiplying quaternions by one another.
With the current definition, if we execute `2 * q1`

, we will get an error.
To fix it, we can use `__rmul__`

which covers our case:

#### __rmul__

1
2
3
4
5

def __rmul__(self, other):
if isinstance(other, (int, float)):
return self.__mul__(other)
else:
raise TypeError("Operation undefined.")

Now, we can multiply a quaternion by a scalar on both sides, while quaternion *can multiply another quaternion* in a strictly defined order.

### Equality

We will skip the *division* as it follows in the same pattern.
Instead look at one more curiosity: equality.

What does it mean that two quaternions are actually equal? Is it when all components are pair-wise equal or perhaps when two objects represent the same truth?

We can go for any of these definitions… however, the very fact that we **asked this question to ourselves, justifies overriding one more method.**

#### __eq__

1
2
3

def __eq__(self, other):
r = list(map(lambda i, j: abs(i) == abs(j), self.__dict__.values(), other.__dict__.values()))
return sum(r) == len(r)

Here we defined our `==`

as a case where all coordinates’ absolute values having to match.

### Other operations

Python defines a list of operators that can be overridden. However, not every mathematical operation is represented in the dunder methods. In these cases, it is better to stick to “normal” methods, since the usage of other symbols would be counterintuitive.

For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

from math import sqrt
def norm(self):
return sqrt(sum([i**2 for i in self.__dict__.values()))
def conjugate(self):
x, y, z = -self.x, -self.y, -self.z
return Quaterion(self.w, x, y, z)
def normalize(self):
norm = self.norm()
return Quaternion(*[i / norm for in self.__dict__.values()])
def inverse(self):
qconj = self.conjugate()
norm = self.norm()
return Quaternion(*[i / norm for i in qconj.__dict__.values()])

## Overriding or overloading?

Throughout this post, we were carefully watching our language.
We “wrote over” some of the dunder methods for good reason.
However, we did not perform any *overloading* of operators.
Overloading of operators does not exist in Python in a strict sense.
One method can only have one interface to it, although Python allows a variable number of arguments.

Do you remember how we instantiated our objects?
We used four numbers `w`

, `x`

, `y`

, and `z`

as arguments.
When dealing with quaternions, however, it is common to derive them from *yaw, pitch* and *roll* angles, which are closely related to Euler angles.

The question arises, how do we go about them programmatically?
Do we extend our `__init__`

method’s interface to accept seven numbers?
Is it better to make some of them optional?
If yes, then how do we **ensure the integrity** of our object?
What price do we need to pay in terms of the code quality?

Speaking of quaternions, we do have an opportunity to implement something *close to overloading*,
making our code even cleaner.

### Pythonic “overloading”

Since all operations, as we saw them, involve `w, x, y, z`

variables, there is no point in adding any more attributes to our class.
What we must do, however, is to have an option to *bypass* the constructor’s interface with something that takes `yaw`

, `pitch`

, and `roll`

converts them to `(w, x, y, z)`

and instantiates of a new object.

First, let’s create the re-calculation method:

1
2
3
4
5
6
7
8
9
10
11
12
13

from math import sin, cos
def _ypr_to_coords(yaw, pitch, roll):
y = 0.5 * yaw
p = 0.5 * pitch
r = 0.5 * roll
w = cos(y) * cos(p) * cos(r) + sin(y) * sin(p) * sin(r)
x = cos(y) * cos(p) * sin(r) - sin(y) * sin(p) * cos(r)
y = sin(y) * cos(p) * sin(r) + cos(y) * sin(p) * cos(r)
z = sin(y) * cos(p) * cos(r) - cos(y) * sin(p) * sin(r)
return w, x, y, z

The method is *protected* in the sense that it is “internal” to the class.
It also does not perform any operations over the object.
It only recalculates the angles returns the coordinates.

Next, we use it as a part of our `__init__`

’s second face.

1
2
3
4
5
6
7
8
9
10
11

class Quaternion:
def __init__(self, w, x, y, z):
self.w = w
self.x = x
self.y = y
self.z = z
@classmethod
def create_from_ypr(cls, yaw, pitch, roll):
r = cls._ypr_to_coords(yaw, pitch, roll)
return cls(*r)

Without affecting the `__init__`

or the attributes, we have now another way to instantiate our quaternion.
With `@classmethod`

decorator, we appoint `create_from_ypr(...)`

method to be a class method rather than an object method.
When invoked on a class, it recalculates our coordinates and returns the class itself (through former `__init__`

) feeding the necessary arguments in.

This trick allows us to stay true to our definition, but adds more flexibility. We can even use this approach to define special kind of objects:

1
2
3
4
5
6

class Quaternion:
...
@classmethod
def create_identity(cls):
return cls(1, 0, 0, 0)

## Conclusions

In this post, we have presented a pattern behind using some of Python’s special features known as dunder methods.
We have given an example of how these methods can be harnessed to model an abstract algebraic object, namely a quaternion.
We have also made a clear distinction between *overriding* and *overloading* and shown how the latter can be implemented to facilitate working with our objects.

To see more methods, take a look at this gist. If there is anything to improve, please give the feedback in the comments below! Thanks ;)