Below is the well known code for creating a singleton metaclass:
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
cls.x = 5
return cls._instances[cls]
class MyClass(metaclass=Singleton):
pass
m = MyClass()
v = MyClass()
print (m.x)
m.x = 420
print (v.x)
My question is why do we need to use the call function of type class again to initialize the class? Why can't we call the init method to do that like normal class initialization. Something like this :
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = cls(*args, **kwargs)
cls.x = 5
return cls._instances[cls]
class MyClass(metaclass=Singleton):
pass
m = MyClass()
v = MyClass()
print (m.x)
m.x = 420
print (v.x)
This is getting into an infinite loop anyways.
Because trying to creating an instance of a class by just calling it as you do in the line cls._instances[cls] = cls(*args, **kwargs) just by itself calls the metaclass __call__ which is the exact method where you attempt the call, as is explained here.
Now, if one thing, you should not really be using metaclasses just for creating singletons.
The Metaclass mechanism in Python is complicated - the problem you've hit on this question shows you are grasping now how simple inheritance and call to methods on super-classes work - and metaclasses are an order of magnitude more complicated than that.
And, beyond been complicated, classes with a custom metaclass can't be ordinarily combined with other classes that feature custom metaclasses, so the general rule is keeping their usage to a minimum anyway.
How to create a singleton class:
But for creatign a singleton, you can just place all your checks in the class ordinary __new__ method. No need to feedle with metaclasses - just plain class inheritance:
_instances = {}
class Singleton(object):
def __new__(cls, *args, **kw):
if not cls in _instances:
instance = super().__new__(cls)
_instances[cls] = instance
return _instances[cls]
And just inherit your singleton classes from this one.
Related
I am trying to create a Singleton class in Python using this code:
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
def clear(cls):
cls._instances = {}
class MyClass(metaclass=Singleton):
def my_attribute(*args):
if len(args) == 1:
MyClass.i_attribute = args[0]
elif len(args) == 0:
try:
return MyClass.i_attribute
except:
MyClass.i_attribute = 0
but the clear() method does not seem to work:
MyClass.my_attribute(42)
MyClass.clear()
MyClass.my_attribute() # still returns 42, but I expect 0
How do I delete the instance of MyClass so that I am back to 0 instances?
The singleton metaclass collects in the _instances attribute all the instantiated children. Therefore, if you want to clear the _instances attributes only for a specific class, you can:
Redefine the Singleton class, to make _instances an attribute of the class instantiated by the metaclass:
class Singleton(type):
"""
Singleton metaclass, which stores a single instance of the children class in the children class itself.
The metaclass exposes also a clearing mechanism, that clear the single instance:
* clear: use as follows 'ClassToClear.clear()
"""
def __init__(cls, name, bases, methods):
cls._instance = None
super().__init__(name, bases, methods)
def __call__(cls, *args, **kwargs):
if cls._instance:
return cls._instance
cls._instance = super().__call__(*args, **kwargs)
return cls._instance
def clear(cls):
cls._instance = None
Using this new definition of the singleton, you can write a clear method that can be called by any of the classes initialized with the Singleton metaclass and it will clear the _instance attribute.
So in your case, MyClass.clear() would reset the _instance attribute to None.
Add a clear method, which removes only the children class from the Singleton._instances dictionary:
class SingletonRegistry(type):
"""
Singleton metaclass, which implements a registry of all classes that are created through this metaclass and
the corresponding instance of that class (added at the first creation).
The metaclass exposes also a clearing mechanism, that clears a specific class from the registry:
* clear: use as follows 'ClassToClear.clear()
* clear_all: use as follows 'SingletonRegistry.clear_all()
"""
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(SingletonRegistry, cls).__call__(*args, **kwargs)
return cls._instances[cls]
def clear(cls):
_ = cls._instances.pop(cls, None)
def clear_all(*args, **kwargs):
SingletonRegistry._instances = {}
In this case, if you would like to clear only one specific child class, then you could write MyClass.clear(), which will cause the MyClass key to be removed from Singleton._instances dictionary.
This structure allows also to clear all key in the _instances dictionary by writing SingletonRegistry.clear_all().
user2357112 is right. Here is the correct code:
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
def clear(cls):
cls._instances = {}
class MyClass(metaclass=Singleton):
def my_attribute(*args):
my = MyClass()
if len(args) == 1:
my.i_attribute = args[0]
elif len(args) == 0:
try:
return my.i_attribute
except:
my.i_attribute = 0
return my.i_attribute
I am trying to implement a metaclass that initializes class variables when a first its instance is being created. I want to keep a new magic method __load__ that should be called as a classmethod (like __new__). So I implemented it like this:
class StaticLoad(type):
__loaded_classes = set()
def __call__(cls, *args, **kwargs):
if cls not in cls.__loaded_classes:
if hasattr(cls, '__load__'):
cls.__load__()
cls.__loaded_classes.add(cls)
return super().__call__(*args, **kwargs)
class BaseClass(metaclass=StaticLoad):
s = 0
class MyClass(BaseClass):
#classmethod
def __load__(cls):
print("Loading", cls.__name__, "...")
cls.s += 1
obj1 = MyClass()
obj2 = MyClass()
print(MyClass.s)
It works fine and gives the correct result:
Loading MyClass ...
1
Now I want to implement the method __load__ as a classmethod by default like __new__ (without the need to type #classmethod above each time). I tried this:
class StaticLoad(type):
__loaded_classes = set()
def __call__(cls, *args, **kwargs):
if cls not in cls.__loaded_classes:
if hasattr(cls, '__load__'):
# I try to apply classmethod routine to make
# cls.__load__ a classmethod
classmethod(cls.__load__)()
cls.__loaded_classes.add(cls)
return super().__call__(*args, **kwargs)
class BaseClass(metaclass=StaticLoad):
s = 0
class MyClass(BaseClass):
# #classmethod line was deleted
def __load__(cls):
print("Loading", cls.__name__, "...")
cls.s += 1
obj1 = MyClass()
obj2 = MyClass()
print(MyClass.s)
I got the error:
Traceback (most recent call last):
File "example.py", line 22, in <module>
obj1 = MyClass()
File "example.py", line 7, in __call__
classmethod(cls.__load__)()
TypeError: 'classmethod' object is not callable
It looks like classmethod routine is correctly available only inside a class definition.
How should I improve my metaclass to make it work fine? I would like to keep the content of classes BaseClass and MyClass as I wrote above, placing all magic into StaticLoad.
With the help of #AnttiHaapala the solution is simple. Instead of calling
classmethod(cls.__load__)()
I had to call
cls.__load__(cls)
If you want to perform transforms on the certain methods and attributes of a class creation, you do that on the metaclass' __new__ function.
Since yu already have a metaclass, all you have to do is to implement its __new__ method to convert any __load__ methods in a classmethod:
class StaticLoad(type):
__loaded_classes = set()
def __new__(metacls, name, bases, namespace):
if "__load__" in namespace and not isinstance(namespace["__load__"], classmethod):
namespace["__load__"] = classmethod(namespace["load"])
return super().__new__(metacls, name, bases, namespace)
def __call__(cls, *args, **kwargs):
if cls not in cls.__class__.__loaded_classes:
if hasattr(cls, '__load__'):
cls.__load__()
type(cls).__loaded_classes.add(cls)
return super().__call__(*args, **kwargs)
(the other change I made was to make explict that "__loaded_classes" should be accessed on the metaclass, not on the class itself).
As answered brilliantly by agf here, a possible implementation of a Singleton in Python could be using this metaclass:
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
However, I do not understand the use of _instance. I know it's a dict used to store instances indexed by their class. However, it is declared as a class attribute (_instances = {}), and it is used as an attribute of cls (if cls not in cls._instances:). How can the two be the same?
I saw a lot of methods of making a singleton in Python and I tried to use the metaclass implementation with Python 3.2 (Windows), but it doesn"t seem to return the same instance of my singleton class.
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class MyClass(object):
__metaclass__ = Singleton
a = MyClass()
b = MyClass()
print(a is b) # False
I use the decorator implementation now which is working, but I'm wondering what is wrong with this implementation?
The metaclass syntax has changed in Python3. See the documentaition.
class MyClass(metaclass=Singleton):
pass
And it works:
>>> MyClass() is MyClass()
True
I have a class which, by design, must follow the singleton pattern. So I went ahead and implemented it using a metaclass. Everything worked nicely until a bug was reported which, in summary, said that deepcopy-ied instances of my singleton class were not the same instances.
I can get around this bug by inheriting from a base singleton-type class, but I'd rather not, for reasons pointed out in this question.
A working example of this issue is presented below:
class SingletonMeta(type):
def __init__(cls, name, bases, dict):
super(SingletonMeta, cls).__init__(name, bases, dict)
cls.instance = None
def __call__(cls,*args,**kw):
print "SingletonMeta __call__ was called"
if cls.instance is None:
cls.instance = super(SingletonMeta, cls).__call__(*args, **kw)
return cls.instance
class MyClass1(object):
__metaclass__ = SingletonMeta
class SingletonBase(object):
_instance = None
def __new__(class_, *args, **kwargs):
print "SingletonBase __new__ was called"
if not isinstance(class_._instance, class_):
class_._instance = object.__new__(class_, *args, **kwargs)
return class_._instance
class MyClass2(SingletonBase):
pass
from copy import deepcopy as dcp
mm1 = MyClass1()
mm2 = dcp(mm1)
print "mm1 is mm2:", mm1 is mm2
mb1 = MyClass2()
mb2 = dcp(mb1)
print "mb1 is mb2:", mb1 is mb2
Output:
SingletonMeta __call__ was called
mm1 is mm2: False
SingletonBase __new__ was called
SingletonBase __new__ was called
mb1 is mb2: True
Can you give me any pointers as to how should one resolve this issue? I'm running Python 2.7.X
The docs on the copy module say this:
In order for a class to define its own copy implementation, it can define special methods __copy__() and __deepcopy__().
[...]
The latter is called to implement the deep copy operation; it is passed one argument, the memo dictionary.
[...]
So if you declare these to return self, that ought to do the trick.
When you need to customize class creation (not instance creation), you do it in the __new__ method of the metaclass:
def __new__(cls, name, bases, dict):
dict['__deepcopy__'] = dict['__copy__'] = lambda self, *args: self
return super(SingletonMeta, cls).__new__(cls, name, bases, dict)
and your test will give
SingletonMeta __call__ was called
mm1 is mm2: True
You need to define __copy__ as well or even shallow copies will result in new instances.
Glad that my solution in that thread came in handy.