建议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

 

这里简单解释下:

  1. 在定义class Highlander的时候已经执行完所有singleton装饰器中的代码,得到了一个instance,所以这之后所有对Highlander的调用实际上是在调用instance的_call_ 方法。
  2. 我们通过lambda函数定义了_call_方法让它始终返回instance,因此Highlander()和Highlander都返回instance
  3. 同时由于在类定义代码执行时就已经创建了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)    收藏  举报

导航