2

I was trying to implement singleton classes for my project and an interesting post in StackOverflow on the same

Creating a singleton in Python

I decided to go with the metaclasses approach mentioned..

Now.. I tried to add a method to get and clear the instances (in case the user wants to get rid of the current instance and create a new one..):

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 getInstance(cls):
        print("Class is {}".format(cls.__name__))

        if not cls in cls._instances:
            raise LookupError("No instance of the class {cls} create yet.".format(cls.__name__))

        return cls._instances[cls]

    def clearInstance(cls):
        cls._instances.pop(cls, None)


class someClass(metaclass=Singleton):
    def __init__(self,val):
        self.value = val

I am able to create objects successfully..

In [9]: sc = someClass(1)

In [10]: sc.value
Out[10]: 1

But when I do dir(someClass) the 2 methods are not displayed:

In [14]: dir(someClass)
Out[14]:
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__']

Nevertheless I am able to call the methods..

In [13]: someClass.getInstance()

Class is someClass

Out[13]: <__main__.someClass at 0x7f728b180860>

In all the examples on metaclass I see online I see __new__, __init__ and __call__ methods implemented but I don't see any additional methods added. Is it correct to add methods to the metaclass?

I also tried a small variation of the above metaclass 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]

    @classmethod
    def getInstance(cls):
        print("Class is {}".format(cls.__name__))

        if not cls in cls._instances:
            raise LookupError("No instance of the class {cls} create yet.".format(cls.__name__))

        return cls._instances[cls]

    @classmethod
    def clearInstance(cls):
        cls._instances.pop(cls, None)

Marked the 2 methods as class method..

Now when I attempt to call them:

In [2]: sc = someClass(1)

In [3]: someClass.getInstance()

Class is Singleton
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-3-c83fe01aa254> in <module>()
----> 1 someClass.getInstance()

<ipython-input-1-9efb6548d92d> in getInstance(cls)
     12
     13                 if not cls in cls._instances:
---> 14                         raise LookupError("No instance of the class {cls} create yet.".format(cls.__name__))
     15
     16                 return cls._instances[cls]

KeyError: 'cls'

As you can see the class is print says its Singleton when I decorate it as classmethod. Otherwise it shows the correct class. I don't understand this behavior, can someone explain?

martineau
  • 99,260
  • 22
  • 139
  • 249

3 Answers3

6

Can python metaclasses have methods?

Yes, as your first example shows they can have methods and they can be called on classes that implement your metaclass.

For example in python-3.x the metaclass type implements the mro attribute:

>>> object.mro()
[object]

But you can't access them on instances:

>>> object().mro()
AttributeError: 'object' object has no attribute 'mro'

But when I do dir(someClass) the 2 methods are not displayed.

dir calls type.__dir__ and that just shows a limited amount of the methods:

If the object is a type or class object, the list contains the names of its attributes, and recursively of the attributes of its bases.

No mention about methods of the metaclass here. That's because these are hidden by default.

That's why you don't see the mro method either:

>>> 'mro' in dir(object)
False

However dir allows to customize what you see, so you can simply override it. Because __dir__ is called on the "class of the instance" and your "metaclass is the type of your class" you have to implement it on your 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]

    def getInstance(cls):
        print("Class is {}".format(cls.__name__))
        if not cls in cls._instances:
            raise LookupError("No instance of the class {cls} create yet.".format(cls.__name__))
        return cls._instances[cls]

   def clearInstance(cls):
        cls._instances.pop(cls, None)
            
   def __dir__(self):
        normal_dir = type.__dir__(self)
        # Also include all methods that don't start with an underscore and
        # not "mro".
        normal_dir.extend([
            f for f in dir(type(self)) 
            if not f.startswith('_') 
                and f != 'mro'
        ])
        return normal_dir
    
class someClass(metaclass=Singleton):
   def __init__(self,val):
       self.value = val
            
>>> dir(someClass)
[..., 'clearInstance', 'getInstance']

