1. 概述
在 Python 中,模块通常就是一个以 .py 结尾的文件。文件里定义的变量、函数和类,都可以被其他 Python 文件导入和复用。
你可以先把模块理解成一个有名字的代码文件:greeting.py 是一个模块,main.py 也是一个模块。模块名通常就是文件名去掉 .py 后的部分。
把代码拆成多个模块,有三个直接好处:
- 每个文件只负责一类事情,更容易阅读;
- 一段逻辑写一次,就可以在多个地方复用;
- 修改某个功能时,不需要在一个很长的文件里寻找所有代码。
Python 自带的标准库和通过 uv 安装的第三方库,本质上也都是可以被导入的模块或模块集合。我们在 FastAPI 项目中导入 FastAPI,在 LangChain 项目中导入 ChatOpenAI,使用的就是同一套模块机制。
2. 创建第一个模块
先在项目根目录创建一个 greeting.py,在里面定义一个问候函数:
1def say_hello(name: str) -> str:2return f"你好,{name}!"
再创建 main.py,从 greeting 模块中导入这个函数:
1from greeting import say_hello23message = say_hello("小微")4print(message)
在项目根目录运行:
1uv run python main.py
程序会输出:
1你好,小微!
这句 from greeting import say_hello 可以拆成两部分理解:greeting 是模块名,say_hello 是要从模块中取出的函数。导入之后,main.py 就可以像使用本地函数一样调用它。
3. 导入模块
我们来学习一下几种常见的导入模块的方式,第一种是导入整个模块
1import math23radius = 34area = math.pi * radius ** 25print(area)
使用 import math 时,math 是一个模块对象。调用模块中的内容时,要写成 math.pi、math.sqrt(...) 这样的形式。这样写的好处是来源清楚:看到 math.sqrt 就知道这个函数来自 math 模块。
第二种是只导入需要的内容
1from math import pi, sqrt23radius = 34area = pi * radius ** 25distance = sqrt(16)6print(area, distance)
这种写法可以少写一个模块前缀,但阅读代码时需要记住 pi 和 sqrt 从哪里来。导入内容较少、来源很明确时可以使用。
第三种是使用别名
1import datetime as dt23today = dt.date.today()4print(today)
as 为模块取一个更短的别名。别名只在当前模块有效,不会修改原模块的名字。常见的第三方库也会使用约定俗成的别名,例如 import numpy as np。
我们还可以直接使用 import * 导入模块中的所有内容
1from math import *
import * 会把模块中的许多名字直接放进当前文件。这样虽然少写了前缀,却会让人很难判断一个名字来自哪里,还可能覆盖当前文件中已有的变量。实际项目中,优先使用 import math 或明确列出 from math import sqrt
4. 导出
Python 没有强制使用的 export 关键字。模块文件顶层定义的函数、变量和类,默认都会放进这个模块的命名空间中,因此可以被其他代码显式导入
1DEFAULT_GREETING = "你好"23def say_hello(name: str) -> str:4return f"{DEFAULT_GREETING},{name}!"56class Greeter:7def greet(self, name: str) -> str:8return say_hello(name)
上面的 DEFAULT_GREETING、say_hello 和 Greeter 都可以被导入:
1from greeting import DEFAULT_GREETING, Greeter, say_hello23print(DEFAULT_GREETING)4print(say_hello("小微"))5print(Greeter().greet("小明"))
所以,通常不需要额外声明「哪些内容可以导出」。只要名称存在于模块的顶层命名空间,就可以通过 from module import name 这种方式导入。这里的「导出」更准确地说,是其他模块能够访问当前模块的公开名称。
用下划线表示内部实现
如果某个函数或变量只想作为模块内部的辅助实现,可以给它加一个前导下划线:
1def _normalize_name(name: str) -> str:2return name.strip()34def say_hello(name: str) -> str:5return f"你好,{_normalize_name(name)}!"
前导下划线主要是一种约定,表示「这是内部使用的名称」。它不会真正阻止别人显式导入,下面的写法仍然可以工作:
1from greeting import _normalize_name
因此,下划线是可读性和协作约定,不是访问权限控制。
用 __all__ 控制通配导入
如果确实需要控制 from greeting import * 能够导入哪些名称,可以在模块中定义 __all__:
1__all__ = ["DEFAULT_GREETING", "say_hello"]23DEFAULT_GREETING = "你好"45def say_hello(name: str) -> str:6return f"{DEFAULT_GREETING},{name}!"78def _normalize_name(name: str) -> str:9return name.strip()
此时执行 from greeting import * 时,只会导入 __all__ 中列出的两个名称。__all__ 不会阻止显式导入,也不会提供真正的权限控制;它主要用于约定模块的公开 API。实际项目中仍然建议显式导入名称,而不是依赖通配导入。
5. 机制剖析
当 Python 第一次导入一个模块时,会执行这个模块的顶层代码,然后把模块对象放入当前进程的模块缓存中。后面再次导入同一个模块时,通常会复用已经加载的结果。
因此,模块的顶层位置适合放函数、类和常量定义,不适合放会在导入时自动执行的业务操作。例如,下面的打印语句在任何导入 greeting 的地方都会执行:
1print("greeting 模块正在加载")23def say_hello(name: str) -> str:4return f"你好,{name}!"
更常见的做法是把可执行逻辑放进函数中,需要时再调用。这样导入模块只会准备代码,不会意外启动服务、发送请求或修改数据。
另外,同一个 Python 文件既可以被直接运行,也可以被其他文件导入。Python 会根据使用方式设置特殊变量 __name__,来区分这两种情况:
- 直接执行
python main.py时,__name__的值是"__main__"; - 被其他模块导入时,
__name__的值是当前模块名,例如"greeting"。
利用这个差异,可以让一个模块同时支持「被导入」和「直接运行」两种方式:
1def say_hello(name: str) -> str:2return f"你好,{name}!"34def main() -> None:5print(say_hello("小微"))67if __name__ == "__main__":8main()
当执行 uv run python greeting.py 时,main() 会被调用;当 main.py 导入 say_hello 时,main() 不会自动执行。这一层判断是 Python 模块中非常常见的结构。
6. 从模块到包
当项目中的模块越来越多时,可以把相关模块放进一个目录,这个目录就是包。对于初学者,可以先把包理解成「用目录组织起来的一组模块」
例如,我们可以把项目整理成下面这样:
1fastapi-langchain/2├── main.py3└── app/4├── __init__.py5├── model.py6└── prompts.py
__init__.py 可以是空文件,它的作用是明确告诉 Python:这个目录可以作为一个包来组织代码。现代 Python 也支持不放这个文件的命名空间包,但在学习和普通应用项目中保留它更直观。
假设 prompts.py 中定义了一个提示词:
1SYSTEM_PROMPT = "你是一名温和、可靠的助手。"
在项目根目录的 main.py 中,可以使用包路径导入:
1from app.prompts import SYSTEM_PROMPT23print(SYSTEM_PROMPT)
app.prompts 表示先找到 app 包,再找到其中的 prompts 模块。使用完整的包路径,能够让代码的来源更加清楚。
7. 在 FastAPI 和 LangChain 中拆分模块
前面文章里的 main.py 为了方便入门,把 FastAPI、模型和 Agent 都写在了同一个文件。项目继续增长后,可以按职责拆分。
1import os2from langchain_openai import ChatOpenAI34def create_model() -> ChatOpenAI:5return ChatOpenAI(6model=os.environ["DEEPSEEK_MODEL"],7api_key=os.environ["DEEPSEEK_API_KEY"],8base_url=os.environ.get("DEEPSEEK_BASE_URL", "https://api.deepseek.com"),9)
我们可以单独创建一个 Agent 模块
1from langchain.agents import create_agent2from .model import create_model34agent = create_agent(5model=create_model(),6system_prompt="你是一名温和、可靠的 Python 学习助手。",7)
这里的点号表示相对当前包导入。agent.py 和 model.py 都在 app 包中,因此 from .model import create_model 可以找到同一个包里的模型模块。
我们再创建一个 FastAPI 入口模块
01from fastapi import FastAPI02from pydantic import BaseModel0304from app.agent import agent0506app = FastAPI()0708class ChatRequest(BaseModel):09question: str1011@app.post("/chat")12async def chat(request: ChatRequest) -> dict[str, str]:13result = await agent.ainvoke({14"messages": [{"role": "user", "content": request.question}],15})16return {"answer": result["messages"][-1].content}
现在请求的入口只有 main.py,模型配置集中在 model.py,Agent 组装集中在 agent.py。模块之间的依赖方向也很清楚:入口依赖 Agent,Agent 依赖模型,而模型模块不需要反过来导入入口。
修改之后依然用如下的指令启动服务
1uv run --env-file .env fastapi dev
然后用如下的命令发送请求验证一下
1curl -X POST http://127.0.0.1:8000/chat \2-H "Content-Type: application/json" \3-d '{"question":"请用一句话介绍 LangChain。"}'