要搞清楚 FastAPI 项目启动的执行逻辑,需要先明确 “项目启动流程”“main 函数角色”“lifespan 作用”“导入语句执行时机” 这几个核心点的关系,下面逐一拆解:

一、FastAPI 项目启动:先执行 “导入语句”,再执行 “main 函数”,最后触发 “lifespan”

FastAPI 项目的启动并非单一入口执行,而是遵循 “Python 解释器执行顺序”+“FastAPI 框架初始化逻辑” 的组合流程,整体顺序如下:

1. 第一步:执行所有 “导入语句”(最早发生)

当你通过命令(如 uvicorn main:app --reload)启动项目时,Python 解释器会先加载所有被引用的模块 ——导入语句(import)的执行时机,是 “模块被加载时”,早于任何函数(包括 main)的调用

FastAPI 项目的启动并非单一入口执行,而是遵循 “Python 解释器执行顺序”+“FastAPI 框架初始化逻辑” 的组合流程,整体顺序如下:

1. 第一步:执行所有 “导入语句”(最早发生)

当你通过命令(如 uvicorn main:app --reload)启动项目时,Python 解释器会先加载所有被引用的模块 ——导入语句(import)的执行时机,是 “模块被加载时”,早于任何函数(包括 main)的调用

举个典型的项目结构例子:

your_project/
├── main.py       # 入口文件,有 app = FastAPI()、main 函数
└── config.py     # 配置文件,有 AGENT_CONFIGS、预加载文件逻辑

如果 main.py 中有这样的代码:

# main.py
# 1. 导入语句:此时会立即执行 config.py 中的所有代码
import config  
from fastapi import FastAPI

# 2. 初始化 FastAPI 实例:此时执行(早于 main 函数)
app = FastAPI()

# 3. 定义 main 函数(启动命令中可能不直接调用,除非主动执行)
def main():
    print("执行 main 函数")

# 4. 定义 lifespan(FastAPI 1.0+ 推荐的生命周期钩子)
@app.lifespan("startup")
async def startup_event():
    print("执行 startup 生命周期事件")

当你运行 uvicorn main:app 时,第一步就是执行 导入语句 import config—— 这会触发 config.py 中的所有代码(包括初始化函数、类、变量如 AGENT_CONFIGS” 等逻辑),且这个过程在 app 实例化、main 函数调用、lifespan 触发之前。

2. 第二步:FastAPI 初始化 app 实例(无 main 时也会执行)

导入完成后,Python 解释器会执行 main.py 中 “顶层代码”(即不在函数 / 类内部的代码),比如 定义常量、全局变量等。如app = FastAPI()—— 这一步会初始化 FastAPI 应用的核心对象(路由、中间件、生命周期等),但不会启动服务。

注意:main 函数并非 FastAPI 启动的 “必需入口”

  • 如果你用 uvicorn main:app 启动,uvicorn 会直接加载 main.py 中的 app 实例,不会主动调用 main 函数(除非你在 main.py 中主动加 if __name__ == "__main__": main());
  • 只有当你用 “自定义启动逻辑”(比如用 FastAPI.run() 或 uvicorn.run() 在 main 函数中启动)时,main 函数才会被执行,例如:
    # 自定义 main 函数启动服务(非必需,但常见于需要预处理的场景)
    def main():
        # 启动前的预处理(如检查配置)
        print("启动前检查配置...")
        # 调用 uvicorn 启动服务
        uvicorn.run("main:app", host="0.0.0.0", port=8000)
    
    if __name__ == "__main__":
        main()  # 此时运行 `python main.py` 会执行 main 函数
    3. 第三步:触发 lifespan 生命周期事件(服务启动 / 关闭时执行)

    当 uvicorn 成功加载 app 实例并准备启动服务时,会触发 FastAPI 的 lifespan 生命周期钩子—— 这是 FastAPI 框架层面提供的 “服务启动 / 关闭时执行代码” 的标准方式,优先级低于 “导入语句” 和 “顶层代码”,但高于 “用户请求处理”。

     

    lifespan 的执行时机:

  • startup 事件:服务启动成功后、开始接收用户请求前执行(比如初始化数据库连接、加载全局缓存);
  • shutdown 事件:服务停止前、断开所有用户连接后执行(比如关闭数据库连接、释放资源)。

例如你在 main.py 中定义的 startup_event,会在 app 初始化完成、uvicorn 准备好接收请求时执行,晚于 import config 和 app = FastAPI(),早于第一个用户请求。


步骤如下:

执行步骤 操作内容 执行时机 是否主动调用
1 执行 import config 等导入语句 Python 加载模块时(最早) 自动(解释器触发)
2 执行 app = FastAPI() main.py 顶层代码执行时(导入后) 自动(解释器触发)
3 执行 main() 函数(若有) 运行 python main.py 且触发 if __name__ == "__main__" 时 手动(需代码调用)
4 执行 startup 生命周期事件 服务启动成功后、接收请求前 自动(FastAPI 触发)
5 处理用户请求 服务启动后、startup 事件执行完成后 被动(用户触发)
6 执行 shutdown 生命周期事件 服务停止前、断开用户连接后 自动(FastAPI 触发)

