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 “” % (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) `{lang=”python”}
The c2 wiki has more info on the Flyweight Pattern.