python面试系列之实现单例模式

1. 前言

在面试中通常会被问到单例模式,那么什么是单例模式呢?说的简单点就是,在一个进程中只能创建出一个类的实例。在《剑指offer》中的面试题02就是考查的单例模式的实现。

2. 模块导入实现单例

在singleton.py中定义一个类,创建一个该类的实例,别的脚本直接导入此实例。

# singleton.py
class Singleton(object):
    def foo(self):
        pass
singleton = Singleton()

然后在其它脚本中导入此模块中的变量singleton。

from singleton import singleton

2. 装饰器实现单例

2.1 函数装饰器实现单例

由于装饰器是基于闭包实现的,经过装饰之后,实际上Test指向的是wrapper函数,而wrapper函数中用到了变量instance,因此instance这个变量是一直不会释放的,所以只会得到一个实例。

def singleton(cls):
	instance = None
	print('被装饰的类是{}'.format(cls.__name__))
	def wrapper(*args, **kwagrs):
		if instance is None:
			instance = cls(*args, **kwargs)
		return instance
	return wrapper

@singleton
class Test(object):
	def __init__(self, *args, **kwargs):
		pass

if __name__ == '__main__':
	s1 = Test()
	s2 = Test()
	print(id(s1))
	print(id(s2))
		

上面的例子是错误的,运行的时候会报错。

被装饰的类是Test
Traceback (most recent call last):
  File "singleton.py", line 47, in <module>
    s1 = Test()
  File "singleton.py", line 29, in wrapper
    if instance is None:
UnboundLocalError: local variable 'instance' referenced before assignment

这是因为在闭包中,内部函数访问外部函数的局部变量导致的错误。详细的解释请移步 【python】*函数:全局局部变量、内部函数、闭包

关于全局变量和局部变量请移步 Python大盘点之全局变量、局部变量、类变量、实例变量
解决方式有两种,一是instace赋值为一个容器,而不是None(在闭包中内部函数可以访问外部函数的变量,但是只有当该变量为可变对象时,才能修改!这一点类似在函数中访问和修改全局变量!);二是使用nonlocal关键字。正确的例子如下:

# 解决方法1
def singleton(cls):
	instance = {} # 定义为一个容器,而不是None
	print('被装饰的类是{}'.format(cls.__name__))
	def wrapper(*args, **kwagrs):
		if not instance.get(cls):
			instance[cls] = cls(*args, **kwargs)
		return instance[cls]
	return wrapper

# 解决方法2
def singleton(cls):
	instance = None
	print('被装饰的类是{}'.format(cls.__name__))
	def wrapper(*args, **kwagrs):
		nonlocal instance # 声明instance不是wrapper函数的局部变量
		if instance is None:
			instance = cls(*args, **kwargs)
		return instance
	return wrapper

@singleton
class Test(object):
	def __init__(self, *args, **kwargs):
		pass
		

if __name__ == '__main__':
	s1 = Test()
	s2 = Test()
	print(id(s1))
	print(id(s2))
		

2.2 类装饰器实现单例

class Singleton(object):
    #不能将_instance定义为类属性,否则装饰完一个类之后,再装饰其他类会都是第一个被装饰类的实例。
	def __init__(self, cls):
        self._cls = cls
        self._instance = None # 必须是实例属性
    
    def __call__(self, *args, **kwargs):
        if self._instance is None:
            self._instance = self._cls(*args, **kwargs)
        return self._instance

@Singleton
class Test(object):
	def __init__(self, *args, **kwargs):
		pass

if __name__ == '__main__':
	s1 = Test()
	s2 = Test()
	print(id(s1))
	print(id(s2))

3. 多线程实现单例

上面的单例实现都是单线程,如果要在多线程中使用就会出现bug了。例如:

import threading # 多线程
from concurrent.futures import ThreadPoolExecutor # 线程池

class Singleton(object):
    #不能将_instance定义为类属性,否则装饰完一个类之后,再装饰其他类会都是第一个被装饰类的实例。
    def __init__(self, cls):
        self._cls = cls
        self._instance = None # 必须是实例属性
    
    def __call__(self, *args, **kwargs):
        if self._instance is None:
            self._instance = self._cls(*args, **kwargs)
        return self._instance
        

