在Python中创建单例模式

1655
这个问题并不讨论单例设计模式是否可取,是否是反模式或任何宗教战争,而是讨论如何以最符合Python风格的方式实现此模式。在这种情况下,我将“最符合Python风格”定义为遵循“最小惊奇原则”。
我有多个类需要成为单例(我的用例是一个记录器,但这不重要)。我不希望在几个类中添加额外的内容,而是可以简单地继承或装饰。
最佳方法:

方法1:装饰器

def singleton(class_):
    instances = {}
    def getinstance(*args, **kwargs):
        if class_ not in instances:
            instances[class_] = class_(*args, **kwargs)
        return instances[class_]
    return getinstance

@singleton
class MyClass(BaseClass):
    pass

优点

  • 装饰器的特性通常比多重继承更加直观。

缺点

  • While objects created using MyClass() would be true singleton objects, MyClass itself is a function, not a class, so you cannot call class methods from it. Also for

    x = MyClass();
    y = MyClass();
    t = type(n)();
    

那么x == y,但是x != t && y != t


方法二:基类
class Singleton(object):
    _instance = None
    def __new__(class_, *args, **kwargs):
        if not isinstance(class_._instance, class_):
            class_._instance = object.__new__(class_, *args, **kwargs)
        return class_._instance

class MyClass(Singleton, BaseClass):
    pass

优点

  • 它是一个真正的类

缺点

  • 多重继承 - 呃!从第二个基类继承时可能会覆盖__new__?这需要比必要更多的思考。

方法三:元类

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]

#Python2
class MyClass(BaseClass):
    __metaclass__ = Singleton

#Python3
class MyClass(BaseClass, metaclass=Singleton):
    pass

优点

  • 它是一个真正的类
  • 自动覆盖继承
  • 使用__metaclass__实现其正确的目的(并让我意识到了这一点)

缺点

  • 有吗?

方法四:返回同名类的装饰器


def singleton(class_):
    class class_w(class_):
        _instance = None
        def __new__(class_, *args, **kwargs):
            if class_w._instance is None:
                class_w._instance = super(class_w,
                                    class_).__new__(class_,
                                                    *args,
                                                    **kwargs)
                class_w._instance._sealed = False
            return class_w._instance
        def __init__(self, *args, **kwargs):
            if self._sealed:
                return
            super(class_w, self).__init__(*args, **kwargs)
            self._sealed = True
    class_w.__name__ = class_.__name__
    return class_w

@singleton
class MyClass(BaseClass):
    pass

优点

  • 它是一个真正的类
  • 自动地覆盖继承

缺点

  • 创建每个新类是否有开销?在这里,我们为每个要使其成为单例的类创建了两个类。虽然在我的情况下这很好,但我担心这可能不能扩展。当然,是否应该轻松扩展这种模式存在争议...
  • _sealed属性的意义是什么
  • 无法使用super()调用基类上同名的方法,因为它们会递归。这意味着您无法自定义__new__并且无法子类化需要您调用__init__的类。

方法五:一个模块

一个名为singleton.py的模块文件。

优点

  • 简单胜于复杂。

缺点

  • 不是延迟实例化的。

20
另外三种技术:使用模块替代(通常情况下,我认为这是Python中更适合的模式,但这也取决于你用它做什么);创建一个单一实例并直接操作它(foo.x 或者如果你坚持可以用 Foo.x 替代 Foo().x);使用类属性和静态/类方法(Foo.x)。 - Chris Morgan
23
如果你只打算使用类方法或静态方法,那么实际上就不需要创建一个类了。 - Cat Plus Plus
5
@猫: 效果类似,但创建全局变量的原因可能是任何事情,包括不知道更好的方法。为什么要创建一个单例?如果你不知道答案,那就不应该在这里。这种明确性不仅更符合Python的风格,而且使维护更加简单。是的,单例是全局变量的语法糖,但类也是一堆难看东西的语法糖,没有人会告诉你总是最好不用它们。 - theheadofabroom
35
反单例情绪是最糟糕的模拟船运动编程。对于那些听过(很少有人真正阅读过)“Goto语句被认为是有害的”并认为无论上下文如何,goto语句都是糟糕代码的标志的人也是如此。 - Hejazzman
5
你好,感谢你详细的帖子。我对模式编程和Python都比较新,而且我很惊讶尽管方法2似乎是最为人熟知的一种(它无处不在),但几乎没有人提到每次使用Singleton()或MyClass()时,尽管仅创建了一个对象,init()也会被调用。我没有尝试过,但据我所知,其他方法也是如此。 当实现单例模式时,这似乎并不理想,难道我漏掉了什么吗?当然,解决方案是设置属性以避免两次执行__init__。只是好奇。 - chrisvp
显示剩余30条评论
42个回答

