19、设计模式:单例模式:模块级单例、装饰器实现、元类实现,确保全局唯一实例

单例模式,说白了就是——一个类,一辈子只有一个对象

你可能会问:为什么非要搞唯一实例?

嗯,我举个例子你就明白了。我在做日志系统的时候,整个项目里所有模块都要往同一个日志文件里写。如果每个模块都 new 一个 Logger,那日志文件就乱套了,互相覆盖、重复打开句柄,最后排查问题你都不知道日志是谁写的。这时候,单例模式就派上用场了——全局只有一个 Logger 实例,谁用都是它。

今天咱们就聊三种 Python 里实现单例的常用手段:模块级单例装饰器实现元类实现。每种都有它的适用场景,我挨个给你拆开讲。

一、模块级单例:最 Pythonic 的方式

Python 的模块本身就是天然的单例。模块在第一次被导入时,会执行一次并生成一个 .pyc 文件,后续再导入,直接拿缓存。利用这个特性,我们可以把实例放在模块顶层。

我个人习惯用这种方式,因为它最简单,不需要额外写类或装饰器。

核心思路:在模块中直接创建实例,外部导入时拿到的永远是同一个对象。

# singleton_module.py
class DatabaseConnection:
    def __init__(self):
        self.host = "localhost"
        self.port = 3306
        print("数据库连接已创建")

# 模块级单例:直接在这里创建实例
db_instance = DatabaseConnection()

使用时,其他模块这样导入:

from singleton_module import db_instance

def query():
    print(f"连接 {db_instance.host}:{db_instance.port}")

你想想看,不管多少个地方 from singleton_module import db_instance,拿到的都是同一个对象。因为模块只加载一次。

小提示:这种方式适合那些初始化开销大、且全局只需要一个实例的场景,比如数据库连接池、配置管理器。

不过要注意一点——如果你在模块里直接写 db_instance = DatabaseConnection(),那只要有人导入这个模块,实例就创建了。哪怕他根本没用。这叫「饿汉式」。如果你希望「懒加载」,那就得用下面两种方式了。

二、装饰器实现:优雅且可复用

装饰器实现单例,说白了就是用一个字典来缓存类和实例的映射关系。每次调用类的时候,先查字典,有就直接返回,没有就创建并缓存。

我在项目中遇到过这样一个场景:多个微服务共享同一个配置中心客户端。每个服务启动时都要初始化一次,但我不想让它们各自 new 一个。用装饰器,一行注解就搞定。

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 ConfigManager:
    def __init__(self):
        self.config = {}
        print("配置管理器初始化")

使用的时候:

c1 = ConfigManager()
c2 = ConfigManager()
print(c1 is c2)  # True

你看,c1 is c2 返回 True,说明是同一个对象。装饰器把原来的类替换成了一个函数,这个函数内部维护了一个字典 instances,闭包的特性保证了字典不会被回收。

避坑指南:我曾经在写多线程程序时,直接用这个装饰器,结果在高并发下出现了两个实例。为什么?因为 if cls not in instances 不是原子操作。解决方案是加锁,或者用 threading.Lock 包裹创建逻辑。

改进后的线程安全版本:

import threading

def singleton_threadsafe(cls):
    instances = {}
    lock = threading.Lock()

    def get_instance(*args, **kwargs):
        if cls not in instances:
            with lock:
                # 双重检查锁定
                if cls not in instances:
                    instances[cls] = cls(*args, **kwargs)
        return instances[cls]

    return get_instance

嗯,这里用了双重检查锁定,既保证了线程安全,又避免了每次调用都加锁的性能损耗。

三、元类实现:最底层、最可控

元类,说白了就是「类的类」。普通类创建对象,元类创建类。通过控制元类的 __call__ 方法,我们可以在类被实例化时做手脚。

我记得有一次,我需要一个单例,但同时又希望这个类能正常继承、正常使用 isinstance 判断。装饰器方案会破坏类的类型信息(因为装饰器返回的是函数,不是类),而元类方案不会。

class SingletonMeta(type):
    """单例元类"""
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            # 调用父类 type 的 __call__ 创建实例
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class Logger(metaclass=SingletonMeta):
    def __init__(self):
        self.log_file = "app.log"
        print("日志器初始化")

使用:

l1 = Logger()
l2 = Logger()
print(l1 is l2)  # True
print(type(l1))  # <class '__main__.Logger'>

你看,type(l1) 仍然是 Logger,而不是函数。这就是元类的优势——它不改变类的本质。

核心区别:装饰器把类变成了函数,元类保留了类的身份。如果你需要 isinstance 判断、需要继承,元类是更好的选择。

不过元类也有缺点:它比装饰器难理解,团队里新人可能看不懂。我个人建议,除非你有特殊需求(比如要保留类结构),否则优先用模块级单例或装饰器。

三种方式对比

实现方式 优点 缺点 推荐场景
模块级单例 最简单,零学习成本 饿汉式,无法懒加载 配置、日志、数据库连接
装饰器实现 优雅,可复用,支持懒加载 破坏类类型,多线程需加锁 工具类、客户端类
元类实现 保留类结构,支持继承 复杂度高,理解门槛高 框架底层、需要类型判断的场景

知识体系图

下面这张图帮你理清三种实现的核心逻辑和关系:

单例模式三种实现方式 模块级单例 模块导入即创建 装饰器实现 闭包缓存实例 元类实现 控制 __call__ 方法 特点 • 最简单 • 饿汉式 • 无法懒加载 特点 • 优雅可复用 • 支持懒加载 • 破坏类类型 特点 • 保留类结构 • 支持继承 • 复杂度高 选择建议 简单场景 → 模块级单例 需要懒加载/可复用 → 装饰器 需要保留类类型/框架底层 → 元类

总结

单例模式的核心就一句话:确保一个类只有一个实例,并提供一个全局访问点

三种实现方式各有千秋。我个人最常用的是模块级单例,因为它简单到不需要解释。但如果遇到需要懒加载或者多线程场景,我会毫不犹豫地选择装饰器加锁版本。至于元类,嗯,我一般只在写框架或库的时候才用,因为它对调用方最透明。

最后送你一句话:设计模式是工具,不是教条。别为了用模式而用模式,合适才是最好的。

避坑总结:我曾经在项目里看到有人用全局变量 + if not instance 的方式实现单例,结果在多线程下翻车了。记住,单例模式一定要考虑线程安全,除非你确定你的代码只在单线程中运行。

公众号:蓝海资料掘金营,微信deep3321