一、FastAPI 依赖注入简介

FastAPI 是一个基于 Python 的快速 Web 框架,它的依赖注入功能非常强大。依赖注入,简单来说,就是在一个对象需要另一个对象来完成某个任务时,通过某种方式将这个依赖对象传递给它,而不是在对象内部自己创建。

比如,我们有一个用户服务,它可能需要数据库连接来获取用户信息。在没有依赖注入的情况下,用户服务可能会自己去创建数据库连接。但这样做有很多缺点,比如代码的可测试性差,因为测试时可能不想真的去连接数据库;而且如果数据库连接的方式发生变化,需要修改的地方可能很多。

而使用依赖注入,我们可以把数据库连接这个依赖对象外部传递给用户服务。这样,我们可以很方便地替换不同的数据库连接实现,也方便进行单元测试。

二、FastAPI 依赖注入的原理

2.1 依赖项的定义

在 FastAPI 中,我们可以使用函数来定义依赖项。例如,我们定义一个获取数据库连接的依赖项:

# 这里使用 SQLAlchemy 库来演示数据库连接相关操作
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

# 创建数据库引擎
engine = create_engine('sqlite:///./test.db')
# 创建会话工厂
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

这个 get_db 函数就是一个依赖项,它会返回一个数据库会话对象。

2.2 依赖项的注入

当我们定义一个 API 端点时,可以在函数参数中使用依赖项。例如:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class User(BaseModel):
    username: str
    password: str

@app.post("/users/")
def create_user(user: User, db=Depends(get_db)):
    # 在这里可以使用 db 来操作数据库
    # 比如将用户信息插入数据库
    try:
        db.add(user)
        db.commit()
        db.refresh(user)
        return user
    except Exception as e:
        db.rollback()
        raise e
    finally:
        db.close()

在这个例子中,create_user 函数的 db 参数就是通过依赖注入获取的。FastAPI 会自动调用 get_db 函数来获取数据库会话对象,并传递给 create_user 函数。

2.3 依赖项的作用域

FastAPI 支持不同的依赖项作用域,比如请求级别的作用域。在上面的例子中,get_db 函数返回的数据库会话对象就是请求级别的,即每个请求都会创建一个新的数据库会话,并且在请求结束后会自动关闭。

还有应用级别的作用域,比如我们可以定义一个全局的配置对象:

from fastapi import FastAPI

app = FastAPI()

class Settings:
    app_name = "My FastAPI App"
    debug = False

def get_settings():
    return Settings()

@app.get("/settings/")
def read_settings(settings=Depends(get_settings)):
    return settings

这里的 get_settings 函数返回的 Settings 对象就是应用级别的,整个应用生命周期内只会创建一次。

三、FastAPI 依赖注入的应用场景

3.1 数据库操作

就像前面的例子一样,在 Web 应用中,数据库操作是非常常见的。通过依赖注入,我们可以方便地管理数据库连接,提高代码的可维护性和可测试性。

比如,我们可以很容易地切换不同的数据库,只需要修改 get_db 函数中的数据库连接字符串即可,而不需要修改所有使用数据库连接的 API 端点。

3.2 认证和授权

在一个需要用户认证和授权的应用中,我们可以使用依赖注入来获取当前用户的信息。

例如,我们有一个认证中间件,它会验证用户的令牌,并将用户信息存储在请求上下文中。然后我们可以定义一个依赖项来获取当前用户:

from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt

# 假设我们有一个密钥和算法
SECRET_KEY = "your-secret-key"
ALGORITHM = "HS256"

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

def get_current_user(token: str = Depends(oauth2_scheme)):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Could not validate credentials",
                headers={"WWW-Authenticate": "Bearer"},
            )
        # 这里可以根据用户名查询用户信息并返回
        return {"username": username}
    except JWTError:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Could not validate credentials",
            headers={"WWW-Authenticate": "Bearer"},
        )

@app.get("/protected/")
def protected_route(user=Depends(get_current_user)):
    return {"message": f"Hello, {user['username']}"}

这样,只有经过认证的用户才能访问 /protected/ 端点。

3.3 日志记录

我们可以通过依赖注入来获取日志记录器。例如:

import logging
from fastapi import FastAPI

app = FastAPI()

def get_logger():
    logger = logging.getLogger(__name__)
    logger.setLevel(logging.INFO)
    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    ch = logging.StreamHandler()
    ch.setFormatter(formatter)
    logger.addHandler(ch)
    return logger

@app.get("/log/")
def log_message(logger=Depends(get_logger)):
    logger.info("This is an info log message")
    return {"message": "Logged successfully"}

这样,每个需要记录日志的 API 端点都可以通过依赖注入获取日志记录器,方便统一管理日志。

四、FastAPI 依赖注入的技术优缺点

4.1 优点

  • 提高代码的可维护性:通过依赖注入,我们可以将依赖对象的创建和管理集中在一个地方,而不是分散在各个业务逻辑代码中。当依赖对象的实现发生变化时,只需要修改依赖项的定义,而不需要修改大量的业务逻辑代码。
  • 增强代码的可测试性:在单元测试中,我们可以很方便地替换依赖对象,从而隔离被测试的代码。例如,在测试一个 API 端点时,我们可以使用一个模拟的数据库连接对象,而不需要真的连接数据库,这样可以提高测试的速度和稳定性。
  • 实现依赖的复用:依赖项可以被多个 API 端点或其他组件复用,减少了代码的重复。

4.2 缺点

  • 增加代码的复杂性:依赖注入本身是一种比较复杂的设计模式,在使用过程中需要了解其原理和机制。对于初学者来说,可能需要花费一些时间来理解和掌握。
  • 可能导致性能问题:如果依赖项的创建过程比较复杂,比如创建数据库连接等,可能会在每次请求时带来一定的性能开销。不过,通过合理的设计和缓存等机制,可以在一定程度上缓解这个问题。

五、FastAPI 依赖注入的注意事项

5.1 依赖项的顺序

在定义 API 端点的参数时,依赖项的顺序很重要。FastAPI 会按照参数的顺序来解析依赖项。如果一个依赖项依赖于另一个依赖项,那么被依赖的依赖项应该放在前面。

例如:

def dependency1():
    return "dependency1"

def dependency2(d1=Depends(dependency1)):
    return f"{d1} and dependency2"

@app.get("/")
def endpoint(d2=Depends(dependency2)):
    return d2

这里 dependency2 依赖于 dependency1,所以 dependency1 应该在 dependency2 的参数列表中首先被解析。

5.2 避免循环依赖

循环依赖是指两个或多个依赖项相互依赖,这会导致 FastAPI 在解析依赖项时陷入死循环。

例如:

def circular_dependency1(d2=Depends(circular_dependency2)):
    return f"dependency1 with {d2}"

def circular_dependency2(d1=Depends(circular_dependency1)):
    return f"dependency2 with {d1}"

这样的代码会导致错误,应该避免出现循环依赖的情况。

5.3 依赖项的清理

对于一些有资源占用的依赖项,比如数据库连接、文件句柄等,需要在使用完毕后进行清理。在前面的数据库连接例子中,我们使用了 try - finally 块来确保数据库会话在使用完毕后被关闭。

六、文章总结

FastAPI 的依赖注入功能为开发者提供了一种强大的方式来管理对象之间的依赖关系。通过合理地使用依赖注入,我们可以提高代码的可维护性、可测试性和复用性。

在实际应用中,我们需要根据不同的场景选择合适的依赖项作用域,并注意依赖项的顺序和清理等问题。同时,我们也要认识到依赖注入带来的一些缺点,比如代码复杂性增加和可能的性能问题,并采取相应的措施来解决。