5

I have a few classes that need to do the following:

When the constructor is called, if an equal object (aka an object with the same id) already exists, return that object. Otherwise, create a new instance. Basically,

>>> cls(id=1) is cls(id=1)
True

To achieve this, I've written a class decorator like so:

class Singleton(object):
    def __init__(self, cls):
        self.__dict__.update({'instances': {},
                                'cls': cls})

    def __call__(self, id, *args, **kwargs):
        try:
            return self.instances[id]
        except KeyError:
            instance= self.cls(id, *args, **kwargs)
            self.instances[id]= instance
            return instance

    def __getattr__(self, attr):
        return getattr(self.cls, attr)
    def __setattr__(self, attr, value):
        setattr(self.cls, attr, value)

This does what I want, but:

@Singleton
class c(object):
    def __init__(self, id):
        self.id= id

o= c(1)
isinstance(o, c) # returns False

How can I fix this? I found a related question, but I just can't seem to adapt those solutions to my use case.


I know someone's gonna ask me to post some code that doesn't work, so here you go:

def Singleton(cls):
    instances= {}
    class single(cls):
        def __new__(self, id, *args, **kwargs):
            try:
                return instances[id]
            except KeyError:
                instance= cls(id, *args, **kwargs)
                instances[id]= instance
                return instance
    return single
# problem: isinstance(c(1), c) -> False

def Singleton(cls):
    instances= {}
    def call(id, *args, **kwargs):
        try:
            return instances[id]
        except KeyError:
            instance= cls(id, *args, **kwargs)
            instances[id]= instance
            return instance
    return call
# problem: isinstance(c(1), c) -> TypeError
Community
  • 1
  • 1
Aran-Fey
  • 39,665
  • 11
  • 104
  • 149
  • I haven't really investigated it much myself, but there is a 10 part blog post on "universal decorators" which is quite good. http://blog.dscpl.com.au/2014/01/how-you-implemented-your-python.html is the first one. Collection referenced here: https://github.com/GrahamDumpleton/wrapt/tree/master/blog – Josh Smeaton Oct 31 '14 at 20:18
  • 4
    To whoever downvoted: I'd appreciate a comment. I can't ask better questions if you don't tell me what I'm doing wrong. – Aran-Fey Oct 31 '14 at 20:32
  • @abarnert: Why not just try it? :) It'll produce a `RuntimeError: maximum recursion depth exceeded` because `__setattr__` attempts to retrieve `self.cls`. – Aran-Fey Oct 31 '14 at 20:49
  • @Rawing: Ah, right. Normally I handle that by having `__setattr__` be smart, rather than by going behind its back, but I guess this works too. – abarnert Oct 31 '14 at 20:59
  • 1
    @Rawing In new-style classes it is [recommended to use](https://docs.python.org/2/reference/datamodel.html#object.__setattr__) something like `object.__setattr__(self, 'instances', {});object.__setattr__(self, 'cls', cls)`, because this respects the descriptor protocol. – Ashwini Chaudhary Oct 31 '14 at 21:01
  • @AshwiniChaudhary: I see. Thanks for the piece of info. – Aran-Fey Oct 31 '14 at 21:04

2 Answers2

8

You can add your custom __instancecheck__ hook in your decorator class:

def __instancecheck__(self, other):
    return isinstance(other, self.cls)
Ashwini Chaudhary
  • 244,495
  • 58
  • 464
  • 504
1

As an alternative solution to to using a decorator to make a singleton class you could instead use a metaclass to create your class. Metaclasses can be used to add functionality to a class in the same way that subclasses can inheiret functionality from their superclass. The advantage of a metaclass is the name c will actually directly refer to the class c rather than a Singleton object or a function that wraps calls to the constructor for c.

For instance:

class SingletonMeta(type):
    """SingletonMeta is a class factory that adds singleton functionality to a 
    class. In the following functions `cls' is the actual class, not 
    SingletonMeta."""

    def __call__(cls, id, *args, **kwargs):
        """Try getting a preexisting instance or create a new one"""
        return cls._instances.get(id) or cls._new_instance(id, args, kwargs)

    def _new_instance(cls, id, args, kwargs):
        obj = super(SingletonMeta, cls).__call__(*args, **kwargs)
        assert not hasattr(obj, "id"), "{} should not use 'id' as it is " \
            "reserved for use by Singletons".format(cls.__name__)
        obj.id = id
        cls._instances[id] = obj
        return obj        

    def __init__(cls, classname, bases, attributes):
        """Used to initialise `_instances' on singleton class"""
        super(SingletonMeta, cls).__init__(classname, bases, attributes)    
        cls._instances = {}

You use it like thus:

# python 2.x
class MySingleton(object):
    __metaclass__ = SingletonMeta

# python 3.x
class MySingleton(object, metaclass=SingletonMeta):
    pass

Comparative use with your decorator:

class IDObject(object):
    def __str__(self):
        return "{}(id={})".format(type(self).__name__, self.id)

@Singleton
class A(IDObject):
    def __init__(self, id):
        self.id = id

class B(IDObject, metaclass=SingletonMeta):
    pass

format_str = """{4} class is {0}
an instance: {1}
{1} is {1} = {2}
isinstance({1}, {0.__name__}) = {3}"""
print(format_str.format(A, A(1), A(1) is A(1), isinstance(A(1), A), "decorator"))
print()
print(format_str.format(B, B(1), B(1) is B(1), isinstance(B(1), B), "metaclass"))

outputs:

decorator class is <__main__.Singleton object at 0x7f2d2dbffb90>
an instance: A(id=1)
A(id=1) is A(id=1) = True
isinstance(A(id=1), A) = False

metaclass class is <class '__main__.B'>
an instance: B(id=1)
B(id=1) is B(id=1) = True
isinstance(B(id=1), B) = True
Dunes
  • 37,291
  • 7
  • 81
  • 97