Last Updated on

单例模式

单例模式一种常见的设计模式,该模式的主要目的是确保某一个类只有一个实例存在。当你希望在整个系统中,某个类只能出现一个实例时,单例对象就能派上用场。

比如,某个项目的配置信息存放在一个配置文件中,通过一个 Config 的类来读取配置文件的信息。如果在程序运行期间,有很多地方都需要使用配置文件的内容,也就是说,很多地方都需要创建 Config 对象的实例,这就导致系统中存在多个 Config 的实例对象,而这样会严重浪费内存资源,尤其是在配置文件内容很多的情况下。事实上,类似 Config 这样的类,我们希望在程序运行期间只存在一个实例对象。

在 Python 中,可以用多种方法实现单例模式,常见的有:

  • 通过使用模块
  • 通过装饰器
  • 通过__new__方法
  • 通过元类

下面就详细说明以下这几种实现方法:

通过使用模块

在Python中,Python 的模块就是天然的单例模式。

因为模块在第一次导入时,会生成 .pyc 文件,当第二次导入时,就会直接加载 .pyc 文件,而不会再次执行模块代码。因此,我们只需把相关的函数和数据定义在一个模块中,就可以获得一个单例对象了。

看如下示例:

# 例如在config.py文件中定义Config类和实例
class Config:
    LOG_LEVEL = 'INFO'
    ...

config = Config()


# 然后在其他文件中,通过import导入此实例
from config import config

print(config.LOG_LEVEL)
# 输出结果:INFO

如上所示,通过在文件中先生成一个类实例,然后在别的文件中直接导入这个类实例,就实现了单例模式。

通过装饰器

通过装饰器可以直接装饰类,使得类只存在一个实例,关于装饰器,不了解的请看:《Python 面向切面编程AOP和装饰器

看下面实例:

from functools import wraps

def Singleton(cls):
    instance = {}
    @wraps(cls)
    def wrapper(*args, **kwargs):
        if cls not in instance:
            instance[cls] = cls(*args, **kwargs)
        return instance[cls]
    return wrapper

@Singleton
class A(object):
    pass

a = A()
b = A()
print(a is b)


# 输出结果:
True

如上通过装饰器实现了单例模式,创建的实例a和b是指向相同内存地址,是同一个实例。

但是有一个问题需要注意:通过装饰器后,返回的将是一个函数,这样类A的__class__是函数,所以将无法继承。只能正常通过此类创建实例。

通过__new__方法

__new__方法是Python的魔法方法之一,在类创建实例时,则是通过调用__new__方法来实现的,所以我们可以改写类的__new__方法,使得类的实例只能存在一个。

看如下示例:

class A(object):
    def __new__(cls, *args, **kwargs):
        if not hasattr(cls, '_instance'):
            cls._instance = super().__new__(cls, *args, **kwargs)
        return cls._instance


class B(A):
    pass


a = A()
print(id(a))
b = B()
print(id(b))

# 输出结果:
4329510952
4329510952

注意:类无法继承通过单例类来实现单例! 为什么? 因为类的属性是可以继承的,这样如果你继承一个单例的类,那么父类的_instance属性就会被继承到子类中,即使子类重新定义了此属性,但是__new__方法也会继承,在此方法中,super()会去调用父类的__new__方法,最后就返回了父类的实例。如果你不能用super(),那么只能直接通过object.__new__(cls, *args, **kwargs)来创建实例了,这样你_instance和__new__方法都要重写!

如下:

class A(object):
    def __new__(cls, *args, **kwargs):
        if not hasattr(cls, '_instance'):
            cls._instance = super().__new__(cls, *args, **kwargs)
        return cls._instance


class B(A):
    _instance = None
    
    def __new__(cls, *args, **kwargs):
        if not hasattr(cls, '_instance'):
            cls._instance = object.__new__(cls, *args, **kwargs)
        return cls._instance

a = A()
print(id(a))

b = B()
b2 = B()
print(id(b))
print(id(b2))


# 输出结果:
4493722624
4493975392
4493975392

如上所示,在子类B中,重新定义_instance=None,并且在__new__函数中,并没有使用super()函数,不然就会调用父类的__new__,就又会返回父类的实例。

所以,在定义单例类的时候,是注意考虑清楚类的继承关系等。

通过元类

我们知道类是元类的实例,我们可以通过修改元类,来使得创建此类时,就直接是一个单例类。

关于元类的问题,如果不了解的,请看:《Python中的元类(metaclass)

看下面示例:

class Singleton(type):
    def __init__(cls, *args, **kwargs):
        cls.__instance = None
        super().__init__(*args,**kwargs)

    def __call__(cls, *args, **kwargs):
        if not cls.__instance:
            cls.__instance = super().__call__(*args,**kwargs)
        return cls.__instance


class A(metaclass=Singleton):
    pass


class B(A):
    pass


a = A()
a2 = A()
b = B()
b2 = B()
print(id(a))
print(id(a2))
print(id(b))
print(id(b2))


# 输出结果:
4526019528
4526019528
4526019584
4526019584

如上所示,元类是可以继承的,通过元类实现单例模式,在原理中去定去和修改类的属性,通过类的属性判断是实现单例。

而且这可以完美继承,使得继承了单例类的类也成为单例类。

总结

Python中单例类的实现有很多方式,但是这些方法或多或少都有一些注意事项。

总结的注意事项如下:

通过装饰器实现时,被装饰后的类就会变为函数,无法继承。

通过__new__实现时,如果此单例类有子类,那么子类也是单例类,且实例化会返回父类的实例,子类需要重写__new__方法和重制类属性。

通过元类实现时,元类会被继承,使得单例类的子类也时单例类。

在大家实现单例类的时候,需要注意一下,根据实际的项目情况选择最合适的实现方式。这些实现方式里面大概只有通过导入的方式是最稳定,没有问题的。


另外,单例类还有一个多线程创建的问题

就是单例类在使用多线程创建时,当创建实例,初始化时,需要一点时间,那么就会导致数据不同步的问题,导致出现多个实例。只有通过import导入模块的方式实现才没有这个问题。但这个问题可以通过线程锁来解决

这在后面将到多线程,多进程的时候再单独说吧。

Ok,Python的单例模式,大概就说到这里,有什么问题,欢迎留言交流。