3

I'm following this link and trying to make a singleton class. But, taking arguments (passed while initiating a class) into account so that the same object is returned if the arguments are same.

So, instead of storing class name/class reference as a dict key, I want to store passed arguments as keys in dict. But, there could be unhashable arguments also (like dict, set itself).

What is the best way to store class arguments and class objects mapping? So that I can return an object corresponding to the arguments.

Thanks anyways.


EDIT-1 : A little more explanation. Let's say there is class as follows

class A:
    __metaclass__ == Singleton
    def __init__(arg1, arg2):
        pass

Now, A(1,2) should always return the same object. But, it should be different from A(3,4)

I think, the arguments very much define the functioning of a class. Let's say if the class is to make redis connections. I might want to create 2 singletons objects with diff redis hosts as parameters, but the underlying class/code could be common.

Community
  • 1
  • 1
Pankaj Singhal
  • 15,283
  • 9
  • 47
  • 86
  • Would be nice to have a `frozendict` as there is a `frozenset`.... I mean there is at least one implementation, but it's not in the standard library. Anyway, if you don't mind dependencies, try this: https://pypi.python.org/pypi/frozendict – vlad-ardelean Aug 19 '16 at 08:23
  • 2
    Maybe I'm wrong but it appears to me that you actually want memoization of equal class instances like `int('1') is int('1')` evaluates to `True`. For me, singleton implies that there is exactly one instance of the class, like the `None` obejct which is the one and only instance of `NoneType`. – code_onkel Aug 19 '16 at 09:33
  • OK, I still think that this is not the singleton pattern any more, but never mind. Lets say you have `l1 = [1, 2]` and `l2 = [1, 2]`, so `l1 is l2` is `False` and `l1 == l2` is `True`. Do you want `A(l1, 'foo')` return the same instance as `A(l2, 'foo')`? What happens to the singleton instance of `A` when an unhashable argument is changed, e.g. by the caller of `A(...)` – code_onkel Aug 19 '16 at 10:22
  • Yes, in the case of `l1 = [1, 2]` and `l2 = [1, 2]`, I want `A(l1, 'foo')` to return the same instance as `A(l2, 'foo')` because the underlying values are same even though the objects might be diff. If an unhashable argument is changed, the assumption is that it is deliberate and a separate instance is needed. – Pankaj Singhal Aug 19 '16 at 10:28
  • Alright, I will post an answer. – code_onkel Aug 19 '16 at 10:42
  • @PankajSinghal but what happens if I now do `l2.append(3)`? l1 and l2 now have different values but objects initialised with either earlier are the same instance. I think what you might want is a memoised function that gives you a redis connection or whatever, which is used in your class based on the parameters passed in. If this works for what you want I'll write up a full answer with examples. – theheadofabroom Aug 19 '16 at 10:47
  • @theheadofabroom This is what my previous comment mentions. If earlier the object is initialized with `l2 = [1,2]` then that object can only be gotten again if we pass `[1,2]`. If `l2.append(3)` is done, then the object stored in `Singleton` class should not be associated with the new `l2`. And a new object should be initialized if the new `l2` is passed as a parameter – Pankaj Singhal Aug 19 '16 at 11:16
  • I'm not sure you have thought about the full implications of this. If I were to mutate l1 rather than l2, now instances based on either use the mutated value without any warning. Having the instances be separate but use a memoised function to get the redis connection when needed vastly simplifies not only your implementation, but also the mental model you must keep out it, as it suddenly behaves in a much more obvious way. Remember the Zen of Python, simple is better than complex https://www.python.org/dev/peps/pep-0020/ – theheadofabroom Aug 19 '16 at 12:00

1 Answers1

0

As theheadofabroom and me already mentioned in the comments, there are some odds when relying on non-hashable values for instance caching or memoization. Therefore, if you still want to do exactly that, the following example does not hide the memoization in the __new__ or __init__ method. (A self-memoizing class would be hazardous because the memoization criterion can be fooled by code that you don't control).

Instead, I provide the function memoize which returns a memoizing factory function for a class. Since there is no generic way to tell from non-hashable arguments, if they will result in an instance that is equivalent to an already existing isntance, the memoization semantics have to be provided explicitly. This is achieved by passing the keyfunc function to memoize. keyfunc takes the same arguments as the class' __init__ method and returns a hashable key, whose equality relation (__eq__) determines memoization.

The proper use of the memoization is in the responsibility of the using code (providing a sensible keyfunc and using the factory), since the class to be memoized is not modified and can still be instantiated normally.

def memoize(cls, keyfunc):
    memoized_instances = {}

    def factory(*args, **kwargs):
        key = keyfunc(*args, **kwargs)
        if key in memoized_instances:
            return memoized_instances[key]

        instance = cls(*args, **kwargs)
        memoized_instances[key] = instance
        return instance

    return factory


class MemoTest1(object):
    def __init__(self, value):
        self.value = value

factory1 = memoize(MemoTest1, lambda value : value)

class MemoTest2(MemoTest1):
    def __init__(self, value, foo):
        MemoTest1.__init__(self, value)
        self.foo = foo

factory2 = memoize(MemoTest2, lambda value, foo : (value, frozenset(foo)))

m11 = factory1('test')
m12 = factory1('test')
assert m11 is m12

m21 = factory2('test', [1, 2])

lst = [1, 2]
m22 = factory2('test', lst)

lst.append(3)
m23 = factory2('test', lst)

assert m21 is m22
assert m21 is not m23  

I only included MemoTest2 as a sublclass of MemoTest1 to show that there is no magic involved in using regular class inheritance.

code_onkel
  • 2,759
  • 1
  • 16
  • 31