在 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没有单例实现,需要自己花式创造一个,这是不对的。
网友评论