Python Flyweights

I mentioned at the end of my post on Python Equality that another way to solve the example given was to use the flyweight pattern. I gave code for playing cards that would end up with many small duplicate objects in memory, and that memory usage can be reduced if (as in the example) the objects don't have extra state that wasn't used for testing equality.

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
            obj.value, obj.suit = value, suit

        return obj

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.