Chapter 1: Data Model
Introduction about what "Pythonic" means.
Pythonic Card Deck
To undertant how python works as a framework it is crutial that you get the Python Data Model. Python is very consistent and by that I mean that once you have some experince with the language you can start to correctly make informed guesses on other features about python even if its new. This will help you make your objects more pythonic by leveraging the options python has for:
- Iteration
- Collections
- Attribute access
- Operator overloading
- Function and method invocation
- Object creation and destruction
- String representation and formatting
- Managed contexts (i.e., with blocks)
Studing these will give you the power to make your own python object play nicely with the python language and use many of the freatures mentioned above. In short makes you code "pythonic".
Let see an example to show you the power of __getitem__
and __len__
.
import collections
# namedtuple - tuples with names for each value in it (much like a class)
Card = collections.namedtuple('Card', ['rank', 'suit'])
c = Card('7', 'diamonds')
# individual card object
print(c)
print(c.rank, c.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]
deck = FrenchDeck()
# with this simple class, we can already use `len` and `__getitem__`
len(deck), deck[0]
Now we have created a class FrenchDeck that is short but still packs a punch. All the basic operations are supported. Now imagine we have another usecase to pick a random card. Normally we would add another function but in this case we can use pythons existing lib function random.choice()
.
from random import choice
choice(deck)
We’ve just seen two advantages of using special methods to leverage the Python data model:> 1. The users of your classes don’t have to memorize arbitrary method names for stan‐dard operations (“How to get the number of items? Is it .size() , .length() , or what?”).
- It’s easier to benefit from the rich Python standard library and avoid reinventing the wheel, like the random.choice function.
But we have even more features
deck[1:5]
for card in deck:
if card.rank == 'K':
print(card)
# the in operator does a sequential scan.
Card('Q', 'spades') in deck
Card('M', 'spades') in deck
we can also make use the build-in sorted()
function. We just need to proved a function for providing the values of the cards. Here the logic is provided in spedes_high
suit_value = dict(spades=3, hearts=2, diamonds=1, clubs=0)
def spades_high(card):
rank_value = FrenchDeck.ranks.index(card.rank)
return rank_value*len(suit_value) + suit_value[card.suit]
for card in sorted(deck, key=spades_high)[:10]:
print(card)
Although FrenchDeck implicitly inherits from object its functionality is not inherited, but comes from leveraging the data model and composition. By implementing the special methods
__len__
and__getitem__
, our FrenchDeck behaves like a standard Python sequence, allowing it to benefit from core language features (e.g., iteration and slicing). and from the standard library, as shown by the examples using random.choice , reversed , and sorted . Thanks to composition, the__len__
and__getitem__
imple‐ mentations can hand off all the work to a list object,self._cards
.
How special methods are used
Normally you just define these special methods and call them via the inbuild methods like len()
in
[index]
instead of calling it via object.__len__()
. This gives you speed up in some cases and also plays nicely with other other python library functions since they all are now interfacing with the same endpoints.
from math import hypot
class Vector:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __repr__(self):
return 'Vector(%d, %d)' %(self.x, self.y)
def __abs__(self):
return hypot(self.x, self.y)
def __bool__(self):
return bool(self.x or self.y)
def __add__(self, other):
x = self.x + other.x
y = self.y + other.y
return Vector(x, y)
def __mul__(self, scalar):
x = scalar * self.x
y = scalar * self.y
return Vector(x, y)
v = Vector(3, 4)
a = Vector(0, 0)
print(v)
print(abs(v))
print(v*2)
print(v + a)
As you can see we implemented many special methods but we don't directly invoke them. The special methods are to be invoked by the interpretor most of the time, unless you are doing a lot of metaprogramming.
bool(a)
String Representation
We use the __repr__
special method to get the buildin string representation of of the object for inspection (note the usage in vector
object. There are also other special methods like __repr__with__str__
which is called by str()
or __str__
which is used to return a string for display to the end user. If your only implementing 1 function stick with __repr__
since print()
will fall back to that if __str__
is not found.
class Test:
def __init__(self, x):
self.x = x
t = Test(0)
t, bool(t)
class Test:
def __init__(self, x):
self.x = x
def __bool__(self):
return bool(self.x)
t = Test(0)
t, bool(t)
Why len is Not a Method
Practicality beats purity
len
(similar to abs) in built-in data types, has a shortcut implmentation in CPython and they are just returning their length from the values defined in the c struct code. This makes it super fast for built-in data types. You can also consider these as unary operations.