读后笔记 -- FastAPI 构建Python微服务 Chapter3:依赖注入
Pre:依赖注入 概念
Python 中的依赖注入是一种软件设计模式,旨在降低代码间的耦合度,提高代码的可维护性、可测试性和可扩展性。通过依赖注入,对象不再自行创建或管理其依赖对象,而是由外部(通常是框架、容器或配置系统)负责提供所需依赖。
这样,对象间的依赖关系变得更加灵活,易于替换和管理,特别是在大型项目或复杂的软件架构中。以下是一些关于 Python 中依赖注入的关键概念和实践方法:
1. 依赖注入的基本原则:
解耦:避免直接在类内部创建或硬编码其依赖对象,使类专注于自身的业务逻辑,而不是依赖对象的管理和生命周期。
外部提供依赖:依赖对象应在类的外部创建,并通过构造函数、setter 方法(即 Set 注入)或特定的框架机制(如装饰器)注入到类中。
依赖透明:类应该声明其所需的依赖类型或接口,而不是具体的实现类,这样在运行时可以灵活地替换为不同的实现。
2. 依赖注入的实现方式
2.1 构造函数注入
通过类的构造函数接受依赖作为参数,将它们直接绑定到对象的实例变量上。这样,创建对象时就必须提供所需的依赖。
class UserService: def __init__(self, user_repository: UserRepository): self.user_repository = user_repository # 创建 UserService 实例时注入 UserRepository 实例 service = UserService(user_repository=ConcreteUserRepository())
2.2 Setter 注入
使用 setter 方法(或属性)在对象创建后注入依赖。这种方式允许在对象生命周期的后期注入依赖,或者在需要时动态替换依赖。
class UserService: def __init__(self): self.user_repository = None def set_user_repository(self, user_repository: UserRepository): self.user_repository = user_repository service = UserService() service.set_user_repository(ConcreteUserRepository())
2.3 装饰器注入
利用 Python 的装饰器特性,在定义函数或类时附加额外的行为,包括注入依赖。装饰器可以捕获函数参数或类实例,从而在调用时提供依赖。
def inject_user_repository(func): @wraps(func) def wrapper(user_service: UserService): user_service.user_repository = ConcreteUserRepository() return func(user_service) return wrapper @inject_user_repository def perform_user_operation(user_service: UserService): pass
2.4 依赖注入框架 Python 中有一些框架和库专门支持依赖注入,它们提供了自动化的依赖解析、生命周期管理等功能,简化了依赖注入的实现: Injector: 一个轻量级的依赖注入容器,通过定义依赖关系的配置,自动管理对象的创建和依赖注入。 Dependency Injector: 另一个功能丰富的依赖注入框架,支持多种注入方式、依赖生命周期管理、配置文件加载等。 Pyramid: 虽然 Pyramid 主要是 Web 框架,但它内置了对依赖注入的支持,通过配置文件或代码定义服务及其依赖关系。 Django 和 Flask 等 Web 框架虽不直接提供依赖注入容器,但可通过第三方库(如 Django-DI 或 Flask-DI)或遵循依赖注入原则自行实现。 3. 依赖注入的应用场景 共享业务逻辑:复用相同的代码逻辑,如数据访问、验证、缓存等。 共享数据库连接:集中管理数据库连接,避免重复创建和释放资源。 实现安全、验证、角色权限:将这些横切关注点的实现作为依赖注入,便于统一管理和更新。 测试:轻松模拟或替换实际依赖,实现单元测试和集成测试。 模块化和可扩展性:通过依赖注入,可以轻松替换组件或添加新功能,适应不断变化的需求。 总之,Python 中的依赖注入是一种提升代码质量、促进模块化和可维护性的强大工具。通过合理选择适合项目的注入方式(构造函数、setter 方法、装饰器等)和(或)依赖注入框架,
可以有效地管理对象间的依赖关系,构建更为健壮、灵活的软件系统。
3.2 IOC & DI
1. IOC & DI 基本概念
Python 中的 IoC(Inversion of Control,控制反转)是一种设计原则和编程范式,它旨在降低代码之间的耦合度,提高模块化程度和系统的灵活性。IoC 的核心思想是将对象的创建和依赖管理从应用程序的主体逻辑中剥离出来,
转交给一个专门的外部实体(通常是容器或框架)负责。在 Python 应用程序中,IoC 主要通过 依赖注入(Dependency Injection, DI)这一具体技术来实现。 以下是 IoC 在 Python 中的概念要点: 1. 控制反转: 反转:传统的程序设计中,对象通常自行创建或查找它们依赖的对象。而在 IoC 模式下,对象不再直接控制其依赖的创建或获取过程,而是由外部容器或框架来负责提供这些依赖。 控制:这里的“控制”指的是对对象创建、初始化及其依赖管理的控制权。通过反转控制权,对象不再负责这些职责,而是由容器动态地决定何时、何地以及如何提供所需依赖。
2. 依赖注入: 依赖:指的是一个对象为了完成其功能可能需要使用的其他对象或服务。例如,一个数据库操作类可能依赖于一个数据库连接对象。 注入:是指在对象实例化或运行时,其依赖不是由对象自身创建或查找,而是由外部容器创建好后“注入”到对象中。注入方式可以是通过构造函数、属性(setter方法)或方法参数等方式传递。
在 Python 中实现 IoC / DI 的典型场景包括: 框架支持:某些 Python Web 框架(如 Flask、FastAPI)内置了对依赖注入的支持。它们通常通过装饰器、工厂函数或特定的配置来声明和注入依赖。例如,FastAPI 使用 Depends 装饰器来声明路径操作函数的依赖,并由框架在运行时自动注入。 第三方库:存在一些专门的 Python 库(如 dependency_injector)用于实现 IoC 容器和依赖注入。这些库提供了创建容器、注册依赖、配置注入规则等功能,帮助开发者构建高度解耦且易于测试的应用程序。 自定义实现:即使不使用框架或第三方库,Python 开发者也可以手动实现简单的依赖注入。这可能涉及编写工厂函数、使用元类(metaclass)或通过设计良好的接口与构造函数来接受依赖。 通过应用 IoC 和 DI 原则,Python 应用可以获得以下好处: 松耦合:对象与其依赖之间解耦,使得各组件可以独立开发、测试和升级,互不影响。 可扩展性:容易更换或添加新的实现,因为依赖关系在容器级别集中管理,而非硬编码在各个类或模块中。 可测试性:在单元测试中,可以轻易地用模拟(mock)对象或测试双替(test double)替换实际依赖,便于隔离测试特定功能。 代码复用:依赖注入使得对象更加通用,减少了重复的依赖管理和创建逻辑,提高了代码的复用率。
总之,Python 中的 IoC 是一种强调将对象创建和依赖管理责任转移给外部容器的设计原则,依赖注入则是其实现的具体手段。通过这种方式,Python 应用能够实现更清晰的职责划分、更高的模块化程度以及更好的可维护性和可测试性。
2.1 注入依赖函数(情形一)
def create_login(id: UUID, username: str, password: str, type: UserType): account = {'id': id, 'username': username, 'password': password, 'type': type} return account @router.get("/users/function/add") # 通过 Depends() 注入依赖函数,其只能传入单一的可调用对象(如函数、类等)!!! Depends 放在参数的末尾 def populate_user_accounts(user_account: Login = Depends(create_login)): account_dict = jsonable_encoder(user_account) login = Login(**account_dict) login_details[login.id] = login return login
注意的是:
尽管 Depends() 通常用于单个依赖项的声明,但 FastAPI 并不限制你在一个路径操作函数中使用多个 Depends() 装饰的参数。实际上,你可以为路径操作函数的多个参数分别使用 Depends(),以声明并注入多个独立的依赖项:
@app.get("/items/{item_id}") async def read_item( item_id: int, current_user: dict = Depends(authenticate), db_session: Session = Depends(get_db_session), ): # item_id、current_user 和 db_session 分别由路径参数、Depends(authenticate) 和 Depends(get_db_session) 注入 ...
2.2. 注入可调用的类(情形二)
#content of \repository\users.py class Login: def __init__(self, id: UUID, username: str, password: str, type: UserType): self.id = id self.username = username self.password = password self.type = type #content of \api\users.py @router.post("/users/datamodel/add") # DI 情形二: 通过 Depends() 注入可调用的类(通过有 __init__实例化,故:1)无参数构造函数 可直接实例化,即 __init__()无参数;2)有参数的,如上面的 Login类,需要对参数进行构造) def populate_login_without_service(user_account=Depends(Login)): account_dict = jsonable_encoder(user_account) login = Login(**account_dict) login_details[login.id] = login return login
2.3 嵌套依赖
# content of \api\user.py
@router.post("/users/add/profile") # 嵌套依赖之 函数方式: add_profile_login -依赖-> create_user_details -依赖-> create_login async def add_profile_login(profile=Depends(create_user_details)): user_profile = jsonable_encoder(profile) user = User(**user_profile) login = user.login login = Login(**login) user_profiles[user.id] = user login_details[login.id] = login return user_profile @router.post("/users/add/model/profile") # 如果是依赖于类,(profile: Profile=Depends(Profile, use_cache=False)) => 简化成下面的形式 async def add_profile_login_models(profile: Profile=Depends(use_cache=False)): # 链式依赖的优势 # 通过 profile -依赖于-> Profile,指向其 __init__方法,这里的 profile.user 指向 UserDetail 类 user_details = jsonable_encoder(profile.user) login_details = jsonable_encoder(profile.login) user = UserDetails(**user_details) login = Login(**login_details) user_profiles[user.id] = user login_details[login.id] = login return {"profile_created": profile.date_created}
3.3 注入依赖项的方法
3.3.1 服务参数上 依赖注入
@router.get("/recipes/list/all") # DI 方法一:服务参数列表上的依赖注入,可用以处理事务 def get_all_recipes(handler=Depends(get_recipe_service)): return handler.get_recipes()
3.3.2 在路径运算符中的 依赖注入
@router.post("/posts/insert", dependencies=[Depends(check_feedback_length)]) # DI 方法二:在路径运算符中注入 # 1)dependencies 是一个 list,说明可有多个可注入项; # 2)dependencies,它用于指定一组依赖项,这些依赖项是函数或可调用对象,在处理该路由的POST请求之前必须先执行。依赖项可以用来实现诸如身份验证、 # 权限检查、获取配置信息、预处理请求数据等逻辑,确保请求到达实际业务处理函数(即视图函数)时,所需的一切上下文或条件已经准备就绪。 async def insert_post_feedback(post=Depends(create_post), handler=Depends(get_post_service)): post_dict = jsonable_encoder(post) post_obj = Post(**post_dict) handler.add_post(post_obj) return post
开发人员可将 触发器(trigger)、验证器(validator)、异常处理程序(exception handler)视为注入函数,这些依赖对象对传入的请求进行过滤,所以,它们的注入可发生在路径运算符,而不是服务参数列表。
3.3.3 路由器的 依赖注入
# DI 方法三: APIRouter 级别的依赖注入,用以处理特定的 REST API 服务组 # 意义:所有在该 APIRouter 中注册的 REST API 服务将始终触发这些依赖项的执行 router = APIRouter(dependencies=[Depends(count_user_by_type), Depends(check_credential_error)]) @router.get("/users/function/add") # DI 情形一: 通过 Depends() 注入依赖函数,其只能传入一个参数. Depends 里面传入的是对象 # Note: 该 router 已经依赖注入了 check_credential_error,对 username 和 password 进行校验,那么,将过滤由 create_login 可注入函数派生的 username 和 password def populate_user_accounts(user_account: Login = Depends(create_login)): account_dict = jsonable_encoder(user_account) login = Login(**account_dict) login_details[login.id] = login return login
3.3.4 针对 main.py 的 依赖注入
# 针对 main.py 的 FastAPI 的依赖注入,也成 '全局依赖项'。可以处理如:异常日志记录、缓存、检测和用户授权等 app = FastAPI(dependencies=[Depends(log_transaction)])
3.4 基于依赖关系组织项目
The Application ├── Model/ # 模型层,由资源、集合和 Python 类组成,存储库层可用它来创建 CRUD 事务 │ └── Ingredients │ └── Recipe │ └── Post ├── Factory/ # 工厂层,在存储库和服务层之间添加更松散的耦合设计 │ └── get_recipe_repo() # 放在存储库层的工厂方法中,在函数参数中依赖 存储库层的 RecipeRepository 类,同时被 服务层的 RecipeService 的构造方法所依赖 │ └── get_post_repo() │ └── get_users_repo() ├── Repository/ # 存储库层,由类依赖项组成,可访问数据存储或临时 dict 存储库。这些存储库类与模型层一起构建 REST API 所需的 CRUD 事务 │ └── RecipeRepository │ └── PostRepository │ └── LoginRepository ├── Factory/ # 工厂层,使用工厂设计模式,在存储库和服务层之间添加更松散的耦合设计。工厂方法在服务层被 依赖注入 │ └── get_recipe_service() │ └── get_post_service() │ └── get_complaint_service() ├── Service/ # 服务层, 拥有应用程序的包含领域逻辑的所有服务。使用的注入策略是类依赖函数,最合适的位置是在构造函数 │ └── RrecipeService │ └── PostService │ └── ComplaintService ├── API/ # API 层 │ └── REST API services
3.5 使用第三方容器
默认容器的优缺点:
- 优点:对象实例化工程、分解单体组件及建立松散耦合的结构等;
- 缺点:没有简单的设置将托管对象设置为单例(单例的好处:避免在 PVM 中浪费内存)、不支持更详细的容器配置,如多容器设置;
1. 知识点:FastAPI + Dependency Injector 配合使用组成微服务框架
FastAPI 和 Dependency Injector 可以很好地配合使用,共同组成一个基于Python的微服务框架。下面详细阐述它们如何协同工作以构建、组织和管理微服务: FastAPI: 1. API定义与路由: FastAPI 提供了简洁、直观的API定义方式,利用 Python 的类型提示来清晰地表述请求参数、响应模型及异常处理。通过装饰器(如@app.get(), @app.post()等)定义HTTP方法和路由,快速构建RESTful API。
2. 请求验证与序列化: 基于OpenAPI和JSON Schema,FastAPI自动进行请求数据的验证,并提供丰富的错误信息。它还支持自动序列化和反序列化请求/响应数据,简化数据处理过程。
3. 高性能与异步支持: FastAPI 建立在Starlette和Uvicorn之上,具有出色的性能表现。它原生支持异步编程,利用Python的 asyncio 库处理并发请求,非常适合构建高吞吐、低延迟的微服务。
Dependency Injector: 1. 依赖管理: Dependency Injector 负责管理微服务内部各组件(如服务类、数据访问层等)之间的依赖关系。通过定义依赖项、配置绑定以及使用 @inject() 装饰器,确保对象在创建时自动获取所需的依赖,实现松耦合和易测试的代码。
2. 生命周期管理: 该库支持单例、瞬态、线程局部等多种依赖生命周期,可以根据具体需求控制依赖对象的创建、共享或销毁方式,优化资源利用。
FastAPI与Dependency Injector配合使用: 1. 服务层设计: 使用FastAPI定义API接口,处理HTTP请求与响应。在服务层(如services.py)中,创建业务逻辑相关的类(如UserService),这些类可以使用Dependency Injector进行依赖注入。
2. 依赖注入配置: 创建一个配置模块(如dependencies.py),定义依赖项、设置绑定关系。例如,将数据库连接、缓存客户端、第三方服务客户端等作为依赖项,通过 Dependency Injector 的 Binder 进行配置。
from dependency_injector import containers, providers container = containers.DynamicContainer() container.db_client = providers.Singleton(DBClient, host='localhost', port=5432) container.cache_client = providers.Singleton(CacheClient, endpoint='redis://localhost:6379')
3. 服务类注入:
在服务类中,使用@inject()装饰器声明需要注入的依赖。例如,在UserService中注入db_client和cache_client:
from dependency_injector import inject from .dependencies import container class UserService: @inject def __init__(self, db_client: DBClient = container.db_client, cache_client: CacheClient = container.cache_client): self.db_client = db_client self.cache_client = cache_client async def get_user(self, user_id: int) -> User: # 使用注入的db_client和cache_client执行业务逻辑...
4. API路由与控制器:
在FastAPI应用中,创建控制器(如controllers.py)来对接API路由与服务层。控制器方法调用相应的服务类处理业务逻辑,然后返回响应。
from fastapi import APIRouter from .services import UserService router = APIRouter() @router.get("/users/{user_id}") async def get_user(user_id: int, user_service: UserService = Depends(UserService)): user = await user_service.get_user(user_id) return user
通过以上步骤,FastAPI与Dependency Injector成功配合,构建了一个结构清晰、依赖管理完善的Python微服务框架。FastAPI负责对外提供API接口、处理HTTP请求与响应,而Dependency Injector则负责内部服务层的依赖注入与管理,
两者协同实现了微服务的高效、解耦开发。
2. FastAPI + dependency_injector 集成
2.1 定义容器
# content of single_container.py """ dependency_injector 包含 容器 containers 和 提供者 provides 1. container: 类型之一是 DeclarativeContainer,它可以被子类化以包含其所有提供者 2. provides: 可以是 Factory、Dict、List、Callable、Singleton 或 其他容器 2.1 Dict & List: 都易于设置,只需分别实例化成 dict & list 2.2 Factory: 可实例化任何类,如存储库、服务或通用 Python 类 2.3 Singleton:只为每个类创建一个实例, 另外一种容器是 DynamicContainer,它是从配置文件、数据库或其他资源中构建的 """ from dependency_injector import containers, providers from repository.admin import AdminRepository from repository.keywords import KeywordRepository from repository.login import LoginRepository from repository.users import login_details from service.recipe_utilities import get_recipe_names # 通过简单地子类化 DeclarativeContainer 来创建单个容器 class Container(containers.DeclarativeContainer): # LoginRepository、KeywordRepository 通过 Factory 提供者作为实例注入 loginservice = providers.Factory(LoginRepository) keywordservice = providers.Factory(KeywordRepository) # AdminRepository 一个注入的单例对象 adminservice = providers.Singleton(AdminRepository) # get_recipe_names 一个注入的函数可依赖项 recipe_util = providers.Callable(get_recipe_names) # login_details 一个包含登录凭据的注入字典 login_repo = providers.Dict(login_details)
2.2 通过 @inject 将依赖项连接到组件
@router.post("/keyword/insert") @inject # 通过 @inject 将依赖项连接到组件 def insert_recipe_keywords(rid: UUID, keywords: List[str], keywordservice: KeywordRepository = Depends(Provide[Container.keywordservice])): if keywords is not None: keywords_list = list(keywords) keywordservice.insert_keywords(rid, keywords_list) return JSONResponse(content={"message": "inserted recipe keywords"}, status_code=201) else: return JSONResponse(content={"message": "invalid operation"}, status_code=403) container = Container() # 调用 container的 wire() 构建组装程序,需要放在末尾 # container.wire(): 方法用于自动解析和注入依赖。它遍历指定模块(在这里是当前模块 sys.modules[__name__],即调用该代码的模块)中所有使用了 # 依赖注入标记(如 @inject、Depends() 等)的组件,并根据配置自动连接这些组件,确保每个依赖都能正确实例化并传递给需要它的部分。 # modules=[sys.modules[__name__]]: 这部分告诉容器从哪里开始查找和解析依赖。sys.modules[__name__] 是一个动态引用,指向当前模块。 # 这意味着容器将扫描当前模块中的所有依赖声明,并基于容器内的配置来满足这些依赖 container.wire(modules=[sys.modules[__name__]])
# 上述代码的解释 依赖注入: keywordservice: 类型为 KeywordRepository,表示一个关键词仓库接口或类,它提供了与关键词数据相关的操作(如添加、查询、删除等)。
这个参数使用了依赖注入的方式传递,具体体现在以下几个方面: Depends: 这是一个来自 FastAPI 或其他类似框架的装饰器,用于声明某个参数应该从依赖项解析器中获取其值,而不是直接作为函数调用时的实参传入。 Provide[Container.keywordservice]: 这部分指定了依赖项的来源。
Provide 通常用于指定依赖项的提供者或容器,这里的 Container 是指一个依赖注入容器,如 Pydantic 的 BaseSettings、FastAPI 的 FastAPI 实例等。
Container.keywordservice 表示在该容器中查找名为 keywordservice 的依赖项。
综上所述,这个函数的作用是在给定食谱 ID (rid) 和关键词列表 (keywords) 的情况下,通过依赖注入的方式获取 KeywordRepository 类型的 keywordservice 实例,并利用该实例提供的方法将关键词插入到指定食谱中。
这样设计的好处是实现了业务逻辑与底层数据操作的解耦,使得代码更易于测试和维护。同时,依赖注入使得外部组件(如数据库连接、缓存服务等)的配置和管理更加集中和灵活。
2.3. 多容器的处理:
# content of \containers\multiple_containers.py # 分别创建相关的实例 class KeywordsContainer(containers.DeclarativeContainer): keywordservice = providers.Factory(KeywordRepository) class AdminContainer(containers.DeclarativeContainer): adminservice = providers.Singleton(AdminRepository) # 最后,通过依赖注入整合这 3 个 容器 class RecipeAppContainer(containers.DeclarativeContainer): keywordcontainer = providers.Container(KeywordsContainer) admincontainer = providers.Container(AdminContainer) # content of \api\admin_mcontainer.py @router.get("/admin/logs/visitors/list") @inject # 依赖注入多容器的时候,需要在连接标记中指明容器 def list_logs_visitors(adminservice: AdminRepository = Depends(Provide[RecipeAppContainer.admincontainer.adminservice])): logs_visitors_json = jsonable_encoder(adminservice.query_logs_visitor()) return logs_visitors_json container = RecipeAppContainer() container.wire(modules=[sys.modules[__name__]])
3. FastAPI + Lagom 结构
# content of \api\complaints.py from uuid import UUID from fastapi import APIRouterfrom fastapi.responses import JSONResponse from lagom import Container # 导入 FastAPI 集成模块 from lagom.integrations.fast_api import FastApiIntegration from repository.complaints import BadRecipeRepository # step1. 初始化容器并注册服务(对比 Dependency Injector,container 的创建是在一开始,而后者是在模块最末尾的地方创建) container = Container() # 容器的行为类型于 dict,注入对象时,类名作为键,实例作为值,但在更复杂的场景中,推荐使用工厂方法或配置类来初始化依赖,以便更好地管理依赖的生命周期和配置。 container[BadRecipeRepository] = BadRecipeRepository() # step2: 设置FastAPI集成 # 实例化 FastApiIntegration,并将容器以及需要作为单例请求的依赖列表(此处为 [BadRecipeRepository])传入。 # 这意味着每次请求都会复用同一个 BadRecipeRepository 实例,符合大多数Web服务的常见模式 deps = FastApiIntegration(container, request_singletons=[BadRecipeRepository]) # step3: 定义API路由器和端点 router = APIRouter() @router.post("/complaint/recipe") # 在 report_recipe 路由中,通过 deps.depends(BadRecipeRepository) 方式注入了 BadRecipeRepository 的实例。 # 这样,FastAPI在处理请求时会自动从Lagom容器中获取这个依赖 def report_recipe(rid: UUID, complaintservice=deps.depends(BadRecipeRepository)): complaintservice.add_bad_recipe(rid) return JSONResponse(content={"message": "reported bad recipe"}, status_code=201)
4. FastAPI 的依赖注入默认不支持单例对象的创建,所以,如果需要则考虑使用 第三方的 Dependency Injector 和 Lagom
# Denpendency Injector:
from dependency_injector import containers, providers from repository.admin import AdminRepository # 通过简单地子类化 DeclarativeContainer 来创建单个容器 class Container(containers.DeclarativeContainer): # AdminRepository 一个注入的单例对象 adminservice = providers.Singleton(AdminRepository)
# Lagom:
from lagom import Container, Singleton from repository.complaints import BadRecipeRepository container = Container() # lagom 的单例设置形式一,通过 lagom 的 Singleton 类 container[BadRecipeRepository] = Singleton(BadRecipeRepository()) deps = FastApiIntegration(container) # lagom 的单例设置形式二, 通过 FastApiIntegration 的构造函数的 request_singletons 参数 container[BadRecipeRepository] = BadRecipeRepository() deps = FastApiIntegration(container, request_singletons=[BadRecipeRepository])