Python data model revisited

shaikhul

Shaikhul Islam

Posted on February 17, 2019

Python data model revisited

Python's double underscore (aka dunder) methods (ex. __len__) aren't magic method but more than that. Each of python's built-in functions has a corresponding dunder method. Officially they are documented under python data model. Try google python data model, first result would point to python docs

So whats the big deal? Ever wonder why python's len method accept almost anything?

>>> len(range(5))
5
Enter fullscreen mode Exit fullscreen mode

Now imagine a simple class like this

>>> class Foo:
...     pass
...
Enter fullscreen mode Exit fullscreen mode

What would it return if I provide a Foo instance to len method?

>>> f = Foo()
>>> len(f)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: object of type 'Foo' has no len()
Enter fullscreen mode Exit fullscreen mode

It throws an exception TypeError complaining Foo has no len. Right, if we implement a __len__ on Foo object and return anything, python's len will look for a corresponding __len__ method for that object.

>>> class Foo:
...     def __len__(self):
...             return 5
...
>>> f = Foo()
>>> len(f)
5
Enter fullscreen mode Exit fullscreen mode

boom! now it works and return what we want it to return by implementing the __len__ method.

Other most common dunder methods are

String representation

In [11]: class Point:
    ...:     def __init__(self, x, y):
    ...:         self.x = x
    ...:         self.y = y
    ...:
    ...:     def __str__(self):
    ...:         return 'Stringified Point: ({x}, {y})'.format(x=self.x, y=self.y)
    ...:
    ...:     def __repr__(self):
    ...:         return 'Representing Point: ({x}, {y})'.format(x=self.x, y=self.y)
    ...:

In [12]: p = Point(2,3)

In [13]: p
Out[13]: Representing Point: (2, 3)

In [14]: str(p)
Out[14]: 'Stringified Point: (2, 3)'
Enter fullscreen mode Exit fullscreen mode

Operator overloading

We can implement binary arithmetic operations (ex +, - etc) implementing specific dunder methods.

In [21]: class Point:
    ...:     # continuing from previous example   
    ...:     def __add__(self, other):
    ...:         return Point(self.x + other.x, self.y + other.y)
    ...:

In [22]: p1 = Point(1, 2)

In [23]: p2 = Point(3,5)

In [26]: p3
Out[26]: Representing Point: (4, 7)
Enter fullscreen mode Exit fullscreen mode

To learn more about emulating numeric types checkout the doc

Object comparison

We can implement __eq__ to compare between objects.

In [36]: class Point:
    ...:     # continuing from previous example
    ...:     def __eq__(self, other):
    ...:         # custom comparison
    ...:         return self.x == other.x and self.y == other.y
    ...:
In [37]: p1 = Point(2, 3)

In [38]: p2 = Point(2, 3)

In [39]: p1 == p2
Out[39]: True

In [40]: p3 = Point(2, 4)

In [41]: p1 == p3
Out[41]: False
Enter fullscreen mode Exit fullscreen mode

To check other object customization please check the doc

Object attribute access

In [44]: class Point:
    ...:     # continuing from previous example
    ...:     def __getattr__(self, attr):
    ...:         if (attr == 'X'):
    ...:             return self.x
    ...:         elif (attr == 'Y'):
    ...:             return self.y
    ...:         else:
    ...:             raise AttributeError('Point object has no attribute {attr}'.format(attr=attr))
    ...:
In [45]: p = Point(2, 3)

In [46]: p.x
Out[46]: 2

In [47]: p.X
Out[47]: 2

In [48]: p.Xx
--------------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-48-9a49f22e192d> in <module>
---------> 1 p.Xx

<ipython-input-44-f5f17ce1e8d6> in __getattr__(self, attr)
     23             return self.y
     24         else:
--------> 25             raise AttributeError('Point object has no attribute {attr}'.format(attr=attr))
     26

AttributeError: Point object has no attribute Xx
Enter fullscreen mode Exit fullscreen mode

More attribute access can be found in the doc

Emulating container/collection

We can also treat objects as collection and do all sort of function call that accept a collection.

In [49]: class Point:
    ...:     # continuing from previous example
    ...:     def __getitem__(self, index):
    ...:         if index == 0:
    ...:             return self.x
    ...:         elif index == 1:
    ...:             return self.y
    ...:         else:
    ...:             raise IndexError('No item found with index {index}'.format(index=index))
    ...:
In [50]: p = Point(2, 3)

In [51]: p[0]
Out[51]: 2

In [53]: p[1]
Out[53]: 3

In [54]: p[2]
--------------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-54-21c545b39f61> in <module>
---------> 1 p[2]

<ipython-input-49-6dd2485f38f5> in __getitem__(self, index)
     31             return self.y
     32         else:
--------> 33             raise IndexError('No item found with index {index}'.format(index=index))
     34

IndexError: No item found with index 2
Enter fullscreen mode Exit fullscreen mode

Checkout the doc for whole list of dunder methods to emulate collection.

The best thing of python data model is that programmers hardly need to call the dunder method (except __init__ and few other customization methods), python interpreter will call these methods as needed.

I have put the complete Point class in this gist, feel free to play with all other dunder methods and share what you have learned with us.

💖 💪 🙅 🚩
shaikhul
Shaikhul Islam

Posted on February 17, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related