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))

    def __add__(self, other):
        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 deck[0] == Card(rank='2', suit='spades')
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