@Singleton
class Test(object):
    def __init__(self, *args, **kwargs):
        print('本实例的id是{}'.format(id(self)))

if __name__ == '__main__':
   	for i in range(5):
		t = threading.Thread(target=Test, args=())
		t.start()
		t.join()

	# my_list = range(5)
	# with ThreadPoolExecutor(max_workers=5) as executor:
	# 	results = executor.map(Test, my_list)

执行上面的脚本,输出如下:

本实例的id1780932059656
本实例的id1780929723080
本实例的id1780932077384
本实例的id1780932059656
本实例的id1780929723080

多进程不用说了,肯定每个进程创建的单例的那个实例是不一样的。因为每个进程都是独立的,不共享。

多线程时,使用上面的这个方式都不能真正实现单例模式,因为执行太快了,在判断if self._instance is None:时,的确还没有任何一个线程创建了该类的实例对象,但是走到下一步创建实例对象时,其实别的线程已经创建了实例对象了。在多线程的环境下实现单例模式才是真正在实际业务中真正会遇到的,这就需要用到线程锁了。

import threading # 多线程
from concurrent.futures import ThreadPoolExecutor # 线程池

class Singleton(object):
    #不能将_instance定义为类属性,否则装饰完一个类之后,再装饰其他类会都是第一个被装饰类的实例。
    def __init__(self, cls):
        self._cls = cls
        self._instance = None # 必须是实例属性
        self._lock = threading.Lock() # 创建一个线程锁
    
    def __call__(self, *args, **kwargs):
    	with self._lock:
	        if self._instance is None:
	            self._instance = self._cls(*args, **kwargs)
        return self._instance
        

@Singleton
class Test(object):
    def __init__(self, *args, **kwargs):
        print('本实例的id是{}'.format(id(self)))

if __name__ == '__main__':
   	for i in range(5):
		t = threading.Thread(target=Test, args=())
		t.start()
		t.join()

输出结果如下:

本实例的id1669934093576

为什么只 print了一句呢,这就是因为只创建了一次类的实例对象,所以只会调用__init__()方法一次。

优化上面的加锁部分,因为当一个线程成功创建该类的实例之后,其它线程都不用在加锁了。也就是说,锁只要加在创建该类的实例这一步就行了。因为锁住的代码越多,效率就越低。

import threading # 多线程
from concurrent.futures import ThreadPoolExecutor # 线程池

class Singleton(object):
    #不能将_instance定义为类属性,否则装饰完一个类之后,再装饰其他类会都是第一个被装饰类的实例。
    def __init__(self, cls):
        self._cls = cls
        self._instance = None # 必须是实例属性
        self._lock = threading.Lock() # 创建一个线程锁
    
    def __call__(self, *args, **kwargs):
    	if self._instance is None: # 首先判断是否已经创建过实例对象了
	    	with self._lock: # 当没有创建过实例对象时,占用线程锁
		        if self._instance is None: # 此时在判断是否已经创建过实例对象,防止在占用锁的这段时间已经创建了实例对象。
		            self._instance = self._cls(*args, **kwargs)
        return self._instance
        

@Singleton
class Test(object):
    def __init__(self, *args, **kwargs):
        print('本实例的id是{}'.format(id(self)))

if __name__ == '__main__':
   	for i in range(5):
		t = threading.Thread(target=Test, args=())
		t.start()
		t.join()

4. 自定义类方法

使用@classmethod装饰器来自定义一个类方法,通过这个类方法创建类的实例。

# 写法一
class Test(object):
	__instance = None # 类属性
	def __init__(self):
		pass
		
 	@classmethod
    def instance(cls, *args, **kwargs):
        if not cls.__instance: # cls是否具有该属性
        	cls.__instance = cls(*args, **kwargs)
        return cls.__instance

# 写法二
class Test(object):
    # __instance = None
    def __init__(self):
        pass
        
    @classmethod
    def instance(cls, *args, **kwargs):
        if not hasattr(cls, '__instance'): # cls是否具有该属性
            setattr(cls, '__instance', cls(*args, **kwargs))
            # print(cls.__dict__)
        return getattr(cls, '__instance')

if __name__ == '__main__':
	s1 = Test.instance()
	s2 = Test.instance()
	print(id(s1))
	print(id(s2))