三、实际开发中的注意事项

  1. “预加载配置” 适合在导入时执行
    在 配置文件如config.py 中 属于 “服务启动时仅需执行一次” 的操作,放在 config.py 的顶层代码中(导入时执行)是最优选择 —— 既无需依赖 main 函数,也无需放在 lifespan 中,且执行时机最早,后续 main.py 或其他模块导入 config 时可直接使用 AGENT_CONFIGS,无运行时阻塞风险。

  2. main 函数仅用于 “自定义启动逻辑”
    不要把 “初始化配置”“连接数据库” 等逻辑放在 main 函数中(除非你必须通过 python main.py 启动且需要自定义参数),因为如果后续用 uvicorn main:app 启动,main 函数不会被执行,可能导致初始化逻辑丢失。

  3. lifespan 适合 “服务级资源管理”
    若你需要在服务启动后、接收请求前做一些 “动态初始化”(比如根据环境变量调整配置、检查外部服务可用性),或在服务停止时释放资源,优先用 lifespan 而非 main 函数 —— 因为 lifespan 是 FastAPI 官方推荐的生命周期管理方式,无论用何种方式启动服务(uvicorn main:app 或 python main.py)都会触发。

总结

启动脚本(如 main.py)被执行 → 执行脚本内的「导入语句」→ 执行脚本内的「顶层代码」→ 启动 FastAPI 应用(uvicorn 等部署时)→ 触发 lifespan(若配置)→ 等待用户请求 → 处理请求

补充

全局变量和lifespan的区别
核心区别:全局变量 vs lifespan
两者的本质差异在于「设计目的」和「执行特性」,具体对比如下:

对比维度 main.py 全局变量 lifespan 生命周期钩子
核心目的 存储「模块级共享数据」(如静态配置、常量),在模块加载时一次性初始化 管理「Web 应用生命周期」(启动时初始化资源、关闭时清理资源)
执行时机 Python 模块加载时执行(服务启动前),且仅执行 1 次 Web 服务启动 / 关闭时执行(服务启动后、关闭前),仅执行 1 次
支持操作类型 仅支持「同步操作」(如同步读文件、静态变量赋值)—— 若写异步代码会报错 支持「异步操作」(如异步读大文件、异步连接数据库)—— 适配 Web 异步场景
作用域 模块级(整个 main.py 及导入它的其他模块可访问) 应用级(仅作用于当前 FastAPI 应用实例,与应用生命周期绑定)
资源清理能力 无(全局变量创建后,除非手动删除,否则一直驻留内存,服务关闭时也无法主动清理) 有(yield 后代码在服务关闭时执行,可主动清理连接、释放资源)

关键场景:什么时候用全局变量?什么时候用 lifespan?

1. 用「全局变量」的场景
  • 存储静态、无 IO 依赖的配置(如超时时间、常量、固定枚举值);
  • 初始化同步、轻量的资源(如小体积的 JSON 配置文件,且服务启动前必须就绪)。
# 全局变量:静态配置(无 IO,轻量)
GLOBAL_CONST = {"MAX_RETRY": 3, "DEFAULT_NAME": "小明"}

# 全局变量:同步加载小文件(服务启动前必须就绪)
import json
with open("small_config.json", "r") as f:
    SMALL_CONFIG = json.load(f)  # 同步读小文件,模块加载时执行
 2.用「lifespan」的场景
  • 处理异步 IO 操作(如异步读大文件、异步连接数据库 / Redis);
  • 管理需要主动清理的资源(如数据库连接池、WebSocket 连接、临时文件);
  • 初始化依赖服务启动后才能获取的资源(如从配置中心拉取动态配置)。
@asynccontextmanager
async def lifespan(app: FastAPI):
    # 服务启动时:异步加载大文件(避免阻塞模块加载)
    import aiofiles
    async with aiofiles.open("large_data.json", "r") as f:
        app.state.large_data = json.loads(await f.read())  # 存入应用状态,全局可用
    
    # 服务启动后:资源就绪,等待服务运行
    yield
    
    # 服务关闭时:主动清理资源(如关闭数据库连接)
    await app.state.db_connection.close()

避坑提醒:不要混淆两者的「异步支持」

  • 全局变量不支持异步操作:如果在模块顶层写 await aiofiles.open(...),Python 会直接报错(因为 await 只能在异步函数内使用);
  • lifespan天然支持异步:作为异步上下文管理器(asynccontextmanager 装饰),内部可安全使用 await,避免阻塞事件循环。

总结

  1. 执行顺序main.py 全局变量(模块加载时)→ lifespan(服务启动时);
  2. 核心差异:全局变量是「静态配置容器」,lifespan 是「动态资源管家」;
  3. 选择原则:静态、轻量、同步的用全局变量;异步、需清理、依赖服务的用 lifespan。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