DhruvPathak DhruvPathak - 4 months ago 12
Python Question

Singleton/Borg pattern based on different parameters passed while creating the object

I am using borg pattern to share state amongst the objects:

class Borg:
__shared_state = {}

def __init__(self):
self.__dict__ = self.__shared_state

Now lets assume that I want to create context based objects of the Borg class, based on parameters I pass while creating an object.
Is this a correct way to create Borg pattern ( state sharing) for multiple contexts ?

import random
import cPickle

class Borg:
__shared_state = { }

def __init__(self,*args,**kwargs):
context_key = hash('{0}{1}'.format(cPickle.dumps(args),cPickle.dumps(kwargs)))
self.__shared_state.setdefault(context_key, {})
self.__dict__ = self.__shared_state[context_key]

def set_random_property(self):
self.num = str(random.randint(1,100000))

a = Borg(x='ONE')
b = Borg(x = 'TWO')
c = Borg(x = 'TWO')
print('a with ONE has num:{0}'.format(a.num))
print('b with TWO has num:{0}'.format(b.num))
print('c with TWO has num:{0}'.format(c.num))


{7373348246660160089: {}}
{7373348246660160089: {'num': '18322'}, 3334843421982509183: {}}
{7373348246660160089: {'num': '18322'}, 3334843421982509183: {'num': '33084'}}
a with ONE has num:18322
b with TWO has num:33084
c with TWO has num:33084

Works correctly. Is there a way to improvise this pattern ? Or any better alternatives available for python 2.7 ?


No, what you use is what I'd use; use a dictionary for the shared states.

You can simplify it slightly by using the return value of dict.setdefault() rather than ignore it:

def __init__(self, *args, **kwargs):
    context_key = hash('{0}{1}'.format(cPickle.dumps(args),cPickle.dumps(kwargs)))
    self.__dict__ = self.__shared_state.setdefault(context_key, {})

All this can be encapsulated in a metatype:

class PerArgsBorgMeta(type):
    def __new__(mcls, name, bases, attrs):
        cls = super(PerArgsBorgMeta, mcls).__new__(mcls, name, bases, attrs)
        setattr(cls, '_{}__shared_state'.format(name), {})
        return cls

    def __call__(cls, *args, **kwargs):
        instance = super(PerArgsBorgMeta, cls).__call__(*args, **kwargs)
        context_key = hash('{0}{1}'.format(cPickle.dumps(args),cPickle.dumps(kwargs)))
        state = getattr(cls, '_{}__shared_state'.format(cls.__name__))
        instance.__dict__ = state.setdefault(context_key, {})
        return instance

Then use this as a __metaclass__ attribute on the class:

class SomeBorgClass:
    __metaclass__ = PerArgsBorgMeta

Do note that using hash(cPickle.dumps(kwargs)) will still create distinct hashes for dictionaries with collisions:

>>> import cPickle
>>> hash(cPickle.dumps({'a': 42, 'i': 81}))
>>> hash(cPickle.dumps({'i': 81, 'a': 42}))

The same applies to sets. Sorting (recursively if you must be exhaustive) can help here, but be careful that you don't then produce false-positives between, say, a set passed in as a value, and a tuple with the same values in it used instead. There are increasingly convoluted work-arounds possible for each of these, but at some point you just have to accept the limitation rather than complicate the hashing code more still.