Python-Design-Patterns flyweight

Table of Contents

  1. flyweight Model
  2. Python-Design-Patterns flyweight
  3. flyweight Test

flyweight Model

Python-Design-Patterns flyweight

#!/usr/bin/env python
# -*- coding: utf-8 -*-

*What is this pattern about?
This pattern aims to minimise the number of objects that are needed by
a program at run-time. A Flyweight is an object shared by multiple
contexts, and is indistinguishable from an object that is not shared.

The state of a Flyweight should not be affected by it's context, this
is known as its intrinsic state. The decoupling of the objects state
from the object's context, allows the Flyweight to be shared.

*What does this example do?
The example below sets-up an 'object pool' which stores initialised
objects. When a 'Card' is created it first checks to see if it already
exists instead of creating a new one. This aims to reduce the number of
objects initialised by the program.


Minimizes memory usage by sharing data with other similar objects.

import weakref

class FlyweightMeta(type):
    def __new__(mcs, name, parents, dct):
        Set up object pool

        :param name: class name
        :param parents: class parents
        :param dct: dict: includes class attributes, class methods,
        static methods, etc
        :return: new class
        dct['pool'] = weakref.WeakValueDictionary()
        return super(FlyweightMeta, mcs).__new__(mcs, name, parents, dct)

    def _serialize_params(cls, *args, **kwargs):
        Serialize input parameters to a key.
        Simple implementation is just to serialize it as a string
        args_list = list(map(str, args))
        args_list.extend([str(kwargs), cls.__name__])
        key = ''.join(args_list)
        return key

    def __call__(cls, *args, **kwargs):
        key = FlyweightMeta._serialize_params(cls, *args, **kwargs)
        pool = getattr(cls, 'pool', {})

        instance = pool.get(key)
        if instance is None:
            instance = super(FlyweightMeta, cls).__call__(*args, **kwargs)
            pool[key] = instance
        return instance

class Card(object):

    """The object pool. Has builtin reference counting"""

    _CardPool = weakref.WeakValueDictionary()

    """Flyweight implementation. If the object exists in the
    pool just return it (instead of creating a new one)"""

    def __new__(cls, value, suit):
        obj = Card._CardPool.get(value + suit)
        if not obj:
            obj = object.__new__(cls)
            Card._CardPool[value + suit] = obj
            obj.value, obj.suit = value, suit
        return obj

    # def __init__(self, value, suit):
    #     self.value, self.suit = value, suit

    def __repr__(self):
        return "<Card: %s%s>" % (self.value, self.suit)

def with_metaclass(meta, *bases):
    """ Provide python cross-version metaclass compatibility. """
    return meta("NewBase", bases, {})

class Card2(with_metaclass(FlyweightMeta)):
    def __init__(self, *args, **kwargs):
        # print('Init {}: {}'.format(self.__class__, (args, kwargs)))

if __name__ == '__main__':
    # comment __new__ and uncomment __init__ to see the difference
    c1 = Card('9', 'h')
    c2 = Card('9', 'h')
    print(c1, c2)
    print(c1 == c2, c1 is c2)
    print(id(c1), id(c2))

    c1.temp = None
    c3 = Card('9', 'h')
    print(hasattr(c3, 'temp'))
    c1 = c2 = c3 = None
    c3 = Card('9', 'h')
    print(hasattr(c3, 'temp'))

    # Tests with metaclass
    instances_pool = getattr(Card2, 'pool')
    cm1 = Card2('10', 'h', a=1)
    cm2 = Card2('10', 'h', a=1)
    cm3 = Card2('10', 'h', a=2)

    assert (cm1 == cm2) and ( cm1 != cm3)
    assert (cm1 is cm2) and ( cm1 is not cm3)
    assert len(instances_pool) == 2

    del cm1
    assert len(instances_pool) == 2

    del cm2
    assert len(instances_pool) == 1

    del cm3
    assert len(instances_pool) == 0

### OUTPUT ###
# (<Card: 9h>, <Card: 9h>)
# (True, True)
# (31903856, 31903856)
# True
# False

flyweight Test

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import unittest
from structural.flyweight import Card

class TestCard(unittest.TestCase):
    def test_instances_shall_reference_same_object(self):
        c1 = Card('9', 'h')
        c2 = Card('9', 'h')
        self.assertEqual(c1, c2)
        self.assertEqual(id(c1), id(c2))

    def test_instances_with_different_suit(self):
        shall reference different objects
        c1 = Card('9', 'a')
        c2 = Card('9', 'b')
        self.assertNotEqual(id(c1), id(c2))

    def test_instances_with_different_values(self):
        shall reference different objects
        c1 = Card('9', 'h')
        c2 = Card('A', 'h')
        self.assertNotEqual(id(c1), id(c2))

    def test_instances_shall_share_additional_attributes(self):
        expected_attribute_name = 'attr'
        expected_attribute_value = 'value of attr'
        c1 = Card('9', 'h')
        c1.attr = expected_attribute_value
        c2 = Card('9', 'h')
        self.assertEqual(hasattr(c2, expected_attribute_name), True)
        self.assertEqual(c2.attr, expected_attribute_value)