Ray Salemi
Ray Salemi

Reputation: 5913

Clearing a MetaClass Singleton

I've created a Singleton using a MetaClass as discussed in Method 3 of this answer

 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 MySing(metaclass=Singleton): ...

I'd like to be able to clear the Singleton in the setUp() method of a unittest.TestCase so that each test starts with a clean Singleton.

I guess I don't really understand what this metaClass is doing because I can't get the correct incantation for a clear() method:

     def clear(self):
       try:
          del(Singleton._instances[type(self)]
       except KeyError:
          pass   #Sometimes we clear before creating

Any thoughts on what I'm doing wrong here? My singleton is not getting cleared.

sing=MySing()
sing.clear()

The type call above returns Singleton not MySing.

Upvotes: 3

Views: 3859

Answers (2)

chepner
chepner

Reputation: 531345

Let's walk through a (corrected) definition of Singleton and a class defined using it. I'm replacing uses of cls with Singleton where the lookup is passed through anyway.

 class Singleton(type):
     _instances = {}
     
     # Each of the following functions use cls instead of self
     # to emphasize that although they are instance methods of
     # Singleton, they are also *class* methods of a class defined
     # with Singleton
     def __call__(cls, *args, **kwargs):
         if cls not in Singleton._instances:
             Singleton._instances[cls] = super().__call__(*args, **kwargs)
         return Singleton._instances[cls]

     def clear(cls):
         try:
             del Singleton._instances[cls]
         except KeyError:
             pass

class MySing(metaclass=Singleton):
    pass

s1 = MySing()   # First call: actually creates a new instance
s2 = MySing()   # Second call: returns the cached instance
assert s1 is s2 # Yup, they are the same
MySing.clear()  # Throw away the cached instance
s3 = MySing()   # Third call: no cached instance, so create one
assert s1 is not s3  # Yup, s3 is a distinct new instance

First, _instances is a class attribute of the metaclass, meant to map a class to a unique instance of that class.

__call__ is an instance method of the metaclass; its purpose is to make instances of the metaclass (i.e., classes) callable. cls here is the class being defined, not the metaclass. So each time you call MyClass(), that converts to Singleton.__call__(MyClass).

clear is also a instance method of the metaclass, meaning it also takes a instance of the meta class (i.e again, a class) as an argument (not an instance of the class defined with the metaclass.) This means MyClass.clear() is the same as Singleton.clear(MyClass). (This also means you can, but probably shouldn't for clarity, write s1.clear().)

The identification of metaclass instance methods with "regular" class class methods also explains why you need to use __call__ in the meta class where you would use __new__ in the regular class: __new__ is special-cased as a class method without having to decorate it as such. It's slightly tricky for a metaclass to define an instance method for its instances, so we just use __call__ (since type.__call__ doesn't do much, if anything, beyond invoking the correct __new__ method).

Upvotes: 13

Shine
Shine

Reputation: 588

I see three useful test cases for this metaclass at first glance.

  • Test whether a single class creation works properly.
  • Test whether no new class is created after the initial one.
  • Test whether multiple classes can use this metaclass in conjunction.

All of these tests can be achieved without a "reset" button. After which you'll have covered most of your bases. (I might have forgotten one).

Simply create a few different TestClasses that use this metaclass and check their Id's and types.

Upvotes: -2

Related Questions