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:

  1. Iteration
  2. Collections
  3. Attribute access
  4. Operator overloading
  5. Function and method invocation
  6. Object creation and destruction
  7. String representation and formatting
  8. 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)
Card(rank='7', suit='diamonds')
7 diamonds
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]
(52, Card(rank='2', suit='spades'))

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)
Card(rank='5', suit='hearts')

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?”).

  1. 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]
[Card(rank='3', suit='spades'),
 Card(rank='4', suit='spades'),
 Card(rank='5', suit='spades'),
 Card(rank='6', suit='spades')]
for card in deck:
    if card.rank == 'K':
        print(card)
Card(rank='K', suit='spades')
Card(rank='K', suit='diamonds')
Card(rank='K', suit='clubs')
Card(rank='K', suit='hearts')
# the in operator does a sequential scan.

Card('Q', 'spades') in deck
True
Card('M', 'spades') in deck
False

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)
Card(rank='2', suit='clubs')
Card(rank='2', suit='diamonds')
Card(rank='2', suit='hearts')
Card(rank='2', suit='spades')
Card(rank='3', suit='clubs')
Card(rank='3', suit='diamonds')
Card(rank='3', suit='hearts')
Card(rank='3', suit='spades')
Card(rank='4', suit='clubs')
Card(rank='4', suit='diamonds')

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.

Enumerating Numeric Types

Special methods can also be used to repond to operators like +, - etc. We will see an example of vector operations.

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)
Vector(3, 4)
5.0
Vector(6, 8)
Vector(3, 4)

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

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.

Arithmetic Operators

In the above example we have implemented __add__ and __mul__. Note in both cases we are returning new object, reading from self, and other. This is the expected behaviour.

Boolean Value of Custom Type

In python any object can be used in a boolean context. If __bool__ or __len__ is not implemented then the object will be truthy by default. IF __bool__ is implemented that is called, if not python calls __len__ and checks if the length is 0.

class Test:
    def __init__(self, x):
        self.x = x

t = Test(0)
t, bool(t)
(<__main__.Test at 0x7fed149a53a0>, True)
class Test:
    def __init__(self, x):
        self.x = x
    
    def __bool__(self):
        return bool(self.x)
    
t = Test(0)
t, bool(t)
(<__main__.Test at 0x7fed149acd30>, False)

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.