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',)

Raulillo
  • 92
  • 13

1 Answers1

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.

AKX
  • 93,995
  • 11
  • 81
  • 98
  • 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)` – Raulillo Jul 23 '19 at 13:30
  • Yes, since `arg3` is different there. – AKX Jul 23 '19 at 13:42
  • 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? – Raulillo Jul 24 '19 at 10:28
  • 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)`. – AKX Jul 24 '19 at 10:35
  • I mean how do you access the A instance in both cases do you proposed. [Singleton pattern](https://en.wikipedia.org/wiki/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. – Raulillo Jul 24 '19 at 10:59