1. 概述

在 Python 中,模块通常就是一个以 .py 结尾的文件。文件里定义的变量、函数和类,都可以被其他 Python 文件导入和复用。

你可以先把模块理解成一个有名字的代码文件:greeting.py 是一个模块,main.py 也是一个模块。模块名通常就是文件名去掉 .py 后的部分。

把代码拆成多个模块,有三个直接好处:

  • 每个文件只负责一类事情,更容易阅读;
  • 一段逻辑写一次,就可以在多个地方复用;
  • 修改某个功能时,不需要在一个很长的文件里寻找所有代码。

Python 自带的标准库和通过 uv 安装的第三方库,本质上也都是可以被导入的模块或模块集合。我们在 FastAPI 项目中导入 FastAPI,在 LangChain 项目中导入 ChatOpenAI,使用的就是同一套模块机制。

2. 创建第一个模块

先在项目根目录创建一个 greeting.py,在里面定义一个问候函数:

greeting.py
1
def say_hello(name: str) -> str:
2
return f"你好,{name}!"

再创建 main.py,从 greeting 模块中导入这个函数:

main.py
1
from greeting import say_hello
2
3
message = say_hello("小微")
4
print(message)

在项目根目录运行:

code.ts
1
uv run python main.py

程序会输出:

code.ts
1
你好,小微!

这句 from greeting import say_hello 可以拆成两部分理解:greeting 是模块名,say_hello 是要从模块中取出的函数。导入之后,main.py 就可以像使用本地函数一样调用它。

3. 导入模块

我们来学习一下几种常见的导入模块的方式,第一种是导入整个模块

code.ts
1
import math
2
3
radius = 3
4
area = math.pi * radius ** 2
5
print(area)

使用 import math 时,math 是一个模块对象。调用模块中的内容时,要写成 math.pimath.sqrt(...) 这样的形式。这样写的好处是来源清楚:看到 math.sqrt 就知道这个函数来自 math 模块。

第二种是只导入需要的内容

code.ts
1
from math import pi, sqrt
2
3
radius = 3
4
area = pi * radius ** 2
5
distance = sqrt(16)
6
print(area, distance)

这种写法可以少写一个模块前缀,但阅读代码时需要记住 pisqrt 从哪里来。导入内容较少、来源很明确时可以使用。

第三种是使用别名

code.ts
1
import datetime as dt
2
3
today = dt.date.today()
4
print(today)

as 为模块取一个更短的别名。别名只在当前模块有效,不会修改原模块的名字。常见的第三方库也会使用约定俗成的别名,例如 import numpy as np

我们还可以直接使用 import * 导入模块中的所有内容

code.ts
1
from math import *

import * 会把模块中的许多名字直接放进当前文件。这样虽然少写了前缀,却会让人很难判断一个名字来自哪里,还可能覆盖当前文件中已有的变量。实际项目中,优先使用 import math 或明确列出 from math import sqrt

4. 导出

Python 没有强制使用的 export 关键字。模块文件顶层定义的函数、变量和类,默认都会放进这个模块的命名空间中,因此可以被其他代码显式导入

greeting.py
1
DEFAULT_GREETING = "你好"
2
3
def say_hello(name: str) -> str:
4
return f"{DEFAULT_GREETING}{name}!"
5
6
class Greeter:
7
def greet(self, name: str) -> str:
8
return say_hello(name)

上面的 DEFAULT_GREETINGsay_helloGreeter 都可以被导入:

main.py
1
from greeting import DEFAULT_GREETING, Greeter, say_hello
2
3
print(DEFAULT_GREETING)
4
print(say_hello("小微"))
5
print(Greeter().greet("小明"))

所以,通常不需要额外声明「哪些内容可以导出」。只要名称存在于模块的顶层命名空间,就可以通过 from module import name 这种方式导入。这里的「导出」更准确地说,是其他模块能够访问当前模块的公开名称。

用下划线表示内部实现

如果某个函数或变量只想作为模块内部的辅助实现,可以给它加一个前导下划线:

greeting.py
1
def _normalize_name(name: str) -> str:
2
return name.strip()
3
4
def say_hello(name: str) -> str:
5
return f"你好,{_normalize_name(name)}!"

前导下划线主要是一种约定,表示「这是内部使用的名称」。它不会真正阻止别人显式导入,下面的写法仍然可以工作:

code.ts
1
from greeting import _normalize_name

因此,下划线是可读性和协作约定,不是访问权限控制。

__all__ 控制通配导入

如果确实需要控制 from greeting import * 能够导入哪些名称,可以在模块中定义 __all__

greeting.py
1
__all__ = ["DEFAULT_GREETING", "say_hello"]
2
3
DEFAULT_GREETING = "你好"
4
5
def say_hello(name: str) -> str:
6
return f"{DEFAULT_GREETING}{name}!"
7
8
def _normalize_name(name: str) -> str:
9
return name.strip()

此时执行 from greeting import * 时,只会导入 __all__ 中列出的两个名称。__all__ 不会阻止显式导入,也不会提供真正的权限控制;它主要用于约定模块的公开 API。实际项目中仍然建议显式导入名称,而不是依赖通配导入。

