Python面试题系列之15 如何实现一个单例?

Python面试题系列之15: 如何实现一个单例?

Question

如何实现一个单例?

知识点详解

单例(Singleton),一种设计模式,应用该模式的类只会生成一个实例。它保证了在程序的不同位置都可以,且仅能取到同一个对象实例:如果实例不存在,会创建一个实例;如果已存在就会返回这个实例。

我们回顾下在 Python 中的类、实例,是通过哪些方法以怎样的顺序被构造的。

简单来说,元类(metaclass)可以通过__metaclass__方法创造出类(class),而类(class)通过__new__方法创造出实例(instance)

在创造类的过程中或者创造实例的过程中稍加控制,从而可以达到最后产生的实例都是一个对象的目的,即实现了单例。

方法一:利用 new 方法

为了使类只能出现一个实例,我们可以使用__new__来控制实例的创建过程,达到实现单例模式的目的。

class Singleton(object):
    _instance = None
    def __new__(cls, *args, **kw):
        if cls._instance is None:
            cls._instance = object.__new__(cls, *args, **kw)
        return cls._instance
    def __init__(self):
        pass

a = Singleton()
b = Singleton()
print(id(a) == id(b)) # True

这里将一个类的实例绑定到类变量_instance上,如果_instance为None,说明该类还没有实例化过,实例化该类并返回;否则直接返回 _instance 存放的实例。

方法二:利用元类(metaclass)

我们在类的创建时进行干预,从而达到实现单例的目的。

# 拦截(intercepting) class的创建
class Singleton(type):
    _instance = None
    # 通过重写__call__拦截实例的创建
    def __call__(cls, *args, **kw):
        if not cls._instance:
            cls._instance = super().__call__(*args, **kw)
        return cls._instance


class MyClass(metaclass=Singleton):
    pass

a = MyClass()
b = MyClass()
print(id(a) == id(b)) # True

这里我们将metaclass指向 Singleton 类,让Singleton中的type来创造新的 MyClass实例。

元类可以控制类的创建过程,它主要做了三件事:拦截类的创建;修改类的定义;返回修改后的类。

方法三:利用(函数)装饰器

装饰器可以动态的修改一个类或函数的功能。这里,我们也可以使用装饰器来装饰某个类,使其只能生成一个实例:

def Singleton(cls):
    _instances = {}
    def get_instance(*args, **kwargs):
        if cls not in _instances:
            _instances[cls] = cls(*args, **kwargs)
        return _instances[cls]
    return get_instance

@Singleton
class MyClass:
    def __init__(self):
        pass

a = MyClass()
b = MyClass()
print(id(a) == id(b)) # True

这里我们定义了一个装饰器singleton,它返回了一个内部函数get_instance,该函数会判断某个类是否在字典_instances中,如果不存在,则会将cls作为keycls(*args, **kwargs)作为value存到instances中,否则,直接返回_instances[cls]

方法四:利用(类)装饰器

上面我们使用函数装饰器实现了单例,用类装饰器来实现的思路与其类似,直接看具体代码:

class Singleton(object):
    def __init__(self, cls):
        self._cls = cls
        self._instance = {}
    def __call__(self):
        if self._cls not in self._instance:
            self._instance[self._cls] = self._cls()
        return self._instance[self._cls]

@Singleton
class MyClass:
    def __init__(self):
        pass

a = MyClass()
b = MyClass()
print(id(a) == id(b)) # True

方法五:利用StaticMethod

class Singleton(object):
    _instance = None

    def __init__(self):
        raise SyntaxError('can not instance, please use get_instance')

    @staticmethod
    def get_instance():
        if Singleton._instance is None:
            Singleton._instance = object.__new__(Singleton)
        return Singleton._instance


a = Singleton.get_instance()
b = Singleton.get_instance()
print(id(a) == id(b)) # True

该方法的要点是在__init__抛出异常,禁止通过类来实例化,只能使用静态get_instance函数来获取实例,通过父类object.__new__来实例化。

方法六:利用ClassMethod

和方法五的实现思路类似,只是这里采用了classmethod,代码:

class Singleton(object):
    _instance = None

    def __init__(self):
        raise SyntaxError('can not instance, please use get_instance')

    @classmethod
    def get_instance(cls):
        if Singleton._instance is None:
            Singleton._instance = object.__new__(Singleton)
        return Singleton._instance


a = Singleton.get_instance()
b = Singleton.get_instance()
print(id(a) == id(b)) # True

方法七:利用名字覆盖

这里采用的是一种名字覆盖的思路

class Singleton(object):
    def foo(self):
        print('foo')

    def __call__(self):
        return self


Singleton = Singleton()

Singleton.foo()

a = Singleton()
b = Singleton()
print(id(a) == id(b)) # True

方法八:使用模块

Python的模块就是天然的单例模式,因为模块在第一次导入时,会生成.pyc文件,当第二次导入时,就会直接加载.pyc文件,而不会再次执行模块代码。

因此我们只需把相关的函数和数据定义在一个模块中,就可以获得一个单例对象了。

# MySingleton.py
class MySingleton:
  def foo(self):
    pass

sinleton = MySingleton()

将上面的代码保存在文件MySingleton.py中,然后这样使用:

from MySingleton import sinleton
singleton.foo()

方法九:共享属性

所谓单例就是所有引用(实例、对象)拥有相同的状态(属性)和行为(方法),同一个类的所有实例天然拥有相同的行为(方法),只需要保证同一个类的所有实例具有相同的状态(属性)即可。

class Singleton(object):
    _state = {}
    def __new__(cls, *args, **kw):
        ob = super(Singleton, cls).__new__(cls, *args, **kw)
        ob.__dict__ = cls._state
        return ob

class MyClass(Singleton):
    x = 666

a = MyClass()
b = MyClass()

print(id(a) ==  id(b))
>>> False

我们可以看到a和b是两个不同的对象,我们再看看它们的__dict__属性呢?

print(id(a.__dict__))
>>> 2679828344048
print(id(b.__dict__))
>>> 2679828344048
print(id(a.x) == id(b.x))
>>> True

不难发现这两不同的对象a、b,却共享了相同属性。所有实例共享属性的最简单直接的方法就是__dict__属性指向(引用) 同一个字典(dict)。

Answer

实现单例模式的方法,详见上文内容。

后记

本文介绍了多种方法来实现单例模式,这其中涉及到了很多的Python高级语法,诸如decorator、metaclass、new、type、super等等。在面试过程以及实际工程项目中,根据实际情况来按需实现即可。关于单例模式的实现,你还能想到有哪些方式呢?

欢迎大家在评论区留言说出自己的看法。好了,以上就是本篇全部内容。

备注:本篇首发于知识星球「人人都是Pythonista」。


文章作者: &娴敲棋子&
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 &娴敲棋子& !
评论
  目录