Python Flyweights

When I wrote Equality for Python, my example didn’t mention how the Card objects could actually be a terrific waste of memory. A commenter named versimilidude (great handle!) beat me to this post, briefly describing the Flyweight Pattern. Luckily he didn’t provide example code, so I still get to publish this post.

Let’s look at the final version of the Card class and its usage again:

 
values = ('2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A')
suits = ('h', 'c', 'd', 's')
 
class Card:
    def __init__(self, value, suit):
        self.value, self.suit = value, suit
 
    def __repr__(self):
        return "<Card: %s%s>" % (self.value, self.card)
 
    def __eq__(self, card):
        return self.value == card.value and self.suit == card.suit
 
    def __ne__(self, card):
       return not self.__eq__(card)
 
>>> c1 = Card('J', 'h')
>>> c2 = Card('J', 'h')
>>> c1 == c2
True
>>> id(c1), id(c2)
(-1210559028, -1210558804)

As designed in the previous blog post, two different objects (as seen by their different ids) equate because of the custom __eq__() handler.

But this is a bit silly — Cards are so small and simple, having identical objects is wasteful. If we’re writing the code for a casino, we don’t really want several thousand Jacks of Hearts, we want many references to one object. The idea is that instantiating a Card object checks a hidden pool of objects for a duplicate and returns it, creating a new object only if needed. (Code based on that of Duncan Booth.)

 
import weakref
 
class Card(object):
    _CardPool = weakref.WeakValueDictionary()
 
    def __new__(cls, value, suit):
        obj = Card._CardPool.get(value + suit, None)
        if not obj:
            obj = object.__new__(cls)
            Card._CardPool[value + suit] = obj
 
        return obj
 
    def __init__(self, value, suit):
        self.value, self.suit = value, suit

Notice that this uses new-style classes. (Side note: I cringe at anything named “new”, “next”, “updated”, etc. because they tell me nothing. If the only thing you can say about a reimplementation is that it’s not the old one, put down your copy of Refactoring and back away from the computer. Take up writing toothpaste advertisements.) If you’re wondering what’s up with __new__, Guido explains the difference best:

__new__ is the first step in instance construction, invoked before __init__. The __new__ method is called with the class as its first argument; its responsibility is to return a new instance of that class. Compare this to __init__: __init__ is called with an instance as its first argument, and it doesn’t return anything; its responsibility is to initialize the instance.

The other possibly-unfamiliar element is the WeakValueDictionary. In a nutshell, it’s a dictionary that deletes its entries if no other variables point to them. When instantiating a Card object, it checks in the Card class’s _CardPool to return an existing object, or creates a new Card object if needed.

If you’re confused, take a few minutes to play with the code. Insert print statements into __new__ to show you the contents of _CardPool.items() and whether a new object is created. There’s a lot of important object orientation concepts at work in a short block of code: inheritance, the difference between classes and objects, references, and overriding.

I’ve removed the __eq__ and __ne__ calls because two Cards with the same value and suit are the same objects, saving our casino many gigs of precious RAM:

 
>>> c1 = Card('9', 'h')
>>> c2 = Card('9', 'h')
>>> c1 == c2
True
>>> id(c1), id(c2)
(-1210940772, -1210940772)

The c2 wiki has more info on the Flyweight Pattern.

Want more? I'm not as good at forgetting to update @pushcx on Twitter.