Python Flyweights «
»


Code:
6 comments

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.


Comments

  1. The only problem I have with this pattern is that __init__ sets self.value and self.suit for objects that already have this set. Is there any way around this short of doing this initialization in __new__ and not having an __init__ at all?

    My particular class is a representation of a on-disk atom file which needs particular initialization when it’s created but just needs to be deserialized off disk subsequent times.

  2. I don’t see that Guido wrote that __init__ should be empty. In fact, in the paragraph after the one I quoted he talked about them working together:

    First, the class’s __new__ method is called, passing the class itself as first argument, followed by any (positional as well as keyword) arguments received by the original call. This returns a new instance. Then that instance’s __init__ method is called to further initialize it

    And later mentioned some built-in Python types work as this blog post described:

    For immutable classes, your __new__ may return a cached reference to an existing object with the same value; this is what the int, str and tuple types do for small values. This is one of the reasons why their __init__ does nothing: cached objects would be re-initialized over and over.

    I didn’t keep the __init__ from re-initializing because I wanted to keep this example simple and recognizable for folks who haven’t used __new__ before, but that may have been a mistake. The simplest way would be for __init__ to check if it’s already been intialized, but that’s a little ugly. There’s probably a nicer way to do it that’s not occurring to me at the moment.

  3. The if not obj: block would become
    obj = object.__new__(cls)
    Card._CardPool[value + suit] = obj
    obj.value, obj.suit = value, suit

    leaving __init__ empty. It wasn’t until I read the later paragraph that it’s ok to leave __init__ empty, which is how you avoid checking it it’s already initialised in __init__.

Leave a Reply

Your email address will not be published.