依赖注入的优雅:不用框架,在 Python 中实现轻量级依赖注入
"真正的架构自由,不是依赖更多工具,而是理解工具背后的思想,然后用最少的代码实现它。"
前言:依赖注入为什么值得认真对待?
Python 编程世界里,有一个话题经常被初学者忽视、被老手反复提及------依赖注入(Dependency Injection,DI)。
很多人第一次听到这个词,以为它是 Spring、Angular 那类重型框架的专属概念,和 Python 这种轻量灵活的语言"格格不入"。实际上,依赖注入是一种思想 ,不是工具。它的本质极其简单:不要在函数或类的内部自己创建依赖,而是从外部把依赖传进来。
当我第一次在生产项目中真正践行这个思想时,我才意识到:原来我之前写的那些代码,有多少是把"创建依赖"和"使用依赖"混在一起的------它们表面上工作正常,却在扩展、测试和维护时处处掣肘。
本文将带你从零理解依赖注入的核心思想,逐步构建一套不依赖任何第三方框架的轻量级 DI 系统,并展示它在真实 Python 项目中的应用价值。
一、先问一个根本问题:什么是"依赖"?
在 Python 编程中,当一个类或函数需要另一个对象来完成它的工作,那个对象就是它的依赖。
python
class OrderService:
def __init__(self):
# OrderService 依赖 MySQLRepository
# 但它自己创建了这个依赖------这就是问题所在
self.repo = MySQLRepository(host="localhost", port=3306)
def create(self, amount: float) -> int:
return self.repo.save({"amount": amount})
这段代码有三个隐藏问题:
测试困难:无法替换真实数据库,单元测试必须连真实 MySQL。
扩展困难 :换 PostgreSQL?必须修改 OrderService 内部------违反开闭原则。
复用困难 :OrderService 和 MySQLRepository 被死死绑定,不能独立复用。
依赖注入的解法很直接:把依赖的创建权交出去。
python
class OrderService:
def __init__(self, repo): # 依赖从外部注入
self.repo = repo
就这一行改动,OrderService 瞬间变得可测试、可扩展、可复用。
二、依赖注入的三种基本模式
Python 实践中,依赖注入有三种常见形式,理解它们的适用场景非常重要。
2.1 构造函数注入(最推荐)
python
class EmailNotifier:
def send(self, to: str, message: str) -> None:
print(f"发送邮件至 {to}: {message}")
class SMSNotifier:
def send(self, to: str, message: str) -> None:
print(f"发送短信至 {to}: {message}")
class UserService:
def __init__(self, notifier):
"""依赖在构造时注入,整个生命周期内固定"""
self.notifier = notifier
def register(self, email: str, phone: str) -> None:
# 注册逻辑...
self.notifier.send(email, "欢迎注册!")
# 生产环境
service = UserService(notifier=EmailNotifier())
service.register("user@example.com", "13800138000")
# 测试环境(换成 Mock,零副作用)
from unittest.mock import MagicMock
mock_notifier = MagicMock()
service = UserService(notifier=mock_notifier)
service.register("user@example.com", "13800138000")
mock_notifier.send.assert_called_once()
构造函数注入是最清晰的形式:依赖关系一目了然,类的所有依赖在实例化时就确定,不存在隐藏状态。
2.2 方法注入(适用于临时依赖)
python
class ReportGenerator:
def generate(self, data: list, formatter) -> str:
"""formatter 是临时依赖,不同同"""
return formatter.format(data)
class CSVFormatter:
def format(self, data: list) -> str:
return "\n".join(",".join(str(v) for v in row) for row in data)
class JSONFormatter:
def format(self, data: list) -> str:
import json
return json.dumps(data, ensure_ascii=False)
generator = ReportGenerator()
data = [["Alice", 30], ["Bob", 25]]
# 同一个 generator,不同格式
print(generator.generate(data, CSVFormatter()))
print(generator.generate(data, JSONFormatter()))
2.3 属性注入(谨慎使用)
python
class DataPipeline:
logger = None # 可选依赖,默认为空
def run(self, data: list) -> list:
if self.logger:
self.logger.info(f"开始处理 {len(data)} 条数据")
return [item for item in data if item]
pipeline = DataPipeline()
pipeline.logger = MyLogger() # 运行前注入
pipeline.run([1, None, 2, None, 3])
属性注入适合可选依赖,但由于注入时机不固定,容易引发隐藏 bug,不建议用于核心依赖。
三、轻量级 DI 容器:从零手写
了解了三种注入模式后,我们面临一个现实问题:当项目依赖关系复杂时,手动管理注入会变得繁琐。
python
# 依赖链条一旦变深,手动组装就很痛苦
db = Database(config["db_url"])
cache = RedisCache(config["redis_url"])
repo = UserRepository(db, cache)
emailer = SMSNotifier(config["sms_key"])
validator = PasswordValidator()
service = UserService(repo, emailer, validator)
这时候,一个轻量级 DI 容器就能发挥作用。它负责统一注册和解析依赖,开发者只需声明"我需要什么",容器负责把依赖组装好送过来。
我们来一步步手写一个:
3.1 最简版容器:字典注册 + 手动解析
python
class SimpleContainer:
def __init__(self):
self._registry: dict = {}
def register(self, name: str, factory):
"""注册依赖工厂函数"""
self._registry[name] = factory
def resolve(self, name: str):
"""解析依赖(每次都调用 factory 创建新实例)"""
if name not in self._registry:
raise KeyError(f"未注册的依赖: '{name}'")
return self._registry[name]()
# 使用示例
container = SimpleContainer()
container.register("db", lambda: Database("sqlite:///app.db"))
container.register("repo", lambda: UserRepository(container.resolve("db")))
container.register("service", lambda: UserService(container.resolve("repo")))
service = container.resolve("service")
这个最简容器已经能解决很多场景,但有两个明显不足:每次 resolve 都创建新实例(某些依赖应该是单例),以及需要手动声明 lambda。
3.2 进阶版:支持单例 + 自动类型注入
python
import inspect
from typing import Type, TypeVar, get_type_hints
T = TypeVar('T')
class Container:
def __init__(self):
self._factories: dict[type, callable] = {}
self._singletons: dict[type, object] = {}
self._singleton_keys: set[type] = set()
def register(self, cls: type, factory=None, singleton: bool = False):
"""
注册依赖。
- factory:可选,自定义创建函数;默认使用类本身
- singleton:True 则全局只创建一个实例
"""
self._factories[cls] = factory or cls
if singleton:
self._singleton_keys.add(cls)
return self # 支持链式调用
def resolve(self, cls: Type[T]) -> T:
"""解析依赖,自动递归注入构造函数所需的所有依赖"""
# 单例检查
if cls in self._singleton_keys:
if cls not in self._singletons:
self._singletons[cls] = self._create(cls)
return self._singletons[cls]
return self._create(cls)
def _create(self, cls: type):
factory = self._factories.get(cls)
if factory is None:
raise KeyError(f"未注册的依赖: {cls.__name__}")
# 如果 factory 是类,自动分析构造函数并注入依赖
if inspect.isclass(factory):
return self._auto_inject(factory)
# 如果是普通函数,直接调用
return factory()
def _auto_inject(self, cls: type):
"""利用类型注解自动解析并注入构造函数参数"""
try:
hints = get_type_hints(cls.__init__)
except Exception:
hints = {}
# 获取构造函数参数(排除 self 和 return)
sig = inspect.signature(cls.__init__)
kwargs = {}
for param_name, param in sig.parameters.items():
if param_name == 'self':
continue
if param_name in hints:
dep_type = hints[param_name]
# 递归解析依赖
kwargs[param_name] = self.resolve(dep_type)
return cls(**kwargs)
这个容器利用了 Python 的 inspect 和类型注解,能够自动分析构造函数,递归注入所有依赖,无需手动写 lambda。
3.3 完整使用示例
python
# 定义业务类(用类型注解声明依赖)
class Database:
def __init__(self):
print("📦 Database 初始化")
def query(self, sql: str) -> list:
return [{"id": 1, "name": "Alice"}]
class Cache:
def __init__(self):
print("⚡ Cache 初始化")
def get(self, key: str):
return None
class UserRepository:
def __init__(self, db: Database, cache: Cache): # 类型注解 = 依赖声明
self.db = db
self.cache = cache
def find_all(self) -> list:
cached = self.cache.get("users")
if cached:
return cached
return self.db.query("SELECT * FROM users")
class UserService:
def __init__(self, repo: UserRepository): # 类型注解声明依赖
self.repo = repo
def list_users(self) -> list:
return self.repo.find_all()
# 注册依赖(Database 和 Cache 作为单例)
container = (
Container()
.register(Database, singleton=True) # 全局唯一
.register(Cache, singleton=True) # 全局唯一
.register(UserRepository) # 每次创建新实例
.register(UserService)
)
# 解析!容器自动完成所有依赖注入
service = container.resolve(UserService)
print(service.list_users())
# 验证单例行为
db1 = container.resolve(Database)
db2 = container.resolve(Database)
print(f"单例验证: db1 is db2 = {db1 is db2}") # True
运行输出:
📦 Database 初始化
⚡ Cache 初始化
[{'id': 1, 'name': 'Alice'}]
单例验证: db1 is db2 = True
Database 和 Cache 只初始化了一次,UserRepository 和 UserService 的依赖被自动注入,开发者不需要手动写任何组装代码。
四、让 DI 更 Pythonic:装饰器风格注册
喜欢装饰器语法的 Pythoner,可以进一步封装:
python
class Container:
# ... 上述代码 ...
def injectable(self, singleton: bool = False):
"""装饰器:标记类为可注入,自动注册到容器"""
def decorator(cls):
self.register(cls, singleton=singleton)
return cls
return decorator
# 使用装饰器注册,代码更简洁
container = Container()
@container.injectable(singleton=True)
class Database:
def __init__(self):
self.connected = True
@container.injectable(singleton=True)
class Cache:
pass
@container.injectable()
class UserRepository:
def __init__(self, db: Database, cache: Cache):
self.db = db
self.cache = cache
@container.injectable()
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo
# 解析方式不变
service = container.resolve(UserService)
装饰器风格让依赖关系在类定义处就清晰可见,代码自文档化程度更高。
五、实战场景:测试中的依赖替换
DI 最直接的收益体现在测试中。来看一个完整的测试场景:
python
# 生产代码
class PaymentGateway:
def charge(self, amount: float, card_token: str) -> dict:
# 调用真实支付 API(测试时不能真的扣钱!)
import requests
return requests.post("https://payment.api/charge", json={
"amount": amount, "token": card_token
}).json()
class OrderService:
def __init__(self, gateway: PaymentGateway):
self.gateway = gateway
def checkout(self, amount: float, card_token: str) -> bool:
result = self.gateway.charge(amount, card_token)
return result.get("status") == "success"
python
# 测试代码:注入 Fake,无需真实 API
import pytest
class FakePaymentGateway:
"""测试专用,模拟支付网关"""
def __init__(self, should_succeed: bool = True):
self.should_succeed = should_succeed
self.charged_amounts: list[float] = []
def charge(self, amount: float, card_token: str) -> dict:
self.charged_amounts.append(amount)
status = "success" if self.should_succeed else "failed"
return {"status": status, "amount": amount}
def test_checkout_success():
fake_gateway = FakePaymentGateway(should_succeed=True)
service = OrderService(gateway=fake_gateway)
result = service.checkout(99.0, "tok_test_123")
assert result is True
assert fake_gateway.charged_amounts == [99.0] # 验证调用了正确金额
def test_checkout_payment_failure():
fake_gateway = FakePaymentGateway(should_succeed=False)
service = OrderService(gateway=fake_gateway)
result = service.checkout(99.0, "tok_test_123")
assert result is False
def test_checkout_zero_amount_edge_case():
fake_gateway = FakePaymentGateway()
service = OrderService(gateway=fake_gateway)
result = service.checkout(0.0, "tok_test_123")
# 根据业务逻辑判断期望行为
assert fake_gateway.charged_amounts == [0.0]
这些测试毫秒级运行,无需网络,无需真实 API Key,完全可靠。这就是依赖注入给测试带来的直接红利。
六、常见陷阱与最佳实践
掌握了 DI 的基本用法,还有几个实战中容易踩的坑需要警惕:
陷阱1:循环依赖
python
# ❌ A 依赖 B,B 依赖 A------容器将无限递归
class A:
def __init__(self, b: 'B'): ...
class B:
def __init__(self, a: A): ...
遇到循环依赖时,通常意味着设计有问题。解决方案是引入第三个类承担共同职责,或将其中一个依赖改为方法注入。
陷阱2:把容器当全局变量到处传
python
# ❌ 这不是依赖注入,是"服务定位器"模式
def some_function(container):
service = container.resolve(UserService) # 隐藏依赖
容器应该只在应用启动的组装层使用,业务代码只接收已注入好的依赖,永远不该直接访问容器。
陷阱3:过度注入
不是所有依赖都需要注入。内置类型(str、int、list)、工具函数、不变的配置值,直接传递即可。DI 的适用场景是有替换需求的服务型依赖(数据库、邮件、支付等)。
最佳实践清单:
✅ 依赖以抽象(Protocol/ABC)为类型注解,不以具体类为注解
✅ 构造函数注入优先,属性注入谨慎使用
✅ 单例只用于无状态或昂贵初始化的对象(数据库连接、缓存)
✅ 容器只在 main.py 或应用入口处组装,不透传到业务层
✅ 为每个 Fake/Stub 写对应测试,验证 fake 行为的正确性
七、前沿生态:何时考虑引入框架?
手写 DI 容器适合中小项目和深入理解原理。当项目规模进一步增长,以下库值得关注:
dependency-injector:Python 生态中最成熟的 DI 框架,支持容器组合、配置驱动注入,适合大型企业项目。
FastAPI 的 Depends():Web 框架内置的轻量 DI,与异步完美融合,是目前 Python API 开发中最优雅的 DI 实践之一。
python
# FastAPI 原生 DI,简洁优雅
from fastapi import FastAPI, Depends
app = FastAPI()
def get_service() -> UserService:
return container.resolve(UserService)
@app.get("/users")
async def list_users(service: UserService = Depends(get_service)):
return service.list_users()
punq:极简 DI 容器,API 设计接近本文手写版,适合想引入 DI 但不想用重型框架的团队。
八、总结:依赖注入是一种思维方式
回顾全文,依赖注入的核心从来不是某个框架或工具,而是一个朴素的工程思想:
不要在内部创造你的依赖,让外部把它们给你。
这个思想让代码模块边界清晰,让测试简单可靠,让扩展不需要修改旧代码。在 Python 的动态类型世界里,配合 Protocol、类型注解和 inspect 模块,我们甚至可以用不到 100 行代码构建一个功能完整的 DI 容器。
从手写容器出发理解 DI,比直接上框架更有价值------当你真正理解了它在做什么,框架的文档和设计决策才会对你变得透明可读。
互动讨论
你的项目中有没有遇到过"依赖地狱"------模块间互相引用,一改就全崩的经历?是怎么解决的?或者,你对手写 DI 容器和引入第三方框架之间的取舍有什么看法?欢迎在评论区分享你的实战经验,让我们一起探讨 Python 工程之美。
参考资源
- Python
inspect模块官方文档 - PEP 544 - Protocols
- FastAPI 依赖注入指南
- dependency-injector 库
- 推荐书籍:《架构整洁之道》、《Python 工匠》、《Effective Python》