前言
最近在整理代码时,重新审视了项目中用到的单例模式实现。发现自己对单例模式的理解经历了一个有趣的循环:从懵懂使用,到追求各种"巧妙"实现,再到回归朴素。
今天想分享几种 python 实现单例模式的方式,并谈谈我多年编程的一个感悟:朴素的就是最好的。
一、单例模式简介
单例模式(singleton pattern)是一种设计模式,确保一个类只有一个实例,并提供一个全局访问点。
🎯 适用场景
| 场景 | 说明 |
|---|---|
| 数据库连接 | 全局共享一个连接池 |
| 日志记录器 | 避免重复打开文件 |
| 配置管理器 | 全局只读一份配置 |
| 缓存实例 | 全局共享缓存 |
二、四种实现方式
方式一:类装饰器
def singleton(cls):
"""单例装饰器:闭包保存唯一实例"""
_instance = none
def wrapper(*args, **kwargs):
nonlocal _instance
if _instance is none:
_instance = cls(*args, **kwargs)
return _instance
return wrapper
@singleton
class dbconnect:
def __init__(self, host):
self.host = host
print("数据库连接初始化")
# 使用
obj1 = dbconnect("127.0.0.1")
obj2 = dbconnect("192.168.1.1") # 不会重新初始化
print(obj1 is obj2) # true
print(obj1.host) # 127.0.0.1
特点:装饰器语法优雅,使用时一目了然。
方式二:__new__方法
class mysingleton:
"""单例类:通过 __new__ 控制实例创建"""
_instance = none
def __new__(cls):
if cls._instance is none:
cls._instance = super().__new__(cls)
return cls._instance
# 使用
obj1 = mysingleton()
obj2 = mysingleton()
print(obj1 is obj2) # true
特点:最直接的方式,但 _instance 作为类变量暴露在外。我之前的博文 playwright理解与封装就是使用这种方式,感觉不太好,准备重构一下,那个类根本不需要单例模式,如果使用者觉得有必要,可以显式自行单例化即可。
方式三:函数
from typing import type, typevar
t = typevar('t')
def singleton(cls: type[t], *args, **kwargs) -> t:
"""单例工厂函数"""
if not hasattr(cls, '_instance'):
cls._instance = cls(*args, **kwargs)
return cls._instance
# 使用
class targetclass:
def __init__(self, config):
self.config = config
obj = singleton(targetclass, config="value")
特点:需要显式调用 singleton(),语义明确但稍显繁琐。
方式四:functools.lru_cache
from functools import lru_cache
@lru_cache(maxsize=1)
def get_instance():
return myclass(arg1="value")
# 使用
obj1 = get_instance()
obj2 = get_instance()
print(obj1 is obj2) # true
特点:python 内置,简单直接,还能享受缓存的好处。
三、优缺点对比
| 实现方式 | 优点 | 缺点 | 推荐指数 |
|---|---|---|---|
| 类装饰器 | 语法优雅、语义清晰、易于理解 | 对于非必要实现单例的类,对使用者会造成困扰 | ⭐⭐⭐⭐ |
| new 方法 | 最直观、最简单 | 实例变量暴露、可复用性不好 | ⭐⭐ |
| 单例函数 | 显式调用、可控性强,使用者清楚知道正在做什么 | 每次都要调用、不够直观 | ⭐⭐⭐ |
| lru_cache | 内置、无需额外代码 | 语义上不是单例、更像缓存,但是胜在简单 | ⭐⭐⭐⭐ |
四、我的建议
💡 优先使用显式单例
相比把类变成单例,显式获取单例的写法更加清晰:
# ✅ 推荐:显式获取单例
_db = none
def get_db():
global _db
if _db is none:
_db = database()
return _db
# 或者更简洁地用 lru_cache
@lru_cache
def get_db():
return database()
# ❌ 不推荐:把类变成单例,除非很确定此类必须是单例类,不会造成使用者误解
@singleton
class database:
...
为什么?
- 透明性:使用者能清楚看到这个类是全局共享的
- 可控性:可以灵活控制何时创建、如何创建
- 易测试:mock 和替换更容易
- 少歧义:不会让使用者误以为每次
new都是新实例
五、多年编程感悟
朴素的就是最好的。所有当时自鸣得意的所谓编程技巧,最后都会被证明要用更多的代价来理解和维护它。
🎯 几个教训
| 当年觉得"巧妙" | 后来发现的问题 |
|---|---|
| 复杂的继承链 | 调试时一头雾水 |
| 动态修改类属性 | 难以追踪状态变化 |
| 魔法装饰器 | 新人看不懂 |
| 元编程炫技 | 给自己挖坑 |
🌿 回归朴素
# 曾经的我:要用单例模式!
@singleton
class configmanager:
...
# 现在的我:简单就好
@lru_cache
def get_db():
return database()
简单、直白、不需要解释。这才是好的代码。
六、总结
📌 要点回顾
- 装饰器方式是最推荐的类单例实现(针对必须实现单例的类)
- 显式获取单例比把类变成单例更好
- lru_cache 是最简单的单例缓存方案
- 朴素思维:能用简单方式解决的问题,不要过度设计
到此这篇关于python 单例模式的几种实现方式的文章就介绍到这了,更多相关python 单例模式内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论