1
如果您不需要对单例实例进行延迟初始化,则以下内容应该很容易且线程安全:
class A:
    instance = None
    # Methods and variables of the class/object A follow
A.instance = A()

这样,A是在模块导入时初始化的单例。

1

在苦苦挣扎了一段时间后,我最终想出了以下解决方案,以便当从不同的模块中调用时,配置对象只会被加载一次。元类允许全局类实例存储在builtins字典中,目前看来这是存储适当程序全局变量的最简洁方法。

import builtins

# -----------------------------------------------------------------------------
# So..... you would expect that a class would be "global" in scope, however
#   when different modules use this,
#   EACH ONE effectively has its own class namespace.  
#   In order to get around this, we use a metaclass to intercept
#   "new" and provide the "truly global metaclass instance" if it already exists

class MetaConfig(type):
    def __new__(cls, name, bases, dct):
        try:
            class_inst = builtins.CONFIG_singleton

        except AttributeError:
            class_inst = super().__new__(cls, name, bases, dct)
            builtins.CONFIG_singleton = class_inst
            class_inst.do_load()

        return class_inst

# -----------------------------------------------------------------------------

class Config(metaclass=MetaConfig):

    config_attr = None

    @classmethod
    def do_load(cls):
        ...<load-cfg-from-file>...

1
我用了这个。
def singleton(cls):
    instances = dict()

    def __new__(klass, *args, **kwargs):
        klass_path = klass.__module__ + '.' + klass.__name__
        instance = instances.get(klass_path, None)
        if instance is not None:
            return instance

        attributes = dict(klass.__dict__)
        attributes.pop('__new__')

        klass = type(klass.__name__, klass.__bases__, attributes)
        instances[klass_path] = klass(*args, **kwargs)
        return instances[klass_path]

    cls.__new__ = __new__
    return cls


@singleton
class ABC:
    pass


@singleton
class QWERTY:
    pass


print('id:', id(ABC()))
print('id:', id(ABC()))
print('id:', id(QWERTY()))
print('id:', id(QWERTY()))

输出如下:
id: 4396645488
id: 4396645488
id: 4396790976
id: 4396790976

0

我更喜欢使用静态方法GetInstance()来创建单例对象(也不允许使用其他方法),以强调我正在使用单例设计模式。

import inspect
class SingletonMeta(type):
    __instances = {}
    GET_INSTANCE = 'GetInstance' # class method ussed to create Singleton instance

    def __call__(cls, *args, **kwargs):
        caller_frame = inspect.currentframe().f_back

        caller_class = caller_frame.f_locals.get('cls_ref')
        caller_method_name = caller_frame.f_code.co_name
        if caller_class is cls and \
            caller_method_name == SingletonMeta.GET_INSTANCE:
            obj = super(SingletonMeta, cls).__call__(*args, **kwargs)
        else:
            raise Exception(f"Class '{cls.__name__}' is a singleton! Use '{cls.__name__}.{SingletonMeta.GET_INSTANCE}()' to create its instance.")

        return obj

    def __new__(cls, name, bases, dct):
        def GetInstance(cls_ref):
            if cls_ref not in cls_ref.__instances:
                cls_ref.__instances[cls_ref] = cls_ref()

            return cls_ref.__instances[cls_ref]
       
        return super().__new__(cls, name, bases, {**dct, GetInstance.__name__: classmethod(GetInstance)})
#------------------------
if __name__ == '__main__':
    class SingletonSample1(metaclass=SingletonMeta):
        def __init__(self):
            self.__x = 1

        @property
        def x(self) -> int:
            return self.__x

        @x.setter
        def x(self, value):
            self.__x = value

    s1 = SingletonSample1.GetInstance()
    s1.x = 3

    try:
        s2 = SingletonSample1()
    Exception as error:
        print(repr(error))