Now these methods of your class are visible when you call dir.

Is it correct to add methods to the metaclass?

That depends a bit on the context. I would say it's fine to add the methods to the metaclass. However, these should be used rarely.

As you can see the class is print says its Singleton when I decorate it as classmethod. Otherwise it shows the correct class. I don't understand this behavior, can someone explain?

It's obvious if you think about it. Singleton is the class of your someClass and when you make it a classmethod the cls argument will be Singleton. However the class that has been added to the _instances is someClass. I can see where that comes from though. All your methods take a cls argument. That probably made you believe they are "like" classmethods (and they are in some way, but not of the metaclass but of the class that implements the metaclass!).

But it's just a convention because, like self is the typical argument name for an instance of a class, so is cls the typical argument name for an instance of a metaclass. When you have classmethods on your metaclass the first argument should probably be called metacls. Also fixing a small problem with the str.format (which is the reason it throws a KeyError instead of a LookupError):

class Singleton(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
            print(cls._instances)  # print the dict after creating an instance
        return cls._instances[cls]

    @classmethod
    def getInstance(metacls):
        print("Class is {}".format(metacls))
        if not metacls in metacls._instances:
            raise LookupError("No instance of the class {0} create yet.".format(metacls.__name__))
        return metacls._instances[metacls]

    @classmethod
    def clearInstance(metacls):
        metacls._instances.pop(metacls, None)
    
class someClass(metaclass=Singleton):
    def __init__(self,val):
        self.value = val
    
>>> sc = someClass(1)
{<class '__main__.someClass'>: <__main__.someClass object at 0x00000235844F8CF8>}
>>> someClass.getInstance()
Class is <class '__main__.Singleton'>
LookupError: No instance of the class Singleton create yet.

So you add the "class" to the dict but then you check if the metaclass is in the dict (which it isn't).

Generally custom classmethods (besides those that should/could be classmethods e.g. __prepare__) on a metaclass don't make much sense because you seldom need the type of the class of your instance.

artu-hnrq
  • 423
  • 1
  • 4
  • 21
MSeifert
  • 118,681
  • 27
  • 271
  • 293
  • yes i am aware that instead of someClass i am getting the SingletonClass as the cls argument.. But i did not understand why i get the cls as SingletonClass once i decorate as class method.. – Arun Kaliraja Baskaran Aug 31 '17 at 04:37
  • @ArunKalirajaBaskaran I edited the answer a bit to include more details. Let me know if anything is unclear. I have learned a lot by answering this question and because some aspects are also new to me it could be a bit "jumpy" in the text. :) – MSeifert Aug 31 '17 at 17:09
4

Can python metaclasses have methods?

Yes.


But when I do dir(someClass) the 2 methods are not displayed

Contrary to what you might have believed, dir doesn't show everything:

Because dir() is supplied primarily as a convenience for use at an interactive prompt, it tries to supply an interesting set of names more than it tries to supply a rigorously or consistently defined set of names, and its detailed behavior may change across releases. For example, metaclass attributes are not in the result list when the argument is a class.


As you can see the class is print says its Singleton when I decorate it as classmethod.

Don't decorate it with classmethod! That specifically says you want the method to operate on Singleton itself or subclasses of Singleton, rather than instances of Singleton. Classes with Singleton as their metaclass are instances of singleton; the fact that they are classes is not a reason to put classmethod on Singleton's methods.

user2357112 supports Monica
  • 215,440
  • 22
  • 321
  • 400
  • 2
    While I agree that they can have methods it was also asked "Is it correct to add methods to the metaclass?" - which is a bit harder to answer. I would be definitely interested in what your thoughts are on this point :) – MSeifert Aug 30 '17 at 23:04
3

Classes are instances of their metaclasses. Just as instances of classes don't have the methods of the class as attributes but nonetheless can have them called, classes do not have the methods of the metaclass as attributes.

Ignacio Vazquez-Abrams
  • 699,552
  • 132
  • 1,235
  • 1,283