Python Injector 完整教學:3 步驟實現依賴注入,解耦你的程式碼

前言

寫了一段時間的程式之後,你有沒有發現自己的程式碼裡到處都是這樣的東西:

db = MySQLDatabase(host="localhost", port=3306)
service = UserService(db) 

乍看沒問題,但當你要換資料庫、寫單元測試、或讓同一個 service 支援不同環境時,因為已經寫死了要用哪個 db,這種寫法就會很不方便。

Python Injector 就是用來解決這個問題的套件,透過依賴注入(Dependency Injection)的方式,讓你的元件不用知道「我要跟誰拿資源」,而是由外部統一管理。

讀完這篇文章,你會學到:

  • 什麼是依賴注入,為什麼它讓程式碼更好維護
  • 如何安裝並開始使用 `injector` 套件
  • 用 Module 組合複雜的依賴關係

如果還不知道什麼是依賴注入,可以先看 Python 中的依賴注入

為什麼選擇 Injector?

injector 的理由很簡單,它完全依賴 Python 的 type hints 驅動,不需要額外寫設定檔,也符合現在 Python 的環境。如果你的專案有用 type hints,那就更方便了。

適合使用 injector 的情況:

  • 專案有多層服務(service → repository → database)
  • 有寫單元測試的習慣,需要 mock 依賴
  • 想讓不同環境(開發、測試、正式)使用不同實作

不適合的情況:

  • 小腳本或單一功能的工具,手動傳參數就夠了
  • 對 DI 概念不熟悉,引入反而增加溝通成本

快速開始:安裝與基本用法

安裝

pip install injector

Step 1:定義你的依賴類別

from injector import inject, Injector, Module, provider, singleton

class DatabaseConfig:
    """資料庫連線設定。"""
    def __init__(self, host: str = "localhost", port: int = 5432):
        self.host = host
        self.port = port


class Database:
    @inject
    def __init__(self, config: DatabaseConfig):
        self.config = config
        print(f"連線到 {config.host}:{config.port}")

    def query(self, sql: str) -> list[dict]:
        # 這裡會執行 SQL
        return [{"id": 1, "name": "Alice"}]

Step 2:在 Service 層使用 Inject

@injectinjector 的核心裝飾器,告訴框架「這個 __init__ 的參數,請幫我自動注入」。

class UserService:
    @inject
    def __init__(self, db: Database):
        self.db = db

    def get_all_users(self) -> list[dict]:
        return self.db.query("SELECT * FROM users")

Step 3:用 Injector 建立實例

不需要宣告:

UserService(Database(DatabaseConfig()))

交給 Injector 處理:

from injector import inject, Injector

injector = Injector()
service = injector.get(UserService)

users = service.get_all_users()
print(users)
# Output: [{'id': 1, 'name': 'Alice'}]

Injector 會自動解析整個依賴關係:UserServiceDatabaseDatabaseConfig,不用手動 new 一個新的。

進階用法:Module 與 Singleton

有時候我們不想用預設的建構子邏輯,想自己控制「這個類別怎麼被建立」,這時候就要用 Module

injector 預設會用類別的 __init__ 來建立實例。但有時候我們不想用預設的方式,想自己控制建立方法,這時候就在 Module 裡寫一個方法,加上 @provider

class AppModule(Module):

    @provider
    @singleton
    def provide_database_config(self) -> DatabaseConfig:
        # 這裡可以從環境變數、設定檔讀取
        return DatabaseConfig(host="prod-db.example.com", port=5432)

@singleton 可以讓整個服務只建立一次,有些資源例如資料庫連線通常不應該每次都重新建立,用 singleton 就可以確保整個 app 共用同一個 instance。

組合 Module 建立 Injector

injector = Injector([AppModule()])
service = injector.get(UserService)

users = service.get_all_users()
# Output: 連線到 prod-db.example.com:5432
# Output: [{'id': 1, 'name': 'Alice'}]

測試時替換假實作

這是 DI 最讚的地方——測試時完全不用動 `UserService` 的程式碼:

class TestModule(Module):
    @provider
    def provide_database(self) -> Database:
        # 回傳一個 mock 版本,不會真的連線
        class FakeDatabase(Database):
            def query(self, sql: str) -> list[dict]:
                return [{"id": 99, "name": "Test User"}]

        return FakeDatabase(DatabaseConfig())


# 測試時注入 TestModule
test_injector = Injector([TestModule()])
service = test_injector.get(UserService)

print(service.get_all_users())
# Output: [{'id': 99, 'name': 'Test User'}]

總結

依賴注入不是某個框架的功能,而是一種設計習慣。

當我們把「建立依賴」和「使用依賴」這兩件事分開,程式碼的每個部分就只需要專注在自己的責任上,service 只管業務邏輯,repository 只管資料存取,這樣的架構帶來的好處不只是換資料庫變容易,更重要的是:程式碼能夠變得「可被替換」。測試時換掉真實的外部連線、不同環境用不同的實作、日後想重構某個元件,這些事情都不會再動到業務邏輯本身。

延伸閱讀