美文网首页
Python 实现单例的几种方式

Python 实现单例的几种方式

作者: 东方胖 | 来源:发表于2022-02-11 19:05 被阅读0次

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

相关文章

网友评论

      本文标题:Python 实现单例的几种方式

      本文链接:https://www.haomeiwen.com/subject/liqokrtx.html