阅读 1456

Python 中 Singleton 的写法及其拓展

为何要有 Singleton ?

重要性无需多言,我们在项目中经常有「要一个进程全局的变量(内存块)」的需求,而且单例模式是几种设计模式中最容易的。

偷懒且有用的做法:模块级别常量

我经常使用这种方式,因为简单且不易出错。

众所周知,Python 的 module 概念,是一个天然的 Singleton。而且 Python 是多范式语言,可以不必像 Java 那样使用 class 去处理这件事情,在 module 级别定义一个常量,是一种很自然的想法。

代码如下:

  • 定义方
singleton.py

class _MySingleton(object):
    """
    我们使用下划线开头, 告诫调用者, 不要直接 new
    也不要来访问这个class
    """
    def __init__(self, name, age):
        self._name = name
        self._age = age

    def print_name(self):
        print(self._name)

# 可以定制多个全局实例
S1 = _MySingleton('s1', 22)
S2 = _MySingleton('s2', 11)
复制代码
  • 调用方
caller.py

from singleton import S1

# 尽情使用S1( 在任意点import 都可以 ), 它是全局唯一的!
复制代码

正规做法:元编程

有些人不喜欢上面那种做法,他们认为「破坏了代码的纯粹性」,这个时候我们可以使用元编程的方式,让我们更近一步。

这是从 Python Cookbook 摘取出来的代码。

  • 元类基类
class SingletonMetaclass(type):
    def __init__(self, *args, **kwargs):
        self.__instance = None
        super().__init__(*args, **kwargs)

    def __call__(self, *args, **kwargs):
        if self.__instance is None:
            self.__instance = super(SingletonMetaclass, self).__call__(*args, **kwargs)
            return self.__instance
        else:
            return self.__instance
复制代码
  • 继承了元类基类的类
class SpamSingleton(metaclass=SingletonMetaclass):
    """注意: 根据Singleton的定义, 构造函数一般需要使用默认的构造函数"""
    def get_addr(self):
        return id(self)
复制代码
  • 调用方
from singleton import SpamSingleton

s1 = SpamSingleton()
s2 = SpamSingleton()

pritn(id(s1), id(s2))   # 内存地址是一样的
复制代码

老实说,理解 SingletonMetaclass 的作用过程还是有点困难的,我花了好久才搞清楚上面各个方法的调用流程。

不过 SingletonMetaclass 的作用也是巨大的,我们定义将其放入一个 base.py 的文件中,任何时刻我们想要定义某个 Singleton class,直接从其继承即可,简单且方便。

比较 Hack 的做法:Borg 模式

不多说,搜索引擎搜出来的结果,全都是推荐这种做法,让人以为这是「主流做法」(其实并不是)。

这种做法,修改了 Singleton 的定义,即:所有变量共享一个内存块,但是这个内存块的内容是可变化的。然后通过将实例的 __dict__ 方法重定向 class 的 __dict__ 方法,以达到其目的。

但是这种做法不是很符合我对 Singleton的感知,即:全局唯一,且其内容最好也不要变化。所以在实际开发中,并不喜欢这种做法。

更进一步

通常来说,更好的方法则是:在应用代码之外引入一个Manager,让这个Manager来为我们创建和管理单例。这种做法也很普遍,就是我们通常所说的「依赖注入框架」。

如果使用 Spring,那么一切都是很美妙的;如果没有使用 Spring,我经常使用 Guice 来做我的依赖注入框架。

不过 Python 社区貌似对「依赖注入框架」、「IoC容器」等等都不怎么感冒(其实是好东西),在此先不提。

总结

从个人的倾向来看,比较喜欢「掌握一种或两种方式,然后使用最熟练,而且不出错」的观点,所以我只推荐第一二种方式。

关注下面的标签,发现更多相似文章
评论