Python中实现依赖注入并不需要一开始就引入重型的IoC容器。很多场景下,只需要调整构造方式,把依赖从“自己创建”改成“外部传入”,代码的可测试性和可维护性就会明显提升。依赖注入的本质是控制反转的一种形式:对象不再负责查找或创建它需要的服务,而是由调用方或装配代码把服务实例提供给它。

在Python中,由于没有编译期的接口约束,依赖注入的实现往往比Java等语言更轻量。可以手工完成,也可以借助框架或语言特性实现自动装配。下面从最基础的构造方式开始,逐步过渡到容器和Web框架中的实践。
为什么需要依赖注入
先看一段典型的紧耦合代码。一个UserService需要访问数据库,常见写法是在内部直接导入并创建连接。
import sqlite3
class UserService:
def __init__(self):
self.conn = sqlite3.connect("app.db")
def get_user(self, user_id: int):
cursor = self.conn.cursor()
cursor.execute("SELECT name FROM users WHERE id = ?", (user_id,))
row = cursor.fetchone()
return row[0] if row else None
这段代码能运行,但存在两个明显问题。第一,UserService被绑定到具体的SQLite连接,如果测试时想使用内存数据库或假数据源,必须修改源码。第二,连接生命周期由UserService自行管理,当多个服务各自创建连接时,很难统一控制事务和关闭行为。
依赖注入的思路是把conn作为参数传入,让UserService只依赖一个抽象的“可执行SQL的对象”,而不是具体的SQLite连接。这样测试时传入一个伪造对象即可验证业务逻辑,生产环境则传入真实的连接。Python的鸭子类型让这种替换非常自然,不需要定义复杂的接口。
手工实现依赖注入的三种方式
构造函数注入
构造函数注入是最常用、也最容易理解的方式。依赖通过__init__的参数传入,对象创建后即可使用。改写上面的例子:
from typing import Protocol
class Connection(Protocol):
def cursor(self): ...
def commit(self): ...
class UserService:
def __init__(self, conn: Connection):
self.conn = conn
def get_user(self, user_id: int):
cursor = self.conn.cursor()
cursor.execute("SELECT name FROM users WHERE id = ?", (user_id,))
row = cursor.fetchone()
return row[0] if row else None
这里的Protocol来自typing模块,它定义了一个结构化类型,表示任何提供cursor和commit方法的对象都可以作为连接传入。实际项目中也可以完全省略类型标注,依靠鸭子类型运行。构造函数注入的优点是依赖关系显式、对象一旦创建就处于可用状态;缺点是如果依赖较多,构造函数参数会变长。
构造函数注入非常适合生命周期较长的服务。例如在应用启动时创建数据库连接池,然后把同一个连接池传给所有需要访问数据库的服务。这样关闭连接、管理事务都可以集中在装配代码中处理。
属性注入
属性注入也叫Setter注入,通过给实例属性赋值来提供依赖。它适合依赖可选、或需要在对象创建后再替换依赖的场景。
class EmailNotifier:
def send(self, message: str):
print(f"发送邮件: {message}")
class OrderService:
def __init__(self):
self.notifier = None
def create_order(self, order_id: int):
# 业务逻辑
print(f"订单 {order_id} 已创建")
if self.notifier:
self.notifier.send(f"订单 {order_id} 创建成功")
service = OrderService()
service.notifier = EmailNotifier()
service.create_order(1001)
属性注入的缺点比较明显:对象在依赖注入完成之前可能处于不完整状态。如果调用方忘记设置notifier,代码仍能运行,但通知不会发送。为了避免这种隐式失败,可以在调用前做显式检查,或者在配置阶段要求必须完成属性赋值。
不过,属性注入在框架和插件系统中仍有价值。例如一个基类提供可选的日志器、缓存器,子类或调用方可以按需挂载,而不必修改构造函数签名。
方法注入
方法注入把依赖作为方法参数传递,适用于依赖只在某次调用中使用的场景。依赖不需要被对象长期持有,调用结束后即可释放。
class ReportGenerator:
def generate(self, data_source):
rows = data_source.fetch_rows()
return f"共生成 {len(rows)} 行报表"
class DatabaseSource:
def fetch_rows(self):
return [("row1",), ("row2",)]
source = DatabaseSource()
report = ReportGenerator().generate(source)
print(report)
这种方式让方法签名更清晰,调用者明确知道这次调用需要什么资源。但如果同一个依赖在多个方法中反复出现,就会造成参数重复传递。方法注入通常和构造函数注入配合使用:长期依赖通过构造函数传入,临时依赖通过方法参数传入。
用容器自动装配依赖
当项目中的类逐渐增多,手工装配会变得繁琐。例如应用有Database、UserRepository、UserService、EmailNotifier等组件,它们之间存在依赖链:UserService依赖UserRepository和EmailNotifier,UserRepository又依赖Database。每次创建服务都要按顺序实例化所有底层依赖。
一个轻量级容器可以负责解析依赖关系并自动创建对象。下面实现一个基于字典和类型提示的简单容器:
import inspect
from typing import Dict, Any, get_type_hints
class Container:
def __init__(self):
self._registry: Dict[Any, Any] = {}
def register(self, key, factory):
self._registry[key] = factory
def resolve(self, key):
factory = self._registry.get(key)
if factory is None:
raise KeyError(f"未注册的依赖: {key}")
if isinstance(factory, type):
hints = get_type_hints(factory.__init__)
kwargs = {}
for name, hint in hints.items():
if name == "return":
continue
kwargs[name] = self.resolve(hint)
return factory(**kwargs)
return factory
使用时先注册依赖,再解析最上层的服务。容器会检查构造函数中的类型提示,自动解析子依赖。
class Database:
def query(self, sql: str):
return [("Alice",), ("Bob",)]
class UserRepository:
def __init__(self, db: Database):
self.db = db
def all_users(self):
return self.db.query("SELECT name FROM users")
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo
def list_users(self):
return self.repo.all_users()
container = Container()
container.register(Database, Database)
container.register(UserRepository, UserRepository)
container.register(UserService, UserService)
service = container.resolve(UserService)
print(service.list_users())
这个容器虽然只有几十行,但已经体现了自动装配的核心思想:用类型提示作为依赖标识,递归解析构造参数。对于复杂项目,可以增加单例缓存、接口到实现的映射、配置文件驱动等功能。也可以直接使用成熟的库,如dependency_injector、injector、punq等,它们提供了作用域、生命周期管理和更完善的错误提示。
但需要注意,容器不一定是必须的。如果团队规模较小、依赖关系简单,显式手工注入更容易调试。引入容器后,依赖关系变得不直观,可能增加理解成本。判断标准是:当你发现装配代码重复且容易出错时,再考虑容器。
结合框架特性的依赖注入实践
现代Python Web框架也大量使用依赖注入。以FastAPI为例,Depends是一个典型的依赖注入机制。它可以用于请求参数解析、数据库会话管理、权限校验等场景。
from fastapi import FastAPI, Depends
from typing import Annotated
app = FastAPI()
class Database:
def __init__(self):
self.conn = "sqlite connection"
def close(self):
print("关闭连接")
def get_db():
db = Database()
try:
yield db
finally:
db.close()
@app.get("/users")
def list_users(db: Annotated[Database, Depends(get_db)]):
return {"db": str(db.conn)}
这里get_db是一个依赖生成器,FastAPI会在请求处理前创建Database实例,在请求结束后执行finally中的清理逻辑。路由函数只需要声明自己需要的依赖类型,框架负责注入。这种模式把资源生命周期管理和业务逻辑解耦,也让测试时可以使用app.dependency_overrides替换真实依赖。
依赖注入还能与配置管理结合。例如从环境变量读取数据库地址,把它注入到连接工厂中。这样不同环境只需提供不同的配置对象,业务代码无需感知配置来源。实践中常见的做法是创建一个Settings类,在应用启动时实例化并注册到容器或框架的依赖系统中。
另一个重要场景是测试替身。通过依赖注入,单元测试可以传入一个返回固定数据的假仓库,而无需访问真实数据库或网络服务。下面是一个使用unittest.mock的简单示例:
from unittest.mock import MagicMock
class FakeRepo:
def all_users(self):
return [("Test",)]
service = UserService(FakeRepo())
assert service.list_users() == [("Test",)]
mock_repo = MagicMock()
mock_repo.all_users.return_value = [("Mock",)]
service = UserService(mock_repo)
assert service.list_users() == [("Mock",)]
可以看出,依赖注入让测试的重点从“如何准备真实资源”转向“如何验证业务规则”。这也是它在企业级Python项目中广泛应用的根本原因。
总结起来,Python中实现依赖注入可以走三条路径:第一,手工构造函数、属性或方法注入,适合小型项目和清晰依赖;第二,编写或引入轻量容器,适合组件较多、装配复杂的中型项目;第三,使用FastAPI、Django等框架自带的注入机制,适合Web应用和请求级资源管理。选择哪种方式,取决于项目规模和团队对显式性的偏好。只要保持依赖从外部传入这个核心原则,就能获得可测试、可替换、可维护的代码结构。
依赖注入Python依赖注入控制反转修改时间:2026-08-19 06:19:24