单例是一种创建型设计模式,让你能够保证一个类只有一个实例,并提供一个访问该实例的全局节点。
单例拥有与全局变量相同的优缺点。尽管它们非常有用,但却会破坏代码的模块化特性。
在某些其他上下文中,你不能使用依赖于单例的类。你也将必须使用单例类。绝大多数情况下,该限制会在创建单元测试时出现。
单例模式亦称:单件模式、Singleton。
单例模式同时解决了两个问题, 所以违反了_单一职责原则_:
保证一个类只有一个实例。 为什么会有人想要控制一个类所拥有的实例数量? 最常见的原因是控制某些共享资源 (例如数据库或文件) 的访问权限。
它的运作方式是这样的: 如果你创建了一个对象, 同时过一会儿后你决定再创建一个新对象, 此时你会获得之前已创建的对象, 而不是一个新对象。
注意, 普通构造函数无法实现上述行为, 因为构造函数的设计决定了它必须总是返回一个新对象。
为该实例提供一个全局访问节点。 还记得你 (好吧, 其实是我自己) 用过的那些存储重要对象的全局变量吗? 它们在使用上十分方便, 但同时也非常不安全, 因为任何代码都有可能覆盖掉那些变量的内容, 从而引发程序崩溃。
和全局变量一样, 单例模式也允许在程序的任何地方访问特定对象。 但是它可以保护该实例不被其他代码覆盖。
还有一点: 你不会希望解决同一个问题的代码分散在程序各处的。 因此更好的方式是将其放在同一个类中, 特别是当其他代码已经依赖这个类时更应该如此。
如今, 单例模式已经变得非常流行, 以至于人们会将只解决上文描述中任意一个问题的东西称为单例。
所有单例的实现都包含以下两个相同的步骤:
new
运算符。 如果你的代码能够访问单例类, 那它就能调用单例类的静态方法。 无论何时调用该方法, 它总是会返回相同的对象。
政府是单例模式的一个很好的示例。 一个国家只有一个官方政府。 不管组成政府的每个人的身份是什么, “某政府” 这一称谓总是鉴别那些掌权者的全局访问节点。
单例 (Singleton) 类声明了一个名为 getInstance
获取实例的静态方法来返回其所属类的一个相同实例。
单例的构造函数必须对客户端 (Client) 代码隐藏。 调用 获取实例
方法必须是获取单例对象的唯一方式。
许多开发者将单例模式视为一种反模式。因此它在 Python 代码中的使用频率正在逐步减少。
识别方法:单例可以通过返回相同缓存对象的静态构建方法来识别。
实现一个粗糙的单例非常简单。你仅需隐藏构造函数并实现一个静态的构建方法即可。下面的代码是实现基础单例。
class SingletonMeta(type): """ The Singleton class can be implemented in different ways in Python. Some possible methods include: base class, decorator, metaclass. We will use the metaclass because it is best suited for this purpose. """ _instances = {} def __call__(cls, *args, **kwargs): """ Possible changes to the value of the `__init__` argument do not affect the returned instance. """ if cls not in cls._instances: instance = super().__call__(*args, **kwargs) cls._instances[cls] = instance return cls._instances[cls] class Singleton(metaclass=SingletonMeta): def some_business_logic(self): """ Finally, any singleton should define some business logic, which can be executed on its instance. """ # ... if __name__ == "__main__": # The client code. s1 = Singleton() s2 = Singleton() if id(s1) == id(s2): print("Singleton works, both variables contain the same instance.") else: print("Singleton failed, variables contain different instances.")
执行结果
Singleton works, both variables contain the same instance.
相同的类在多线程环境中会出错。多线程可能会同时调用构建方法并获取多个单例类的实例。
为了解决这个问题,你必须在创建首个单例对象时对线程进行同步。
from threading import Lock, Thread class SingletonMeta(type): """ This is a thread-safe implementation of Singleton. """ _instances = {} _lock: Lock = Lock() """ We now have a lock object that will be used to synchronize threads during first access to the Singleton. """ def __call__(cls, *args, **kwargs): """ Possible changes to the value of the `__init__` argument do not affect the returned instance. """ # Now, imagine that the program has just been launched. Since there's no # Singleton instance yet, multiple threads can simultaneously pass the # previous conditional and reach this point almost at the same time. The # first of them will acquire lock and will proceed further, while the # rest will wait here. with cls._lock: # The first thread to acquire the lock, reaches this conditional, # goes inside and creates the Singleton instance. Once it leaves the # lock block, a thread that might have been waiting for the lock # release may then enter this section. But since the Singleton field # is already initialized, the thread won't create a new object. if cls not in cls._instances: instance = super().__call__(*args, **kwargs) cls._instances[cls] = instance return cls._instances[cls] class Singleton(metaclass=SingletonMeta): value: str = None """ We'll use this property to prove that our Singleton really works. """ def __init__(self, value: str) -> None: self.value = value def some_business_logic(self): """ Finally, any singleton should define some business logic, which can be executed on its instance. """ def test_singleton(value: str) -> None: singleton = Singleton(value) print(singleton.value) if __name__ == "__main__": # The client code. print("If you see the same value, then singleton was reused (yay!)\n" "If you see different values, " "then 2 singletons were created (booo!!)\n\n" "RESULT:\n") process1 = Thread(target=test_singleton, args=("FOO",)) process2 = Thread(target=test_singleton, args=("BAR",)) process1.start() process2.start()
执行结果
If you see the same value, then singleton was reused (yay!) If you see different values, then 2 singletons were created (booo!!) RESULT: FOO FOO
在本例中, 数据库连接类即是一个单例。
该类不提供公有构造函数, 因此获取该对象的唯一方式是调用 获取实例
方法。 该方法将缓存首次生成的对象, 并为所有后续调用返回该对象。
// 数据库类会对`getInstance(获取实例)`方法进行定义以让客户端在程序各处 // 都能访问相同的数据库连接实例。 class Database is // 保存单例实例的成员变量必须被声明为静态类型。 private static field instance: Database // 单例的构造函数必须永远是私有类型,以防止使用`new`运算符直接调用构 // 造方法。 private constructor Database() is // 部分初始化代码(例如到数据库服务器的实际连接)。 // ... // 用于控制对单例实例的访问权限的静态方法。 public static method getInstance() is if (Database.instance == null) then acquireThreadLock() and then // 确保在该线程等待解锁时,其他线程没有初始化该实例。 if (Database.instance == null) then Database.instance = new Database() return Database.instance // 最后,任何单例都必须定义一些可在其实例上执行的业务逻辑。 public method query(sql) is // 比如应用的所有数据库查询请求都需要通过该方法进行。因此,你可以 // 在这里添加限流或缓冲逻辑。 // ... class Application is method main() is Database foo = Database.getInstance() foo.query("SELECT ...") // ... Database bar = Database.getInstance() bar.query("SELECT ...") // 变量 `bar` 和 `foo` 中将包含同一个对象。
如果程序中的某个类对于所有客户端只有一个可用的实例, 可以使用单例模式。
单例模式禁止通过除特殊构建方法以外的任何方式来创建自身类的对象。 该方法可以创建一个新对象, 但如果该对象已经被创建, 则返回已有的对象。
如果你需要更加严格地控制全局变量, 可以使用单例模式。
单例模式与全局变量不同, 它保证类只存在一个实例。 除了单例类自己以外, 无法通过任何方式替换缓存的实例。
请注意, 你可以随时调整限制并设定生成单例实例的数量, 只需修改 获取实例
方法, 即
getInstance 中的代码即可实现。
在类中添加一个私有静态成员变量用于保存单例实例。
声明一个公有静态构建方法用于获取单例实例。
在静态方法中实现"延迟初始化"。 该方法会在首次被调用时创建一个新对象, 并将其存储在静态成员变量中。 此后该方法每次被调用时都返回该实例。
将类的构造函数设为私有。 类的静态方法仍能调用构造函数, 但是其他对象不能调用。
检查客户端代码, 将对单例的构造函数的调用替换为对其静态构建方法的调用。
之前面试字节跳动的时候,面试官让我手写一个单例,我就写了双重检查这个例子,然后面试官说这个已经过时了,有什么其他写法吗。我才跟他开始扯用枚举实现。其实用枚举和静态内部类都是很好的选择,因为可以防序列化问题。