0

I recently came across Python's metaclass concept and I am trying to enforce Singleton for a few existing classes. I want to reuse the previously created instance when the class is instantiated again.

Here is my attempt at it. But the singleton condition doesn't seem to get enforced.

The existing class that should enforce Singleton. I have multiple such classes like Bike, Train which inherits Vehicle and all these should be made singleton.

from singleton import Singleton
from vehicle import Vehicle

class Car(Vehicle, object):
    __metaclass__ = Singleton

    def __init__(self):
        print("Constructor: Car is starting")
        pass

    def print_car(self):
        print("Car is running")

Car class inherits a Vehicle

from abc import ABC

class Vehicle(ABC):
    def __init__(self):
        pass

Here is my attempt at Singleton. I used a few reference SO posts.

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, when I create multiple objects for Car, I don't see singleton being enforced.

car = Car()
car.print_car()
car2 = Car()
car2.print_car()
print(car is car2)  # Returns False

As I am still learning Python's metaclass and how it works, wondering what I am missing here?

All these classes (base class and derived class) are in my control and I can make changes to them. How can I achieve enforcing singleton to my sub-classes like Car, Bike etc?

[Update 1] Using Python 3 version of metaclass gives me the below error

from singleton import Singleton
from vehicle import Vehicle

class Car(Vehicle, object, metaclass=Singleton):
# class Car(Vehicle, object):
    # __metaclass__ = Singleton
    def __init__(self):
        print("Constructor: Car is starting")

    def print_car(self):
        print("Car is running")

Error:

  File "/Users/user/HelloVsCode/hello.py", line 5, in <module>
    from car import Car
  File "/Users/user/HelloVsCode/car.py", line 7, in <module>
    class Car(Vehicle, object, metaclass=Singleton):
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
SyncMaster
  • 9,754
  • 34
  • 94
  • 137

2 Answers2

1

First: Metaclasses are overkill for a singleton.

If you want a singleton, you can just instantiate the class right after it is declared, and throw away the "class" and keep only its instance.

Like in:

class _Sing1:
    ...

Sing1 = _Sing1()

# done.

If you want the singleton to behave like a class, that is called, and them will yield always the same instance, a decorator that injects a __call__method and instantiate the class at once is also simple enough, and can be used across several classes:

def singleton(cls):
   cls.__call__ = lambda self: self
   return cls()

@singleton
class Sing2:
    ...

This should be enough for you, unless your singleton instances need to be callables themselves.

Other than that, the recipe you found around to create singletons using a metaclass works - what is wrong is how to specify that a class should use that as a metaclass.

The syntax you used in your post, declaring a __metaclass__ attribute inside the class body, was Python 2 way of determining the use of a custom metaclass. In Python 3, we simply specify the metaclass in the metaclas kwarg when declaring the class.

That means, this will work, with the metaclass above, to make your "Car" class work as a singleton factory:

class Car(Vehicle, object, metaclass=Singleton):

    def __init__(self):
        print("Constructor: Car is starting")
        
    def print_car(self):
        print("Car is running")

update If you want to use ABCMeta along the metaclass for singletons antipattern, your metaclass have to inherit from ABCMeta - otherwise you will have a conflict of metaclasses:

import abc

class Singleton(abc.ABCMeta):
   def __call__(...):
       ...
jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • In general, the class will be instantiated in multiple different classes. That is the whole reason I am looking for a singleton. So I can't use your first approach of throwing away the class. – SyncMaster Jun 25 '22 at 10:35
  • When I use Python 3 metaclass suggestion "class Car(Vehicle, object, metaclass=Singleton):", I get this error - "TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases" – SyncMaster Jun 25 '22 at 10:36
  • I have updated my posted with your suggestions. – SyncMaster Jun 25 '22 at 10:40
  • Oh, yes -=there is a reason for which I said metaclasses are overkill: they are the last resource for customiziung building classes. In =Python, as singleton is just a class for which you won't want any other instances, so the _first_ suggestion in my answer should be ok The metaclass "pattern" for singletons is overkill, for the third time. – jsbueno Jun 25 '22 at 17:49
  • 1
    But if you think you need it, _and_ want to use other combining metaclasses, then, combine the metaclass: Your `Singleton` metaclass have to inherit from `abc.ABCMeta` - – jsbueno Jun 25 '22 at 17:50
1

Thanks to @jsbueno for the answer. Here are two solutions that solved my use case.

Method 1: A decorator

With this approach, I can use a decorator for every derived class.

singleton.py

import functools

# https://github.com/siddheshsathe/handy-decorators/blob/master/src/decorators.py   
def singleton(cls):
    previous_instances = {}
    @functools.wraps(cls)
    def wrapper(*args, **kwargs):
        if cls in previous_instances and previous_instances.get(cls, None).get('args') == (args, kwargs):
            return previous_instances[cls].get('instance')
        else:
            previous_instances[cls] = {
                'args': (args, kwargs),
                'instance': cls(*args, **kwargs)
            }
            return previous_instances[cls].get('instance')
    return wrapper
 

car.py

from singleton import singleton
from vehicle import Vehicle

@singleton
class Car(Vehicle):
    def __init__(self):
        print("Constructor: Car is starting")

    def print_car(self):
        print("Car is running")

vehicle.py

from abc import ABC

class Vehicle(ABC):

    def __init__(self, vehicle_type, can_fly: bool = False, enable_timeout: bool = False):
        self._vehicle_type = vehicle_type
        self._can_fly = can_fly
        self._enable_timeout = enable_timeout

Method 2: Metaclass

Ref: https://stackoverflow.com/a/33364149/73137

With this approach, I can make my base class as singleton. Thus enforcing it on all derived classes.

singleton.py

class Singleton(ABCMeta):
    _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]

car.py

from vehicle import Vehicle

class Car(Vehicle):
    def __init__(self):
        print("Constructor: Car is starting")

    def print_car(self):
        print("Car is running")

vehicle.py

from abc import ABC
from singleton import Singleton

class Vehicle(ABC, metaclass=Singleton):
    def __init__(self, vehicle_type, can_fly: bool = False, enable_timeout: bool = False):
        self._vehicle_type = vehicle_type
        self._can_fly = can_fly
        self._enable_timeout = enable_timeout

Common Code:

test.py

from car import Car

car = Car()
car.print_car()
car2 = Car()
car2.print_car()
print(car is car2)

output:

Constructor: Car is starting
Car is running
Car is running
True

As I am still new to the world of Python's metaclass and singleton, let me know if there are any specific pros and cons between the two approaches.

SyncMaster
  • 9,754
  • 34
  • 94
  • 137