ruk·si

# 🐍 Python - Python Data Model

Updated at 2018-06-11 02:33

Python data/object model is very consistent. Python data model describes API of "magical" dunder methods such as `__len__`. This is the same approach as in Ruby; there is nothing magical about it, is just syntactical sugar and well-defined protocols. In contrast, JavaScript has some magical things e.g. some build-in objects have read-only attributes.

``````from math import hypot

class Vector:

def __init__(self, x=0, y=0):
self.x = x
self.y = y

def __repr__(self):
return f'Vector({self.x!r}, {self.y!r})'

def __eq__(self, other):
return type(other) == Vector and self.x == other.x and self.y == other.y

def __abs__(self):
return hypot(self.x, self.y)

def __bool__(self):
return bool(abs(self))

x = self.x + other.x
y = self.y + other.y
return Vector(x, y)

def __mul__(self, other):
return Vector(self.x * other, self.y * other)

assert bool(Vector(1, 4))
assert not bool(Vector(0, 0))
assert Vector(2, 4) == Vector(2, 4)
assert Vector(2, 4) != Vector(3, 4)
assert Vector(2, 4) + Vector(2, 1) == Vector(4, 5)
assert abs(Vector(3, 4)) == 5
assert Vector(3, 4) * 3 == Vector(9, 12)
``````

To get the most out of Python, add relevant dunder methods for your class. Here FrenchDeck implements `__len__` and `__getitem__` methods so it works Python sequence protocol. `__getitem__` is a bit more complex to support slicing as an example, but slicing should be supported only if it makes sense for the class in question.

``````from collections import namedtuple
from typing import Sequence, Tuple, Union

Card = namedtuple('Card', ['rank', 'suit'])

class FrenchDeck:
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
suits = 'spades diamonds clubs hearts'.split()

def __init__(self, cards: Sequence[Tuple] = None) -> None:
if cards is None:
self._cards = [Card(rank, suit) for suit in self.suits
for rank in self.ranks]
else:
self._cards = list(cards)

def __len__(self) -> int:
return len(self._cards)

def __getitem__(self, index) -> Union[Tuple, 'FrenchDeck']:
cls = type(self)
if isinstance(index, slice):
return cls(self._cards[index])
elif isinstance(index, int):
return self._cards[index]
else:
raise TypeError('string indices must be integers')

deck = FrenchDeck()
assert len(deck) == 52
assert isinstance(deck[0:2], FrenchDeck)
assert len(deck[0:2]) == 2

assert Card('Q', 'hearts') in deck
assert Card('7', 'beasts') not in deck

from random import choice

assert type(choice(deck)) == Card
assert type(choice(deck)) == Card
assert type(choice(deck)) == Card

suit_values = {'spades': 3, 'hearts': 2, 'diamonds': 1, 'clubs': 0}

def card_value(card):
rank_value = FrenchDeck.ranks.index(card.rank)
return rank_value * len(suit_values) + suit_values[card.suit]

for card in sorted(deck, key=card_value):
assert type(card) is Card
``````

Your code should not have many direct calls to dunder methods. You should use the related built-in function instead.

``````# bad
my_class.__len__()

# good
len(my_class)
``````

# Sources

• Fluent Python, Luciano Ramalho