attrs icon indicating copy to clipboard operation
attrs copied to clipboard

Is there a canonical way to create a singleton with attrs?

Open rohit507 opened this issue 3 years ago • 2 comments

I've got a few cases where I require a singleton object in my code and was wondering if there's an accepted way to create a singleton with attrs support for initialization and setattr.

#579 makes it reasonably clear that there won't be builtin support for singletons, but the pattern is still useful when you need a per-process unique instance of a class. That thread mentions a few alternatives, using the class directly and using a module. The latter option doesn't let me use dunders like __getitem__ or __class_getitem__ and neither option lets me use attrs' existing infrastructure for initialization, validation, or freezing.

Right now I add the following to an attrs class to make a singleton:

@define
class Config(): 

    def __new__(cls):
        """
        Creates a singleton object, if it is not created,
        or else returns the previous singleton object
        """

        if not hasattr(cls, "instance"):
            instance = super(Config, cls).__new__(cls)
            instance.__init__() 
            instance.__attrs_init__()
            cls.instance = instance

        return cls.instance

    def __init__(self):
        pass

This works well enough for my case, but I'm wondering if there's a better way to accomplish the same thing? Either an actual implementation for a singleton using attrs or an alternate design pattern that achieves the same goal.

rohit507 avatar Jun 05 '22 19:06 rohit507

In attrs we have exactly one singleton and that's _Nothing:

https://github.com/python-attrs/attrs/blob/4252adbab3aa05fe14b3112f31fbfc2edaa41e51/src/attr/_make.py#L56-L76

If you find a better way, pls let us know. :)


In my own code, I tend to enforce such policies using global variables + functions which is less magicy:

_config = None

def get_config():
    global _config
    if _config is None:
        _config = Config()

    return _config

hynek avatar Jun 07 '22 11:06 hynek

There's so many variables here that can affect how you'd implement this. Does it take zero arguments, and thus it's just a fancy sentinel object? Does it take arguments, and should use a cached instance based on what the arguments are? Or does it just ignore the arguments from the second time? Does it need to be made into a singleton lazily, or as soon as the module is imported?

It seems like it's outside the scope of attrs. The docs recommend a builder pattern over hooking in to initialization-- maybe they should recommend doing singleton logic outside of the class, and merely hiding the class itself? Or just not exposing the class outside of the module?

But also, if your singleton doesn't take any parameters-- what do you need attrs for?

(By the way: if your example is close to what you want, and you need something reusable...)
def singleton(class_):
    instance = None
    @functools.wraps(class_)
    def _wrapped():
        nonlocal instance
        if instance is None:
            instance = class_()
        return instance
    return _wrapped

KevinMGranger avatar Aug 10 '22 15:08 KevinMGranger