5. 机制剖析

当 Python 第一次导入一个模块时,会执行这个模块的顶层代码,然后把模块对象放入当前进程的模块缓存中。后面再次导入同一个模块时,通常会复用已经加载的结果。

因此,模块的顶层位置适合放函数、类和常量定义,不适合放会在导入时自动执行的业务操作。例如,下面的打印语句在任何导入 greeting 的地方都会执行:

greeting.py
1
print("greeting 模块正在加载")
2
3
def say_hello(name: str) -> str:
4
return f"你好,{name}!"

更常见的做法是把可执行逻辑放进函数中,需要时再调用。这样导入模块只会准备代码,不会意外启动服务、发送请求或修改数据。

另外,同一个 Python 文件既可以被直接运行,也可以被其他文件导入。Python 会根据使用方式设置特殊变量 __name__,来区分这两种情况:

  • 直接执行 python main.py 时,__name__ 的值是 "__main__"
  • 被其他模块导入时,__name__ 的值是当前模块名,例如 "greeting"

利用这个差异,可以让一个模块同时支持「被导入」和「直接运行」两种方式:

greeting.py
1
def say_hello(name: str) -> str:
2
return f"你好,{name}!"
3
4
def main() -> None:
5
print(say_hello("小微"))
6
7
if __name__ == "__main__":
8
main()

当执行 uv run python greeting.py 时,main() 会被调用;当 main.py 导入 say_hello 时,main() 不会自动执行。这一层判断是 Python 模块中非常常见的结构。

6. 从模块到包

当项目中的模块越来越多时,可以把相关模块放进一个目录,这个目录就是包。对于初学者,可以先把包理解成「用目录组织起来的一组模块」

例如,我们可以把项目整理成下面这样:

code.ts
1
fastapi-langchain/
2
├── main.py
3
└── app/
4
├── __init__.py
5
├── model.py
6
└── prompts.py

__init__.py 可以是空文件,它的作用是明确告诉 Python:这个目录可以作为一个包来组织代码。现代 Python 也支持不放这个文件的命名空间包,但在学习和普通应用项目中保留它更直观。

假设 prompts.py 中定义了一个提示词:

app/prompts.py
1
SYSTEM_PROMPT = "你是一名温和、可靠的助手。"

在项目根目录的 main.py 中,可以使用包路径导入:

main.py
1
from app.prompts import SYSTEM_PROMPT
2
3
print(SYSTEM_PROMPT)

app.prompts 表示先找到 app 包,再找到其中的 prompts 模块。使用完整的包路径,能够让代码的来源更加清楚。

7. 在 FastAPI 和 LangChain 中拆分模块

前面文章里的 main.py 为了方便入门,把 FastAPI、模型和 Agent 都写在了同一个文件。项目继续增长后,可以按职责拆分。

__init__.py声明 app 为 Python 包
model.py创建和配置聊天模型
agent.py组装 LangChain Agent
main.pyFastAPI 服务入口
.env本地模型配置
pyproject.toml项目依赖和脚本配置
app/model.py
1
import os
2
from langchain_openai import ChatOpenAI
3
4
def create_model() -> ChatOpenAI:
5
return ChatOpenAI(
6
model=os.environ["DEEPSEEK_MODEL"],
7
api_key=os.environ["DEEPSEEK_API_KEY"],
8
base_url=os.environ.get("DEEPSEEK_BASE_URL", "https://api.deepseek.com"),
9
)

我们可以单独创建一个 Agent 模块

app/agent.py
1
from langchain.agents import create_agent
2
from .model import create_model
3
4
agent = create_agent(
5
model=create_model(),
6
system_prompt="你是一名温和、可靠的 Python 学习助手。",
7
)

这里的点号表示相对当前包导入。agent.pymodel.py 都在 app 包中,因此 from .model import create_model 可以找到同一个包里的模型模块。

我们再创建一个 FastAPI 入口模块

main.py
01
from fastapi import FastAPI
02
from pydantic import BaseModel
03
04
from app.agent import agent
05
06
app = FastAPI()
07
08
class ChatRequest(BaseModel):
09
question: str
10
11
@app.post("/chat")
12
async def chat(request: ChatRequest) -> dict[str, str]:
13
result = await agent.ainvoke({
14
"messages": [{"role": "user", "content": request.question}],
15
})
16
return {"answer": result["messages"][-1].content}

现在请求的入口只有 main.py,模型配置集中在 model.py,Agent 组装集中在 agent.py。模块之间的依赖方向也很清楚:入口依赖 Agent,Agent 依赖模型,而模型模块不需要反过来导入入口。

修改之后依然用如下的指令启动服务

macos.sh
1
uv run --env-file .env fastapi dev

然后用如下的命令发送请求验证一下

macos.sh
1
curl -X POST http://127.0.0.1:8000/chat \
2
-H "Content-Type: application/json" \
3
-d '{"question":"请用一句话介绍 LangChain。"}'
订阅后可阅读剩余内容
LangChain Python
已发布5计划发布50目标已完成10%