0

I came across some materials online regarding how to force a class to be created as Singleton by taking advantage of python metaclass. The code snippet is roughly as below:

class SingletonMetaClass(type):
    _instance = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instance:
            # usually a lock is used here, but omitted for simplicity
            cls._instance[cls] = super().__call__(*args, **kwargs)

        return cls._instance[cls]

class SingletonBaseClass(object, metaclass=SingletonMetaClass):
    def __init__():
        pass

class SingletonDerivedClass(SingletonBaseClass):
    def __init__():
        pass

The Above snippet works perfectly for me - all instances of SingletonDerivedClass will be identical ones. However, what I found is strange is the line in SingletonMetaClass that cls._instance[cls] = super().__call__(*args, **kwargs). Apparently, the former cls refers to SingletonMetaClass itself and the latter one refers to the subclass, in this case, it will be SingletonDerivedClass, but inside __call__'s signature there's only one cls, how does python interpreter tell which cls refers to what?

Any replies would be appreciated!

Simon Wu
  • 13
  • 3

2 Answers2

2

None of the uses of cls refer to the metaclass. They refer to the instance of the metaclass, either SingletonBaseClass or SingletonDerivedClass.

Looking up cls._instance when cls is SingletonBaseClass finds SingletonMetaClass._instance because SingletonBaseClass is an instance of SingletonMetaClass, and looking up attributes on an object also searches its class, even when the object is itself also a class.

user2357112
  • 260,549
  • 28
  • 431
  • 505
0

"Create singleton class in python by taking advantage of meta class"

don't.

please.

This defines overkill.

So, before explaining what went wrong or your doubt, here is how you can define a singleton in Python, which can only be usd as a singleton, unless someone decides to use introspection to "create more than one singleton"- at which point the person could just as well abuse whatever thr mechanisms you have there.

Simply create a class, then create the instance that will be your singleton. Then remove the reference to the class from the namespace. If there is code that would try to instantiate the class that should be a singleton, put a __call__ method returning self:

class MySingleton():
    ...
    def __call__(self):
        return self

mysingleton = MySingleton()
del MySingleton

If the code won't expect to instantiate the class, or ou need the singleton to be callable for other reasons: this __call__ is simply cosmetic. Actually, the singleton instance could have the same name as the class, and even the del statement is not needed.

Now, forthe part you got confused in your code: as far as any method in the metaclass is concerned, if it is a method that would be an "instance" method: i.e. take "self" as the first parameter, that first parameter refers to a class! (not the metaclass). So, we usually write cls as the parameter name when writting methods for the metaclass.

In other words, in the case of your __call__ that "cls" is the instance of the metaclass: the classes themselves. Using the name cls for the parameter in __call__ does not magically makes it receive a reference to the class where the method is written (the metaclass itself).

If given your class you want to get a reference to the metaclass, just proceed as with any other Python object: use either the type call or check the __class__ attribute.

So the fix for your code would be, if you choose to ignore my advice to have metaclasses just for having a singleton:

def __call__(cls, *args, **kwargs):
    if cls not in cls._instance:
        # usually a lock is used here, but omitted for simplicity
        cls.__class__._instance[cls] = super().__call__(*args, **kwargs)

    return cls._instance[cls]

If you want the first parameter in a metaclass method to be the metaclass itself, the @classmethod decorator does that(but such modifier can only be applied for ordinay methods - other than "dunder" special methods, like __call__ - the language expects __call__ and others to take the instance as first parameter)

jsbueno
  • 99,910
  • 10
  • 151
  • 209