修改一下代码,以满足多线程的情形。

import threading

class Test(object):
	__instance = None # 类属性
	__lock = threading.Lock() # 线程锁
	def __init__(self):
		pass
		
 	@classmethod
    def instance(cls, *args, **kwargs):
        if not cls.__instance: # cls是否具有该属性
        	with cls.__lock:
        		if not cls.__instance:
            		cls.__instance = cls(*args, **kwargs)
        return cls.__instance

if __name__ == '__main__':
	for i in range(5):
		t = threading.Thread(target=Test.instance, args=())
		t.start()
		t.join()

5. 重写类的__new()__实现单例

直接定义只能创建一个实例的类。

class Singleton(object):
	
    __instance = None #定义一个类属性做判断

	def __init__(self):
		pass
	
	def __new__(cls, *args, **kwargs):
		if cls.__instance is None:
			cls.__instance = super().__new__(cls, *args, **kwargs)
		return cls.__instance

不太明白super()函数是做什么的吗?请看此片博客 Python super() 函数

super() 函数是用于调用父类(超类)的一个方法。
super 是用来解决多重继承问题的,直接用类名调用父类方法在使用单继承的时候没问题,但是如果使用多继承,会涉及到查找顺序(MRO)、重复调用(钻石继承)等种种问题。
MRO 就是类的方法解析顺序表, 其实也就是继承父类方法时的顺序表。

但是这样重写__new()__方法只适用于单线程。为了满足多线程情形,进行如下修改。

import threading

class Singleton(object):
	
    __instance = None #定义一个类属性做判断
    __lock = threading.Lock() # 线程锁

	def __init__(self):
		pass
	
	def __new__(cls, *args, **kwargs):
		if cls.__instance is None:
			with cls.__lock:
				if cls.__instance is None:
					cls.__instance = super().__new__(cls, *args, **kwargs)
		return cls.__instance

6. 元类

简单的讲,元类就是创建类的类。

你首先写下class Foo(object),但是类Foo还没有在内存中创建。Python会在类的定义中寻找__metaclass__属性,如果找到了,Python就会用它来创建类Foo;如果没有找到,则会一级一级的检查父类中有没有__metaclass__,用来创建对象。如果当前类和父类都没有,则会在当前package中寻找__metaclass__方法,如果还没有,就会用内建的type来创建这个类。
__metaclass__就是指定当前类的元类,也就是说用哪个类来创建当前类。

import threading

class SingletonType(type):
    _instance_lock = threading.Lock()
    def __call__(cls, *args, **kwargs):
        if not hasattr(cls, "_instance"):
            with SingletonType._instance_lock:
                if not hasattr(cls, "_instance"):
                    cls._instance = super().__call__(*args, **kwargs)
        return cls._instance

# 指定了metaclass,所以用SingletonType来创建Foo这个类,
# 也就是说Foo = SingletonType(),
# 然后当用类Foo来创建实例对象时,实际时调用的SingletonType的__call__()方法。
class Foo(metaclass=SingletonType):
    def __init__(self,name):
        self.name = name


obj1 = Foo('name')
obj2 = Foo('name')
print(obj1,obj2)

我们在做类的定义时,在class声明处传入关键字metaclass=SingletonType,那么如果传入的这个metaclass有__call__函数,这个__call__函数将会覆盖掉Foo的__new__函数。这是为什么呢?请大家回想一下,当我们实例化Foo的时候,用的语句是obj1=Foo(),而我们知道,__call__函数的作用是能让类实例化后的对象能够像函数一样被调用。也就是说Foot是SingletonType实例化后的对象,而Foo()调用的就是SingletonType的__call__函数。
需要区别的是,如果class声明处,我们是让Foo继承SingletonType,那么SingletonType的__call__函数将不会覆盖掉Foo的__new__函数。
所以要区别是继承还是指定的元类。

7. 参考文献

[1] Python中的单例模式的几种实现方式的及优化
[2] 【python】*函数:全局局部变量、内部函数、闭包
[3] Python大盘点之全局变量、局部变量、类变量、实例变量
[4] Python 单例模式实现的五种方式
[5] 谈谈Python中元类Metaclass(一):什么是元类
[6] Python 元类

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值