Python 实现单例的几种方式

在 GoF 四人帮的设计模式中描述了一种单例模式。
它的定义(即书中的意图)是,保证一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式描述的意图可以认为是一个协议,当我希望这样的时候,它应该是这样子。

仅有一个实例解释了这个模式的命名由来,因此,无论用任何语言实现,这个类的实例必须在该语言系统内是“唯一”的。
提供一个全局访问点 意味着我们可以通过一个全局的 API 拿到那个唯一的实例

在 C++ 中, 唯一性是指从进程开始创建到它结束生命,对象的地址不会发生变化。
因此它可以借助语言的两种特性实现这个唯一性

  • static 静态声明天然具有“只创建一次”的特性
  • new 运算并通过条件判断保证对象唯一性

Python 中的唯一性保证方式有哪些 ?

  • 对象创建并用条件保证对象唯一性,这一点和 C++ 的动态内存申请类似
  • import. Python的模块导入在语言层具有无论任何时候只导入一次的特点,把单例的类名和唯一对象单独放置于一个模块中,需要时导入

方案一

模仿 C++的实现方案

class Singleton(object):
    __instance = None
    def __new__(cls, *args, **kwargs):
        if cls.__instance is None:
            cls.__instance = super(Singleton, cls).__new__(cls)
        return cls.__instance

"""
Test:
objA = Singleton("Xiaoming")
objB = Singleton("Tom")
print(id(objA), id(objB)) 
assert(id(objA) == id(objB)) 
"""

这一招有赖于对 __new__ 用法的掌握

但是 在 Python 中,这样的实现有点“丑陋”

你能说出这种实现方案的几个缺点 ?

方案二:

使用模块
做法是,将你要封装成单例的代码封装在一个模块文件中, 如 singleton.py
内容如下:

class Handler(object):
    def __init__():
        pass  
    ### some design and imp

handler = Handler() 

使用方式:

from singleton import handler

这种方案比较 Pythonic ,非常优雅。单例的实现可以在文件内部尽情实现。
典型的应用就是日志组件,我们会用 logger 组件的日志句柄在程序到处打日志,把日志的书写规范定义好,放在一个文件中,然后 生成对象放在那里,就随处可用,而且它是唯一的。

import logging
import os
import sys
import time

FORMAT = '%(asctime)-4s %(levelname)s  %(filename)s  %(funcName)s %(lineno)d %(message)s'

cur_path = os.path.abspath(os.path.dirname(os.getcwd()))
logdir = cur_path + '/logs'
if not os.path.exists(logdir):
    os.mkdir(logdir)

logfile = logdir + "/serverlog_{}.log".format(time.strftime("%Y_%m_%d"))
# sys.stdout = logfile
logging.basicConfig(format=FORMAT,
                    datefmt='%m-%d %H:%M',
                    filename=logfile,
                    level=logging.DEBUG,
                    filemode='a')

logger = logging.getLogger("myapp")

使用时

from log import logger

logger.info("hello world")

方案三

装饰器.
基本的想法是设计一个装饰器函数,将一个类标记为单例。它就是单例。
这种方法的好处是显而易见的,可以随时随地制造一个单例类,而不需要经历一个比较复杂的过程,比如定义一个模块文件啦,实现 __new__啦。装饰器的方式将单例的创建过程抽象出来,可供大家复用,这也是装饰器独有的优势

此方案的关键在于单例装饰器函数的设计。
首先,因为装饰器是修饰类定义的,那么,它的参数须是一个类参数。其次,装饰器的返回值应当是一个对应类的唯一对象。

def Singleton(cls):
    __instance = {}
    def _singleton(*args, **kwargs):
        if cls not in __instance:
            __instance[cls] = cls(*args, **kwargs)
        return __instance[cls]
    return _singleton

@Singleton
class Foo(object):
    def __init__(self):
        pass

### 以下为测试代码,预期输出两个相同的整数
y1 = You()
y2 = You()
print (id(y1), id(y2))

当需要创建多个单例时,可以考虑装饰器方法。

方案四

类属性方法,与上面的方案二类似

class Singleton(object):
    def __init__(self, abc=None):
        pass

    @classmethod
    def _instance(cls, *args, **kwargs):
        if not hasattr(Singleton, "_instance"):
            Singleton._instance = Singleton(*args, **kwargs)
        return Singleton._instance

# 以下为测试代码,预期s1 和 s2 的id一致
s1 = Singleton("abc")
s2 = Singleton("efg")
print (id(s1), id(s2))

这种方案有何问题?

方案二和方案四基本上是一致的,在方案四种,获取单例的代码显得有点啰嗦,是这样的
obj = Singleton.instance()
方案二也可以改成 hasattr 的方式
这两者都有多线程冲突的问题。
根源在于判断 hasattr 下面的初始化代码,如果有IO操作,重新进入临界区的线程将会获取另一个对象,而切回上下文的那段代码将会对此浑然不觉。解决此问题的方案是对临界区加锁。我们需要 threading 模块

方案五

元类 metaclass ,很少用到

class SingletonType(type):
    def __init__(self,*args,**kwargs):
        super(SingletonType,self).__init__(*args,**kwargs)

    def __call__(cls, *args, **kwargs): # 这里的cls,即Foo类
        print('cls',cls)
        obj = cls.__new__(cls,*args, **kwargs)
        cls.__init__(obj,*args, **kwargs) # Foo.__init__(obj)
        return obj

class Foo(metaclass=SingletonType): # 指定创建Foo的type为SingletonType
    def __init__(self,name):
        self.name = name
    def __new__(cls, *args, **kwargs):
        return object.__new__(cls)

以上五种方案,无疑是装饰器和模块方案更为优雅,加锁的方案更多的是作为玩具玩完,因为引入多线程和互斥锁增加了复杂性,实际项目中,单例最多的是模块import方式实现,以至于没有见过的人感受不到其存在,以为 python没有单例实现,需要自己花式创造一个,这是不对的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容