Data Model in Python

One of the best qualities of Python is its consistency.

Luciano Ramalho, author of Fluent Python: Clear, Concise, and Effective Programming

By the first time I read this sentence, all I felt was wired. As we all know, Python is a dynamically-typed language, you can never succeed in guessing the class or type of a variable. So at least, in terms of type, Python is far from consistent. However, soon I understand the meaning of consistency in Python: Nothing is special. Here, we're going to see several examples and understand what "consistency" really means in Python.

Note: In Python, we're going to meet a lot of methods that looks like __getitem__, and we'll call them "dunder_getitem" (dunder means double-under). And also notice that, double under like __x has other meanings in Python, and please don't mess up with them.

It's very important to implement those methods that can be called by the framework itself, we're going to show you how to implement two special methods called __getitem__ and __len__.

1.1 Pythonic Cards

Code 1-1: Cards with order.

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

card = collections.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):
self._cards = [Card(rank, suit) for suit in self.suits
for rank in self.ranks]

def __len__(self):
return len(self._cards)

def __getitem__(self, position):
return self._cards[position]

Now, let's look at the FrenchDeck class. It's short and powerful. First of all, like any Python standard collection class, we can use len() function to check how many cards in it:

1
2
3
>>> deck = FrenchDeck()
>>> len(deck)
52

We can also pick any specific card in the cards using [ ] like the following code:

1
2
3
4
>>> deck[0]
Card(rank='2', suit='spades')
>>> deck[-1]
Card(rank='A', suit='hearts')

The more interesting thing is that, we don't need to write a new function to randomly pick a card, Python already had a function called random.choice to do the job, and we can just use it on the instance of our cards.

1
2
3
>>> from random import choice
>>> choice(deck)
Card(rank='4', suit='spades')

See! We already benefit from implementing special methods to use Python data model:

  • The users of your class doesn't need to remember many different names of a standard manipulation (sizeof() or length() or something else).
  • Just use Python standard library, no need for re-inventing the wheels.

Even more, since __getitem__ hands [ ] manipulation to self._cards list, so our deck class automatically supports slicing. See below:

1
2
3
4
5
6
7
8
9
>>> deck[: 3]
[Card(rank='2', suit='spades'),
Card(rank='3', suit='spades'),
Card(rank='4', suit='spades')]
>>> deck[12:: 13]
[Card(rank='A', suit='spades'),
Card(rank='A', suit='diamonds'),
Card(rank='A', suit='clubs'),
Card(rank='A', suit='hearts')]

The second function means that, pick all cards that has index 12, and pick one in every 13 cards.

And even more! Just by implementing __getitem__ method, the cards become iterable:

1
2
3
4
5
6
7
>>> for card in deck: # doctest: +ELLIPSIS
... print(card)
Card(rank='2', suit='spades')
Card(rank='3', suit='spades')
Card(rank='4', suit='spades')
Card(rank='5', suit='spades')
...v

Just by implementing __getitem__ and _\len__ methods, FrenchDeck class is just like any sequential data model in Python, showing core features of Python (like iterating and slicing). It can also be used for standard libraries like random.choice, reversed and sorted.

Here, we see how consistency exists in Python, and definitely we can write more elegant and pythonic code by implementing these special methods.

However, our cards still cannot be shuffled so far. We'll not talk about it now, although it's easy enough to be implemented by a single line of code in __setitem__, it influences more important things and we'll talk about them together.