建议50:Python 中的单例模式
一、利用模块实现单例模式
满足单例模式的 3 个需求:
-
只能有一个实例
-
必须自行创建这个实例
-
必须自行向整个系统提供这个实例
下面我们使用 Python 实现一个带锁的单例:
class Singleton(object):
objs = {}
objs_locker = threading.Lock()
def __new__(cls, *args, **kw):
if cls in cls.objs:
return cls.objs(cls)
cls.objs_locker.acquire()
try:
if cls in cls.objs:
return cls.objs(cls)
cls.objs[cls] = object.__new__(cls)
finally:
cls.objs_locker.release()
当然这种方案也存在问题:
-
如果 Singleton 的子类重载了__new__(),会覆盖或干扰 Singleton 类中__new__()的执行
-
如果子类有__init__(),那么每次实例化该 Singleton 的时候,__init__()都会被调用,这显然是不应该的
虽然以上问题都有解决方案,但让单例的实现不够 Pythonic。我们可以重新审视 Python 的语法元素,发现模块采用的其实是天然的单例的实现方式:
-
所有的变量都会绑定到模块
-
模块只初始化一次
-
import 机制是线程安全的,保证了在并发状态下模块也只是一个实例
# World.py import Sun def run(): while True: Sun.rise() Sun.set() # main.py import World World.run()
二、单例模式
单例模式(Singleton Pattern)是一种常用的软件设计模式,该模式的主要目的是确保某一个类只有一个实例存在。当你希望在整个系统中,某个类只能出现一个实例时,单例对象就能派上用场。
比如,某个服务器程序的配置信息存放在一个文件中,客户端通过一个 AppConfig 的类来读取配置文件的信息。如果在程序运行期间,有很多地方都需要使用配置文件的内容,也就是说,很多地方都需要创建 AppConfig 对象的实例,这就导致系统中存在多个 AppConfig 的实例对象,而这样会严重浪费内存资源,尤其是在配置文件内容很多的情况下。事实上,类似 AppConfig 这样的类,我们希望在程序运行期间只存在一个实例对象。
在 Python 中,我们可以用多种方法来实现单例模式:
- 使用模块
- 使用
__new__
- 使用装饰器(decorator)
- 使用元类(metaclass)
使用模块
其实,Python 的模块就是天然的单例模式,因为模块在第一次导入时,会生成 .pyc
文件,当第二次导入时,就会直接加载 .pyc
文件,而不会再次执行模块代码。因此,我们只需把相关的函数和数据定义在一个模块中,就可以获得一个单例对象了。如果我们真的想要一个单例类,可以考虑这样做:
# mysingleton.py class My_Singleton(object): def foo(self): pass my_singleton = My_Singleton()
将上面的代码保存在文件 mysingleton.py
中,然后这样使用:
from mysingleton import my_singleton
my_singleton.foo()
使用 __new__
为了使类只能出现一个实例,我们可以使用 __new__
来控制实例的创建过程,代码如下:
class Singleton(object): _instance = None def __new__(cls, *args, **kw): if not cls._instance: cls._instance = super(Singleton, cls).__new__(cls, *args, **kw) return cls._instance class MyClass(Singleton): a = 1
在上面的代码中,我们将类的实例和一个类变量 _instance
关联起来,如果 cls._instance
为 None 则创建实例,否则直接返回 cls._instance
。
执行情况如下:
>> one = MyClass() >>> two = MyClass() >>> one == two True >>> one is two True >>> id(one), id(two) (4303862608, 4303862608)
使用装饰器
我们知道,装饰器(decorator)可以动态地修改一个类或函数的功能。这里,我们也可以使用装饰器来装饰某个类,使其只能生成一个实例,代码如下:
from functools import wraps def singleton(cls): instances = {} @wraps(cls) def getinstance(*args, **kw): if cls not in instances: instances[cls] = cls(*args, **kw) return instances[cls] return getinstance @singleton class MyClass(object): a = 1
在上面,我们定义了一个装饰器 singleton
,它返回了一个内部函数 getinstance
,该函数会判断某个类是否在字典 instances
中,如果不存在,则会将 cls
作为 key,cls(*args, **kw)
作为 value 存到 instances
中,否则,直接返回 instances[cls]
。
使用 metaclass
元类(metaclass)可以控制类的创建过程,它主要做三件事:
- 拦截类的创建
- 修改类的定义
- 返回修改后的类
使用元类实现单例模式的代码如下:
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(object): __metaclass__ = Singleton # Python3 # class MyClass(metaclass=Singleton): # pass
小结
- Python 的模块是天然的单例模式,这在大部分情况下应该是够用的,当然,我们也可以使用装饰器、元类等方法
三、多线程情况下的单实例
以上例子中有一个问题我们没有解决,那就是多线程的问题,当有多个线程同时去初始化对象时,就很可能同时判断__instance is None,从而进入初始化instance的代码中。所以为了解决这个问题,我们必须通过同步锁来解决这个问题。以下例子来自
import threading try: from synchronize import make_synchronized except ImportError: def make_synchronized(func): import threading func.__lock__ = threading.Lock() def synced_func(*args, **kws): with func.__lock__: return func(*args, **kws) return synced_func class Singleton(object): instance = None @make_synchronized def __new__(cls, *args, **kwargs): if cls.instance is None: cls.instance = object.__new__(cls, *args, **kwargs) return cls.instance def __init__(self): self.blog = "xiaorui.cc" def go(self): pass def worker(): e = Singleton() print id(e) e.go() def test(): e1 = Singleton() e2 = Singleton() e1.blog = 123 print e1.blog print e2.blog print id(e1) print id(e2) if __name__ == "__main__": test() task = [] for one in range(30): t = threading.Thread(target=worker) task.append(t) for one in task: one.start() for one in task: one.join()
至此我们的单例模式实现代码已经接近完美了,不过我们是否可以更简单地使用单例模式呢?答案是有的,接下来就看看如何更简单地使用单例模式。
怎么用
在Python的官方网站给了两个例子是用装饰符来修饰类,从而使得类变成了单例模式,使得我们可以通过更加简单的方式去实现单例模式
例子:(这里只给出一个例子,因为更简单,另外一个大家可以看官网Singleton
def singleton(cls): instance = cls() instance.__call__ = lambda: instance return instance # # Sample use # @singleton class Highlander: x = 100 # Of course you can have any attributes or methods you like. Highlander() is Highlander() is Highlander #=> True id(Highlander()) == id(Highlander) #=> True Highlander().x == Highlander.x == 100 #=> True Highlander.x = 50 Highlander().x == Highlander.x == 50 #=> True
这里简单解释下:
- 在定义class Highlander的时候已经执行完所有singleton装饰器中的代码,得到了一个instance,所以这之后所有对Highlander的调用实际上是在调用instance的_call_ 方法。
- 我们通过lambda函数定义了_call_方法让它始终返回instance,因此Highlander()和Highlander都返回instance
- 同时由于在类定义代码执行时就已经创建了instance,所以后续不论是多线程还是单线程,在调用Highlander时都是在调用instance的_call_方法,也就无需同步了。
最后我想说的是这种方法简直碉堡了~~~
附上我用于多线程的测试代码
import threading def singleton(cls): instance = cls() instance.__call__ = lambda: instance return instance @singleton class Highlander: x = 100 # Of course you can have any attributes or methods you like. def worker(): hl = Highlander() hl.x += 1 print hl print hl.x def main(): threads = [] for _ in xrange(50): t = threading.Thread(target=worker) threads.append(t) for t in threads: t.start() for t in threads: t.join() if __name__ == '__main__': main()
这里的代码有一点小问题,就是在打印的时候有可能x属性已经被别的线程+1了,所以有可能导致同一个数打印多次,而有的数没有打印,但是不影响最终x属性的结果,所以当所有线程结束之后,属性x最终的值是可以保证正确的。
四、再来一个实际的例子:
class Application(tornado.web.Application): u"""程序唯一实例, 管理公用资源分配""" __shared_state = {} _firstinit = True _instance_lock = threading.Lock() max_workers = default.MAX_WORKERS rp = RedisAccess( host=default.RedisUrl, port=default.RedisPort, db=default.RedisDb, max_connections=default.MAX_WORKERS) def __init__(self, options=None, capp=None, events=None, io_loop=None, **kwargs): global pid if pid != os.getpid(): global _db1, _db2, _db3, _db4 _db1 = dbutils() _db2 = dbutils() _db3 = dbutils() _db4 = dbutils() pid = os.getpid() self.__dict__ = self.__shared_state if self._firstinit is True: with self._instance_lock: if Application._firstinit is True: super(Application, self).__init__(**kwargs) # 线程资源单独分配 self.thdata = threading.local() # 公共配置 self.options = default_options # memcache客户端 self.mc = memcache.Client([default.MemCacheUrl], debug=0) self.log = logger Application._firstinit = False
posted on 2018-02-11 11:06 myworldworld 阅读(161) 评论(0) 收藏 举报