1

I'm making a singleton with an arbitrary number of keyword arguments. While debugging the class, the exception shown after execution don't match with how the debugging trace develops.

I'm using a implementation pretty similar to what I found in this web and also in this question.

I have tried to play around with / and *, because in the official documentation there is a reference to some special cases, but it didn't work.

class A:
    class B:
        def __init__(self, *, arg1, arg2, arg3, arg4='default'):
            self.arg1 = arg1
            self.arg2 = arg2
            self.arg3 = arg3
            self.arg4 = arg4

    _singleton_instance = None

    def __init__(self, **args):
        if not A._singleton_instance:
            _singleton_instance = A.B(**args)

    def __getattribute__(self, name):
        getattr(self._instance, name)


A(arg1=1, arg2=2, arg3=3)
A.arg1

The exception after the execution says:

AttributeError: type object 'A' has no attribute 'arg1'.

The exception that only appears while debugging says:

RecursionError('maximum recursion depth exceeded',)

2
  • Why are you calling __setattr__ in __getattribute__? Commented Jul 23, 2019 at 13:01
  • @AKX I have corrected it, thank you for pointing that out. Commented Jul 23, 2019 at 13:18

1 Answer 1

1

In Python 3, you could use a metaclass something like this to cache A objects with various different parameter sets.

This will fail if any of the parameters passed in are unhashable, though.

import inspect


class Singleton(type):
    def __call__(cls, *args, **kwargs):
        # Compute a cache key based on the arguments passed in and applying
        # any defaults from the original __init__ signature
        ar = inspect.signature(cls.__init__).bind_partial(*args, **kwargs)
        ar.apply_defaults()
        key = frozenset(ar.arguments.items())

        # Initialize the cache if it doesn't exist yet
        if not hasattr(cls, "_singleton_cache"):
            cls._singleton_cache = {}

        # If the key is in the cache, return it
        cache = cls._singleton_cache
        if key in cache:
            return cache[key]

        # Otherwise initialize a new object, save it and return it
        cache[key] = obj = super().__call__(*args, **kwargs)
        return obj


class A(metaclass=Singleton):
    def __init__(self, *, arg1, arg2, arg3, arg4="default"):
        self.arg1 = arg1
        self.arg2 = arg2
        self.arg3 = arg3
        self.arg4 = arg4


a = A(arg1=1, arg2=2, arg3=3)
b = A(arg1=1, arg2=2, arg3=3)
print(a is b)

EDIT: If you really, really want a Singleton metaclass that'll ignore any new argument sets, here you go...

class VerySingleton(type):
    def __call__(cls, *args, **kwargs):
        if not hasattr(cls, "_singleton_cache"):
            # Initialize the new object
            cls._singleton_cache = super().__call__(*args, **kwargs)
        return cls._singleton_cache


class A(metaclass=VerySingleton):
    def __init__(self, *, arg1, arg2, arg3, arg4="default"):
        self.arg1 = arg1
        self.arg2 = arg2
        self.arg3 = arg3
        self.arg4 = arg4


a = A(arg1=1, arg2=2, arg3=3)
b = A(arg1=1, arg2=2, arg3=0)
print(a is b)
print(a.arg3)
print(b.arg3)

This prints out

True
3
3

– no matter if b was constructed with arg3=0, that's just thrown away and the old instance is used, which, if you ask me, is thoroughly counterintuitive.

Sign up to request clarification or add additional context in comments.

5 Comments

I have tried to create a new instance with that and i could. I used it as follows c = A(arg1=5, arg2=2, arg3=0) print(a.arg1) print(c.arg1) print(a is c)
Yes, since arg3 is different there.
As _singleton_cache is private, the way to access the instance of the VerySingleton should be a static method in A? In the Singleton metaclass case, as it can have multiple instances, how do you access them?
You don't access the VerySingleton instance, it's the metaclass that constructs A. _singleton_cache lives on the subclass (e.g. A); see for yourself with print(A._singleton_cache).
I mean how do you access the A instance in both cases do you proposed. Singleton pattern has a get_instance() to access the instance. I could do it like A._singleton_cache in your last implementation, but if you make that attribute private i supose there is another way to access it.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.