Python 模組進階 – 單例模式(Singleton)
簡介
在大型 Python 專案中,我們常會遇到「全域唯一」的資源,例如資料庫連線池、設定檔管理器、或者日誌記錄器。若每次都重新建立這些物件,不僅會浪費記憶體與 IO,還可能造成資源競爭與不一致的狀態。單例模式(Singleton) 正是為了解決「全域唯一」需求而設計的創建型設計模式。
本篇文章將以 Python 為例,說明單例模式的概念、實作方式、常見陷阱與最佳實踐,並提供多個可直接套用的程式碼範例,幫助讀者在實務開發中快速掌握並正確運用單例。
核心概念
什麼是單例模式?
單例模式保證一個類別 只能被實例化一次,之後所有對該類別的呼叫都會返回同一個物件。這個唯一的物件在程式執行期間會一直存在,直至程式結束。
在 Python 中,我們可以透過以下幾種技巧實現這個行為:
- 模組層級的變數(最簡單的方式)
__new__方法 直接控制實例化流程- 裝飾器(Decorator) 包裝類別
- Metaclass(類別的類別)
- Thread‑safe 實作(確保多執行緒環境下不會產生多個實例)
下面分別示範這些技巧的實作方式與適用情境。
1. 使用模組層級變數(最簡易)
# logger.py
class Logger:
"""簡易的日誌記錄器"""
def __init__(self):
self.logs = []
def write(self, message: str):
self.logs.append(message)
print(f"[LOG] {message}")
# 在模組載入時即建立唯一實例
logger = Logger()
使用方式:
# app.py
from logger import logger
logger.write("程式啟動")
# 其他模組再次 import logger,仍會得到同一個 logger 物件
優點:最直接、效能最佳。
缺點:只能在同一個模組內共享,若需要在多個模組間切換實例,彈性較低。
2. 透過 __new__ 方法實作單例
class SingletonBase:
"""使用 __new__ 實作單例的基底類別"""
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
# 第一次呼叫時建立實例
cls._instance = super().__new__(cls)
return cls._instance
class ConfigManager(SingletonBase):
"""設定檔管理器"""
def __init__(self):
# 防止重複初始化
if not hasattr(self, "_initialized"):
self.settings = {}
self._initialized = True
def set(self, key, value):
self.settings[key] = value
def get(self, key, default=None):
return self.settings.get(key, default)
測試:
c1 = ConfigManager()
c2 = ConfigManager()
c1.set("host", "localhost")
print(c2.get("host")) # 輸出: localhost
print(c1 is c2) # True
重點:
__new__只在物件建立時呼叫一次,透過類別屬性_instance保存唯一實例。
注意:若子類別自行覆寫__new__,需自行呼叫super().__new__,否則會破壞單例機制。
3. 裝飾器(Decorator)方式
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 DatabaseConnection:
"""模擬資料庫連線"""
def __init__(self, dsn: str):
self.dsn = dsn
self.connected = False
def connect(self):
if not self.connected:
print(f"Connecting to {self.dsn}...")
self.connected = True
def query(self, sql: str):
if not self.connected:
raise RuntimeError("尚未連線")
print(f"執行查詢: {sql}")
使用:
db1 = DatabaseConnection("postgresql://user@localhost/db")
db2 = DatabaseConnection("postgresql://user@localhost/db")
db1.connect()
db2.query("SELECT * FROM users")
print(db1 is db2) # True
優點:不需要繼承或改寫特殊方法,只要在類別前加上
@singleton即可。
缺點:裝飾器會把原本的類別名稱變成函式返回的實例,若需要存取類別本身的屬性(如__doc__)可能需要額外處理。
4. Metaclass(類別的類別)
class SingletonMeta(type):
"""Metaclass 版單例實作"""
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
# 第一次呼叫時才建立實例
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class Cache(metaclass=SingletonMeta):
"""快取系統"""
def __init__(self):
self.store = {}
def set(self, key, value):
self.store[key] = value
def get(self, key, default=None):
return self.store.get(key, default)
測試:
c1 = Cache()
c2 = Cache()
c1.set("token", "abc123")
print(c2.get("token")) # abc123
print(c1 is c2) # True
適用情境:當你希望 所有使用同一 metaclass 的類別 都自動具備單例行為,且不想在每個類別中重複寫
__new__或裝飾器時,非常方便。
5. Thread‑Safe 單例(雙重檢查鎖)
在多執行緒環境下,同時有多個執行緒嘗試建立單例,可能會產生 競爭條件,導致產生多個實例。下面示範使用 threading.Lock 的雙重檢查鎖(Double‑checked locking):
import threading
class ThreadSafeSingleton:
_instance = None
_lock = threading.Lock()
def __new__(cls, *args, **kwargs):
if cls._instance is None: # 第一次檢查(非同步)
with cls._lock: # 取得鎖
if cls._instance is None: # 第二次檢查(同步)
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
# 防止重複初始化
if not hasattr(self, "_init"):
self.value = 0
self._init = True
驗證:
def worker():
obj = ThreadSafeSingleton()
print(f"Thread {threading.current_thread().name}: id={id(obj)}")
threads = [threading.Thread(target=worker, name=f"T{i}") for i in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
所有執行緒都會印出相同的 id,證明只產生 唯一實例。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
| 重複初始化 | 單例的 __init__ 仍會在每次呼叫時執行,可能覆寫資料。 |
在 __init__ 中加入 if not hasattr(self, "_initialized"): 檢查,或將初始化搬到 __new__。 |
| 序列化/反序列化產生新實例 | 使用 pickle 時會呼叫 __new__,若實作不當會生成新物件。 |
實作 __getstate__、__setstate__ 或 __reduce__,確保返回已存在的實例。 |
| 多執行緒競爭 | 同時建立實例會產生多個單例。 | 使用 threading.Lock(如雙重檢查鎖)或 concurrent.futures 的同步工具。 |
| 測試困難 | 單例的全域狀態會在測試間相互干擾。 | 在測試前手動重設單例(SingletonBase._instance = None),或使用依賴注入(DI)將單例抽象為介面。 |
| 過度使用 | 把所有全域資源都做成單例,會降低程式彈性。 | 僅在確實需要唯一性(如資料庫連線池、設定管理)時使用,其他情況考慮普通類別或工廠模式。 |
最佳實踐小結
- 先從模組層級變數開始,簡單且效能佳。
- 若需要 多個類別共享單例機制,使用 Metaclass 或 裝飾器。
- 在 多執行緒或多程序環境 中,務必加入 鎖 或使用 process‑safe 的 IPC 機制。
- 為了 易於測試,提供 重設介面(例如
reset_instance())或把單例包在 依賴注入容器 中。 - 別忘了 文件化 單例的使用規範,讓團隊成員了解何時可以直接呼叫,何時需要透過工廠函式。
實際應用場景
| 場景 | 為何適合單例 | 範例程式碼 |
|---|---|---|
| 資料庫連線池 | 連線建立成本高,需共享同一個池子 | 參考上面的 ThreadSafeSingleton 改寫為 ConnectionPool |
| 全域設定檔 | 程式啟動時載入一次,之後所有模組共用同一份設定 | ConfigManager(__new__ 版) |
| 日誌記錄器 | 日誌檔案只能被單一實例寫入,避免檔案競爭 | logger.py 的模組變數方式 |
| 快取系統 | 需要跨模組共享快取資料,且快取物件不宜頻繁重建 | Cache(Metaclass 版) |
| 服務發現(Service Registry) | 微服務架構中,服務清單只需要一個全域來源 | 裝飾器版 ServiceRegistry |
實務技巧:在 Flask、Django 這類 Web 框架中,常把資料庫連線、Redis 客戶端等放在 app factory 中,然後透過 單例 或 模組變數 暴露給其他藍圖(Blueprint)或視圖(View)。這樣既能確保資源唯一,又能保持程式碼的可讀性。
總結
單例模式是 保證全域唯一、降低資源開銷 的重要工具,在 Python 中有多種實作方式:最直接的 模組變數、靈活的 __new__、易於閱讀的 裝飾器、強大的 Metaclass,以及支援 多執行緒 的 Thread‑Safe 實作。每種方法都有自己的優缺點,選擇時應依照 專案規模、執行環境、測試需求 以及 團隊慣例 來決定。
在使用單例時,務必注意 避免重複初始化、處理序列化與多執行緒競爭,並遵循 重設、文件化 的最佳實踐,以免在大型系統中產生難以追蹤的錯誤。只要掌握這些要點,單例模式將成為你在 Python 專案中管理全域資源的可靠夥伴。祝開發順利!