要搞清楚 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 触发) |
三、实际开发中的注意事项
“预加载配置” 适合在导入时执行
在 配置文件如config.py
中 属于 “服务启动时仅需执行一次” 的操作,放在config.py
的顶层代码中(导入时执行)是最优选择 —— 既无需依赖main
函数,也无需放在lifespan
中,且执行时机最早,后续main.py
或其他模块导入config
时可直接使用 AGENT_CONFIGS,无运行时阻塞风险。main
函数仅用于 “自定义启动逻辑”
不要把 “初始化配置”“连接数据库” 等逻辑放在main
函数中(除非你必须通过python main.py
启动且需要自定义参数),因为如果后续用uvicorn main:app
启动,main
函数不会被执行,可能导致初始化逻辑丢失。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 aiofilesasync 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
,避免阻塞事件循环。
总结
- 执行顺序:
main.py
全局变量(模块加载时)→lifespan
(服务启动时); - 核心差异:全局变量是「静态配置容器」,lifespan 是「动态资源管家」;
- 选择原则:静态、轻量、同步的用全局变量;异步、需清理、依赖服务的用 lifespan。