0
我知道我可能来晚了,但我认为元类的方法可以改进。我们不需要一个字典来将类映射到其单例实例。我们可以简单地将单例实例作为类的属性。请记住,元类的实例是类,所以元类的实例属性是类属性(对于来自Java或C#的人来说是静态属性)。以下是实现方法:
class Singleton(type):
    _instance = None  # This is an attribute of the class

    def __call__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__call__(*args, **kwargs)
            return cls._instance
        name = cls.__name__
        raise RuntimeError(
            f"{name} already created. use {name}.get() to get the {name} instance."
        )

    def get(cls):
        return cls._instance


class Foo(metaclass=Singleton):
    def __init__(self, a, b):
        self.a = a
        self.b = b


class Bar(metaclass=Singleton):
    def __init__(self, a, b):
        self.a = a
        self.b = b


f = Foo(1, 2)
print(f)
print(f.a, f.b)
print(Foo.get() is f)
# f2 = Foo(3, 4)  # error

b = Bar(3, 4)
print(b)
print(b.a, b.b)
print(Bar.get() is b)
b.a = 5
# b2 = Bar(3, 4)  # error

print("Foo", f.a, f.b)
print("Bar", b.a, b.b)

这段代码会打印出以下内容:
<__main__.Foo object at 0x00000224A192FD90>
1 2
True
<__main__.Bar object at 0x00000224A192FE10>
3 4
True
Foo 1 2
Bar 5 4

如你所见,每个类都有自己的单个_instance,需要像正常实例化一样进行实例化,并且稍后可以通过get()类方法访问。尝试创建另一个实例将引发RuntimeError
当然,你可以修改代码,如果已经创建了实例,则返回现有实例而不是引发错误,并且可以实现__new__(cls, *args, **kwargs),在定义类时立即创建实例,而不是在首次调用构造函数时进行延迟创建,但这些都是你可以根据需要轻松进行的修改。
这样就不再需要像问题和第一个答案中那样将类映射到它们的单例实例的字典。这样实现更简单,可能稍微更快,因为我们使用简单的属性访问而不是字典访问(我没有运行任何基准测试,所以不要引用我)。

0

0

我想指出的是,第一种方法定义了一个查找字典,直到今天我仍然不理解它,而且我看到这个解决方案在各个地方都在传播,所以我猜每个人都是从这里复制粘贴的。

我说的是这个:

def singleton(class_):
    instances = {} # <-- will always only have one entry.
    def getinstance(*args, **kwargs):
        if class_ not in instances:
            instances[class_] = class_(*args, **kwargs)
        return instances[class_]
    return getinstance

对于元类解决方案来说是有意义的,但使用一次性装饰器解决方案时,每次调用装饰器都会定义一个新函数,以及一个新的实例变量,因此每个“实例”始终只有一个条目,除非将其设置为全局变量。它也无法与继承一起使用。

类似的、更简单、更易调整的解决方案:

def singleton(class_):
    def wrapper(*args, **kwargs):
        if not wrapper._instance:
            wrapper._instance = class_(*args, **kwargs)
        return wrapper._instance

    wrapper._instance = None
    return wrapper

添加一个简单的

    ...
    wrapper.__wrapped__ = class_
    return wrapper

同时还允许继承或模拟,通过访问__wrapped__,这也是内部字典查找无法实现的。

(当然,如果我没有理解字典查找背后的奥秘,请原谅我。也许是因为我没有理解它背后特定的意图所以出了错)


0
这里是一个简单的实现,结合了@agf和@(Siddhesh Suhas Sathe)的解决方案,它使用元类并考虑构造函数参数,因此如果您使用完全相同的参数创建了foo类,则可以返回相同的实例。

class SingletonMeta(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        """
        Possible changes to the value of the `__init__` argument do not affect
        the returned instance.
        """
        cls_instances = cls._instances.get(cls) or []
        matching_instances = list(
            filter(
                lambda x: x["args"] == args and x["kwargs"] == kwargs,
                cls_instances,
            )
        )
        if len(matching_instances) == 1:
            return matching_instances[0]["instance"]
        else:
            instance = super().__call__(*args, **kwargs)
            cls_instances.append({"instance": instance, "args": args, "kwargs": kwargs})
            cls._instances[cls] = cls_instances
            return instance


class foo(metaclass=SingletonMeta):
    def __init__(self, param, k_param=None) -> None:
        print("Creating new instance")
        self.param = param
        self.k_param = k_param
        self._creation_time = time.time()

0

正如@Staale在这里提到的那样,Python中最简单的创建单例的方法是使用具有全局变量(作为“属性”)和全局函数(作为“方法”)的模块。但我想要补充一些非常重要的内容:继承在这里也适用!

你所需要做的就是创建一个'B.py'的“单例模块”,它从另一个“单例模块”'A.py'继承。只需在B.py中加入以下代码:from A import *,这将尊重私有变量(默认不导入它们)。


0
老实说,我找到了一个非常简单的解决方案:
@lambda x: x()
class ClassA:
    ...

这确实使ClassA成为ClassA的一个实例,并使未实例化的类无法访问。如果你想要更灵活一点的东西,你可以定义一个方法,像这样:
def singleton(*args, **kwargs):
    def _decorator(cls):
        return cls(*args, **kwargs)
    return decorator

@singleton(...)
class ClassA:
    def __init__(self, ...):
        ...

虽然我无法想象为什么你会在你的init类中添加参数,如果你想要一个单例,那么让我们简化成这个函数,它与lambda函数相同但更清晰:
def singleton(cls):
    return cls()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接