本文 AI 產出,尚未審核

Python 模組進階 – 單例模式(Singleton)

簡介

在大型 Python 專案中,我們常會遇到「全域唯一」的資源,例如資料庫連線池、設定檔管理器、或者日誌記錄器。若每次都重新建立這些物件,不僅會浪費記憶體與 IO,還可能造成資源競爭與不一致的狀態。單例模式(Singleton) 正是為了解決「全域唯一」需求而設計的創建型設計模式。

本篇文章將以 Python 為例,說明單例模式的概念、實作方式、常見陷阱與最佳實踐,並提供多個可直接套用的程式碼範例,幫助讀者在實務開發中快速掌握並正確運用單例。


核心概念

什麼是單例模式?

單例模式保證一個類別 只能被實例化一次,之後所有對該類別的呼叫都會返回同一個物件。這個唯一的物件在程式執行期間會一直存在,直至程式結束。
在 Python 中,我們可以透過以下幾種技巧實現這個行為:

  1. 模組層級的變數(最簡單的方式)
  2. __new__ 方法 直接控制實例化流程
  3. 裝飾器(Decorator) 包裝類別
  4. Metaclass(類別的類別)
  5. 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)將單例抽象為介面。
過度使用 把所有全域資源都做成單例,會降低程式彈性。 僅在確實需要唯一性(如資料庫連線池、設定管理)時使用,其他情況考慮普通類別或工廠模式。

最佳實踐小結

  1. 先從模組層級變數開始,簡單且效能佳。
  2. 若需要 多個類別共享單例機制,使用 Metaclass裝飾器
  3. 多執行緒或多程序環境 中,務必加入 或使用 process‑safe 的 IPC 機制。
  4. 為了 易於測試,提供 重設介面(例如 reset_instance())或把單例包在 依賴注入容器 中。
  5. 別忘了 文件化 單例的使用規範,讓團隊成員了解何時可以直接呼叫,何時需要透過工廠函式。

實際應用場景

場景 為何適合單例 範例程式碼
資料庫連線池 連線建立成本高,需共享同一個池子 參考上面的 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 專案中管理全域資源的可靠夥伴。祝開發順利!