读后笔记 -- 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
3. 依赖注入框架 Python 中有一些框架和库专门支持依赖注入,它们提供了自动化的依赖解析、生命周期管理等功能,简化了依赖注入的实现: Injector: 一个轻量级的依赖注入容器,通过定义依赖关系的配置,自动管理对象的创建和依赖注入。 Dependency Injector: 另一个功能丰富的依赖注入框架,支持多种注入方式、依赖生命周期管理、配置文件加载等。 Pyramid: 虽然 Pyramid 主要是 Web 框架,但它内置了对依赖注入的支持,通过配置文件或代码定义服务及其依赖关系。 Django 和 Flask 等 Web 框架虽不直接提供依赖注入容器,但可通过第三方库(如 Django-DI 或 Flask-DI)或遵循依赖注入原则自行实现。
4. 依赖注入的应用场景 共享业务逻辑:复用相同的代码逻辑,如数据访问、验证、缓存等。 共享数据库连接:集中管理数据库连接,避免重复创建和释放资源。 实现安全、验证、角色权限:将这些横切关注点的实现作为依赖注入,便于统一管理和更新。 测试:轻松模拟或替换实际依赖,实现单元测试和集成测试。 模块化和可扩展性:通过依赖注入,可以轻松替换组件或添加新功能,适应不断变化的需求。 总之,依赖注入是一种提升代码质量、促进模块化和可维护性的强大工具。合理选择适合项目的注入方式(构造函数、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 只能用于单个依赖项的声明,但可以为路径操作函数的多个参数分别使用 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 # 无参数的类,即 __init__ 无参数入参 class Member: def __init__(self): self.member_class= "vip" #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\users.py def create_login(id: UUID, username: str, password: str, type: UserType): account = {"id": id, "username": username, "password": password, "type": type} return account async def create_user_details(id: UUID, firstname: str, lastname: str, middle: str, bday: date, pos: str, login=Depends(create_login)): user = {"id": id, "firstname": firstname, "lastname": lastname, "middle": middle, "bday": bday, "pos": pos, "login": login} return user @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
依赖于类时,可简化写法
class Profile: # Profile 类依赖于 Login 类和 UserDetails 类 def __init__(self, id: UUID, date_created: date, login=Depends(Login), user=Depends(UserDetails)): self.id = id self.date_created = date_created self.login = login self.user = user @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,通过 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}
所有依赖项都是可缓存的。by default, use_cache=True,一般情况下,该依赖项是通用的,下次请求时,直接从容器中/缓存中 获取该对象。
上面例子中,使用 use_cache=False,说明每次获取时,不从缓存中,而是 实时获取。
2.4 注入异步依赖项
容器可以管理同步和异步函数依赖项:
- 异步 API 服务上连接同步依赖项; 2.3 例子中的第1个例子,就是在异步 API 中,注入同步依赖项。即 异步函数 create_user_details() 依赖注入 同步函数 create_login()
- 异步 API 服务上连接 异步依赖项;=> 一般直接使用 async / await
- 同步 API 服务上连接 异步依赖项;
- 同步 API 服务上连接 同步依赖项;
3.3 注入依赖项的方法
3.3.1 API 参数上 依赖注入
函数和类依赖项可以与 GET, POST, PUT, PATCH 一起使用。但具有属性类型(如 Enum 和 UUID)的依赖项,可能由于转换问题而导致 HTTP 422。
@router.get("/recipes/list/all") # DI 方法一:服务参数列表上的依赖注入,可用以处理事务,位置放在最后 def get_all_recipes(handler=Depends(get_recipe_service)): return handler.get_recipes()
3.3.2 在 router 路径运算符中的 依赖注入
@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 路由器 Router 的 依赖注入
# 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)])
总结并扩展:
1)一个路由端点可以与多个路由函数相关联:
from fastapi import FastAPI app = FastAPI() @app.get("/recipes/123") async def get_recipe(): return {"method": "GET", "description": "Retrieve recipe details"} @app.post("/recipes/123") async def update_recipe(): return {"method": "POST", "description": "Update recipe details"} @app.delete("/recipes/123") async def delete_recipe(): return {"method": "DELETE", "description": "Delete recipe"}
2)用途:
应用方式 | 用途/应用范围 |
API 服务参数上依赖注入 | 前处理,仅影响该 API |
Router 路径运算符上依赖注入 | 前处理,影响访问该路由端点的所有 API 函数(如上面总结的例子) |
Router 的依赖注入 | 前处理,影响所有该路由创建的端点 |
FastAPI 的依赖注入 | 前处理,影响该 FastAPI include_router() 的所有路由,“全局依赖” |
middleware | 前处理 + 后处理,具体实现参照 Chapter2 |
3.4 基于依赖关系组织项目
1. 基于依赖关系形成的项目结构
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
2. 相关代码(按请求执行的顺序):
2.1 main.py
from fastapi import FastAPI, Depends from api import recipes from dependencies.global_transactions import log_transaction # 针对 main.py 的 FastAPI 的依赖注入,也成 '全局依赖项'。可以处理如:异常日志记录、缓存、检测和用户授权等 app = FastAPI(dependencies=[Depends(log_transaction)]) app.include_router(recipes.router, prefix="/ch03")
2.2 api\recipes.py
from fastapi import APIRouter, Depends router = APIRouter() @router.get("/recipes/list/all") # API 调用 svc 的方式,可通过 注入服务类或工厂方法 # 这里的实现:依赖注入 get_recipe_service 这个工厂方法 -> 依赖注入 服务类 def get_all_recipes(handler=Depends(get_recipe_service)): return handler.get_recipes()
2.3 service\factory.py, recipes.py
from fastapi import Depends from service.recipes import RecipeService def get_recipe_service(repo=Depends(RecipeService)): return repo
from fastapi import Depends from model.recipes import Recipe from repository.factory import get_recipe_repo class RecipeService: # 服务层使用的注入策略是类依赖函数。构造函数式最合适的位置 # 该类的方法 get_recipes() 和 add_recipe() 是由 get_recipe_repo() 派生的事务而实现的 def __init__(self, repo=Depends(get_recipe_repo)): self.repo = repo def get_recipes(self): return self.repo.query_recipes() def add_recipe(self, recipe: Recipe): self.repo.insert_recipe(recipe)
2.4 repository\factory.py, recipes.py
from fastapi import Depends def get_recipe_repo(repo=Depends(RecipeRepository)): return repo
from uuid import uuid1 from model.classifications import Category, Origin from model.recipes import Ingredient, Recipe recipes = dict() class RecipeRepository: """ 该类由两个事务: insert_recipe() 和 query_recipes() 其构造函数用于使用一些初始化来来填充 """ def __init__(self): ingrA1 = Ingredient(measure='cup', qty=1, name='grape tomatoes', id=uuid1()) ingrA2 = Ingredient(measure='teaspoon', qty=0.5, name='salt', id=uuid1()) ingrA3 = Ingredient(measure='pepper', qty=0.25, name='pepper', id=uuid1()) ingrA4 = Ingredient(measure='pound', qty=0.5, name='asparagus', id=uuid1()) ingrA5 = Ingredient(measure='teaspoon', qty=2, name='olive oil', id=uuid1()) ingrA6 = Ingredient(measure='pieces', qty=4, name='large eggs', id=uuid1()) ingrA7 = Ingredient(measure='cup', qty=1, name='milk', id=uuid1()) ingrA8 = Ingredient(measure='cup', qty=0.5, name='whipped cream cheese', id=uuid1()) ingrA9 = Ingredient(measure='cup', qty=0.25, name='Parmesan cheese', id=uuid1()) recipeA = Recipe(orig=Origin.european, ingredients=[ingrA1, ingrA2, ingrA3, ingrA4, ingrA5, ingrA6, ingrA7, ingrA8, ingrA9], cat=Category.breakfast, name='Crustless quiche bites with asparagus and oven-dried tomatoes', id=uuid1()) ingrB1 = Ingredient(measure='tablespoon', qty=1, name='oil', id=uuid1()) ingrB2 = Ingredient(measure='cup', qty=0.5, name='chopped tomatoes', id=uuid1()) ingrB3 = Ingredient(measure='minced', qty=1, name='pepper', id=uuid1()) ingrB4 = Ingredient(measure='drop', qty=1, name='salt', id=uuid1()) ingrB5 = Ingredient(measure='pieces', qty=2, name='large eggs', id=uuid1()) recipeB = Recipe(orig=Origin.carribean, ingredients=[ingrB1, ingrB2, ingrB3, ingrB4, ingrB5], cat=Category.breakfast, name='Fried eggs, Caribbean style', id=uuid1()) ingrC1 = Ingredient(measure='pounds', qty=2.25, name='sweet yellow onions', id=uuid1()) ingrC2 = Ingredient(measure='cloves', qty=10, name='garlic', id=uuid1()) ingrC3 = Ingredient(measure='minced', qty=1, name='blackpepper', id=uuid1()) ingrC4 = Ingredient(measure='drop', qty=1, name='kasher salt', id=uuid1()) ingrC5 = Ingredient(measure='cup', qty=4, name='low-sodium chicken brothlarge eggs', id=uuid1()) ingrC6 = Ingredient(measure='tablespoon', qty=4, name='sherry', id=uuid1()) ingrC7 = Ingredient(measure='sprig', qty=7, name='thyme', id=uuid1()) ingrC8 = Ingredient(measure='cup', qty=0.5, name='heavy cream', id=uuid1()) recipeC = Recipe(orig=Origin.mediterranean, ingredients=[ingrC1, ingrC2, ingrC3, ingrC4, ingrC5, ingrC6, ingrC7, ingrC8], cat=Category.soup, name='Creamy roasted onion soup', id=uuid1()) recipes[recipeA.id] = recipeA recipes[recipeB.id] = recipeB recipes[recipeC.id] = recipeC def insert_recipe(self, recipe: Recipe): recipes[recipe.id] = recipe def query_recipes(self): return recipes
2.5 model\recipes.py
from uuid import UUID from model.classifications import Category, Origin from typing import List class Ingredient: def __init__(self, id: UUID, name: str, qty: float, measure: str): self.id = id self.name = name self.qty = qty self.measure = measure class Recipe: def __init__(self, id: UUID, name: str, ingredients: List[Ingredient], cat: Category, orig: Origin): self.id = id self.name = name self.ingredients = ingredients self.cat = cat self.orig = orig
2.6 dependencies\global_transactions.py
from fastapi import Request from uuid import uuid1 service_paths_log = dict() def log_transaction(request: Request): service_paths_log[uuid1()] = request.url.path
此时的调用路径是:request -> main.py -> api -> service\factory -> service\具体 service -> repository\factory 对象 + repository\具体方法 -> 引用 model
3.5 使用第三方容器
默认容器的优缺点:
- 优点:对象实例化工程、分解单体组件及建立松散耦合的结构等;
- 缺点:没有简单的设置将托管对象设置为单例(单例的好处:避免在 PVM 中浪费内存)、不支持更详细的容器配置,如多容器设置;
3.5.1 FastAPI + Dependency Injector 容器 配合使用组成微服务框架,知识点:
FastAPI 和 Dependency Injector 可以很好地配合使用,共同组成一个基于Python的微服务框架。下面详细阐述它们如何协同工作以构建、组织和管理微服务: FastAPI: 1. API定义与路由: 提供了简洁、直观的API定义方式,利用 Python 的类型提示来清晰地表述请求参数、响应模型及异常处理。通过装饰器(如@app.get(), @app.post()等)定义HTTP方法和路由,快速构建RESTful API。
2. 请求验证与序列化: 基于OpenAPI和JSON Schema,FastAPI自动进行请求数据的验证,并提供丰富的错误信息。它还支持自动序列化和反序列化请求/响应数据,简化数据处理过程。
3. 高性能与异步支持: 建立在Starlette和Uvicorn之上,具有出色的性能表现。它原生支持异步编程,利用Python的 asyncio 库处理并发请求,非常适合构建高吞吐、低延迟的微服务。
Dependency Injector: 1. 依赖管理: 负责管理微服务内部各组件(如服务类、数据访问层等)之间的依赖关系。通过定义依赖项、配置绑定以及使用 @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则负责内部服务层的依赖注入与管理,
两者协同实现了微服务的高效、解耦开发。
3.5.1.1 FastAPI + dependency_injector 集成的项目结构
# 安装包 pip install dependency_injector
使用容器后的项目结构:
The Application ├── api/ # 接口包 │ └── admin.py # FastAPI + DI 的微服务结构,单容器方式 │ └── admin_mcontainer.py # FastAPI + DI 的微服务结构,多容器方式 │ └── admin_mcontainer.py # FastAPI + Lagom 的微服务结构 │ └── keywords.py # FastAPI + DI 的微服务结构,单容器方式,通过 @inject 将依赖项连接到组件,并在依赖项组件上进行装饰 │ └── ... ├── containers/ # 容器包 │ └── single_container.py # 简单地子类化 DeclarativeContainer,创建单个容器, 其实例由 providers 注入,如注入 LoginRepository │ └── multiple_containers.py # 多容器 ├── repository/ # 存储库包 │ └── KeywordRepository(keywords.py) │ └── ... └── main.py 生成 app
3.5.1.2 定义容器
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): # 每次请求都创建一个实例 loginservice = providers.Factory(LoginRepository) keywordservice = providers.Factory(KeywordRepository) # 应用程序的生命周期中仅一个实例 adminservice = providers.Singleton(AdminRepository) # get_recipe_names 一个注入的函数可依赖项 recipe_util = providers.Callable(get_recipe_names) # login_details 一个包含登录凭据的注入字典 login_repo = providers.Dict(login_details)
对上面代码的详细解释:
dependency_injector 包含 容器 containers 和 提供者 provides
1. containers.DeclarativeContainer 是dependency_injector中的一个基类,用于定义容器。容器是依赖项的集合,它管理着依赖项的生命周期和它们之间的关系。
2. provides: 可以是 Factory、Dict、List、Callable、Singleton 或 其他容器
Provider 容器类型 | 作用 | 应用场景 |
Dict & List |
允许注入一个字典或列表 |
适用于那些需要传递静态数据或配置的服务 |
Factory |
用于创建新的实例。可实例化任何类,如存储库、服务或通用 Python 类。 |
适合那些需要频繁创建新实例的服务,例如每次请求都需要一个新的数据库会话 |
Singleton | 确保在整个应用程序的生命周期内,只为每个类创建一个实例 | 适用于那些开销较大或状态需要在整个应用中共享的服务 |
Callable | 允许将一个函数或方法作为依赖项注入 | 适用于那些需要在运行时动态计算或获取数据的服务 |
3. 另外一种容器是 containers.DynamicContainer,它是从配置文件、数据库或其他资源中构建的
3.5.1.3 通过 @inject 将依赖项连接到组件,实现 FastAPI 和 Depency Injector 容器的集成 + wire() 构建组装程序
import sys from typing import List from uuid import UUID from dependency_injector.wiring import Provide, inject from fastapi import APIRouter, Depends from fastapi.encoders import jsonable_encoder from containers.single_container import Container from fastapi.responses import JSONResponse from repository.keywords import KeywordRepository router = APIRouter() @router.post("/keyword/insert") @inject # 通过 @inject 将依赖项连接到组件,并在依赖项组件上进行装饰 # keywordservice 是依赖项组件,KeywordRepository 是依赖项的类型 # Depends(Provide[Container.keywordservice]) :特别指出了如何获取这个依赖。它使用了依赖注入容器(dependency_injector), # 通过 Container.keywordservice 来提供 KeywordRepository 的具体实现实例 # 这意味着在运行时,框架会自动解析并注入符合 KeywordRepository 接口的实例给 keywordservice 参数,而无需直接在函数内部创建或管理这个对象的生命周期 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 是指一个依赖注入容器 Dependency Injector,如 Pydantic 的 BaseSettings、FastAPI 的 FastAPI 实例等。
Container.keywordservice 表示在该容器中查找名为 keywordservice 的依赖项。
综上所述,这个函数的作用是在给定食谱 ID (rid) 和关键词列表 (keywords) 的情况下,通过依赖注入的方式获取 KeywordRepository 类型的 keywordservice 实例,并利用该实例提供的方法将关键词插入到指定食谱中。
这样设计的好处是实现了业务逻辑与底层数据操作的解耦,使得代码更易于测试和维护。同时,依赖注入使得外部组件(如数据库连接、缓存服务等)的配置和管理更加集中和灵活。
3.5.1.4 多容器的处理(大型应用程序时):
from dependency_injector import containers, providers from repository.admin import AdminRepository from repository.keywords import KeywordRepository from repository.login import LoginRepository # step1:分别创建 3 个相关的实例,按业务用途区分 # 组装与关键字相关的所有依赖项 class KeywordsContainer(containers.DeclarativeContainer): keywordservice = providers.Factory(KeywordRepository) # 保存与管理任务相关的所有实例 class AdminContainer(containers.DeclarativeContainer): adminservice = providers.Singleton(AdminRepository) # 与登录和用户相关的服务 class LoginContainer(containers.DeclarativeContainer): loginservice = providers.Factory(LoginRepository) # step2:最后,通过依赖注入整合这 3 个 容器 class RecipeAppContainer(containers.DeclarativeContainer): keywordcontainer = providers.Container(KeywordsContainer) admincontainer = providers.Container(AdminContainer) logincontainer = providers.Container(LoginContainer)
import sys from dependency_injector.wiring import Provide, inject from fastapi import APIRouter, Depends from fastapi.encoders import jsonable_encoder from containers.multiple_containers import RecipeAppContainer from repository.admin import AdminRepository router = APIRouter() @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.5.2 FastAPI + Lagom 容器
安装: pip install lagom
lagom 的优点:线程安全,非阻塞,同时简单、流畅和直接的 API、设置和配置
与 Dependency Injector 的最大不同:container 的创建是在一开始,而后者是在模块最末尾的地方创建
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)
注意的是:lagom 的单例容器的实现有两种方式:
# lagom 的单例设置方式一: 使用 Singleton 类 from lagom import Container, Singleton # 导入 FastAPI 集成模块 from lagom.integrations.fast_api import FastApiIntegration container = Container() container[BadRecipeRepository] = Singleton(BadRecipeRepository()) deps = FastApiIntegration(container)
# lagom 的单例容器设置方式二:使用 FastAPIIntegration 构造函数 from lagom.integrations.fast_api import FastApiIntegration # 实例化 FastApiIntegration,并将容器以及需要作为单例请求的依赖列表(此处为 [BadRecipeRepository])传入。 # 这意味着每次请求都会复用同一个 BadRecipeRepository 实例,符合大多数Web服务的常见模式 deps = FastApiIntegration(container, request_singletons=[BadRecipeRepository])
4. FastAPI 的依赖注入默认不支持单例对象的创建,所以,如果需要则考虑使用 第三方的 Dependency Injector 和 Lagom。具体代码参考上面实现