《Python AI 应用开发入门》第 3.3 节。
理解 import,将单文件程序拆分为模块和包,并建立稳定的启动入口。
本节目标
学完本节后,你应当能够:
- 4. 使用
__name__ == "__main__" 保护入口代码。 - 7. 使用
python -m 从稳定入口启动程序。
1. 什么是模块
一个 .py 文件就是一个 Python 模块。例如:
prompts.py
其中定义:
def build_prompt(topic: str) -> str:
return f"请解释:{topic}"
另一个文件可以导入:
import prompts
prompt = prompts.build_prompt("模块")
print(prompt)
使用 模块名.成员名 能清楚看出函数来自哪里。
2. 为什么拆分模块
前一章的聊天程序可能同时负责:
全部放在 main.py 中会越来越难维护。可以按职责拆分:
chat_app/
├── main.py # 程序入口和菜单
├── prompts.py # Prompt 构造
├── storage.py # 聊天历史读写
└── config.py # 配置读取和验证
拆分标准不是“每个函数一个文件”,而是把高内聚、同一职责的功能放在一起。
3. 常见导入形式
导入模块
import prompts
print(prompts.build_prompt("函数"))
优点是来源明确。
导入指定成员
from prompts import build_prompt
print(build_prompt("函数"))
适合频繁使用且名称不冲突的成员。
使用别名
import json as json_module
标准库中最常见的别名示例是:
from pathlib import Path
别名应提高可读性,不要创建难懂缩写。
避免星号导入
# from prompts import *
它会把多个名字直接放入当前模块,来源不清楚,也更容易覆盖已有名字。
4. 导入时会执行模块
第一次导入模块时,Python 会从上到下执行模块顶层代码:
# prompts.py
print("prompts 模块正在加载")
def build_prompt(topic: str) -> str:
return f"请解释:{topic}"
导入它会产生输出:
import prompts
因此模块顶层适合放:
不适合在导入时:
这些导入副作用会让测试和复用变得困难。
5. name 与程序入口
5.1 name 是什么
Python 执行一个 .py 文件时,会先为它创建一个模块对象和一组全局名字。__name__ 就是 Python 自动放入模块全局作用域的特殊变量,用来表示这个模块当前使用的名称。
print(__name__)
名称前后各有两个下划线。此类名字常被称为“双下划线名字”或 dunder name。__name__ 由 Python 设置,程序通常只读取它,不需要手动赋值。
它的值取决于 Python 怎样加载这个文件,而不仅仅取决于文件名:
| __name__ |
|---|
python main.py | "__main__" |
import main | "main" |
import chat_app.main | "chat_app.main" |
python -m chat_app.main | "__main__" |
这里最重要的一点是:
"__main__" 不是某个固定文件的名称,而是 Python 赋给“本次程序入口模块”的特殊名称。
直接运行 main.py 时,这个文件就是程序入口,所以 __name__ 是 "__main__"。导入它时,它的身份是一个可复用模块,所以 __name__ 是导入使用的模块名。
5.2 用实验观察两种结果
新建 greeting.py:
# greeting.py
print(f"greeting.py 中的 __name__:{__name__}")
def main() -> None:
print("聊天程序启动")
if __name__ == "__main__":
main()
直接运行:
python greeting.py
输出:
greeting.py 中的 __name__:__main__
聊天程序启动
Python 把 greeting.py 作为程序入口,将 __name__ 设置为 "__main__",所以条件成立并调用 main()。
再新建 test_import.py:
# test_import.py
import greeting
print(f"导入后看到的模块名:{greeting.__name__}")
运行:
python test_import.py
输出:
greeting.py 中的 __name__:greeting
导入后看到的模块名:greeting
这次 greeting.py 以普通模块身份加载,__name__ 是 "greeting",所以不会调用 main()。不过,入口保护之外的 print(...) 仍属于顶层代码,导入时照常执行。
5.3 入口保护到底保护了什么
下面这段代码通常称为入口保护:
if __name__ == "__main__":
main()
它不是特殊语法,只是一个普通的 if 条件判断:
- • 当前文件作为程序入口运行时,条件成立,调用
main()。 - • 当前文件被其他模块导入时,条件不成立,不调用
main()。
这样,同一个文件既能直接运行,也能安全地被其他代码或测试导入,而不会在导入时意外启动交互菜单。
main 不是 Python 关键字,Python 不会因为函数叫 main() 就自动执行它。它只是常用的入口函数名,必须在条件成立时显式调用。
可以把整段机制概括为:
Python 用 __name__ 记录模块当前的加载身份;入口保护根据这个身份决定是否启动程序。
6. 什么是包
包是组织多个模块的目录。初学阶段建议放置 __init__.py:
stage2/
└── chat_app/
├── __init__.py
├── main.py
├── prompts.py
├── storage.py
└── config.py
init.py 的作用
__init__.py 是包的初始化文件。第一次执行 import chat_app 时,Python 会加载并执行 chat_app/__init__.py,然后再把 chat_app 作为包提供给程序使用。
这个文件可以完全为空。即使为空,它也能明确表示“这个目录是一个 Python 包”,并兼容更多编辑器和开发工具。
也可以只放少量与整个包有关的内容,例如包说明或版本号:
"""聊天应用包。"""
__version__ = "0.1.0"
不要在 __init__.py 中放交互菜单、网络请求或复杂业务逻辑,否则仅仅导入包就会产生意外行为。
现代 Python 还支持不含 __init__.py 的命名空间包,但这是更高级的组织方式。本课程中的普通项目统一保留这个文件。
术语要区分:
- • 第三方发行包:通过 pip 安装的项目,可能包含一个或多个导入包。
pip 安装名和 import 名不一定相同。
7. 包内导入
main.py 可以使用绝对导入:
from chat_app.prompts import build_prompt
from chat_app.storage import load_history, save_history
包内也可以使用相对导入:
from .prompts import build_prompt
from .storage import load_history, save_history
开头的点表示当前包。
建议:
8. 使用 python -m 启动
从 stage2 目录运行:
python -m chat_app.main
-m 表示按照模块名运行。Python 会把当前目录作为查找包的起点,并以包上下文加载 chat_app.main。
不要依赖:
python chat_app/main.py
直接按文件路径运行时,包内相对导入可能失败:
ImportError: attempted relative import with no known parent package
稳定做法是:
9. Python 按什么顺序查找模块
执行 import prompts 时,负责定位模块的是 Python 运行时的导入系统,不是编译器。找到 Python 源文件后,Python 才可能把它编译成字节码并执行。
下面先讨论 import prompts 这种顶层模块导入。
9.1 模块查找顺序
Python 大致按以下顺序处理:
- 1. 检查模块是否已经存在于
sys.modules。 - 2. 检查它是不是 Python 内置或冻结模块。
- 3. 按照
sys.path 中路径的先后顺序逐个查找。 - 4. 所有位置都找不到时,抛出
ModuleNotFoundError。
sys.modules 是已经加载模块的缓存。模块存在于其中时,Python 通常会直接复用,不再重新查找和执行该模块:
import sys
import json
print("json" in sys.modules) # True
如果缓存中没有,导入系统再查找内置模块和磁盘上的模块。查找磁盘模块时,sys.path 是最重要的路径列表。
9.2 sys.path 中有什么
sys.path 是一个有顺序的字符串列表。正常情况下,它通常包含:
- • 启动脚本所在目录;使用
python -m 或交互式解释器时,则与当前工作目录有关。 - •
PYTHONPATH 环境变量额外配置的目录(如果设置了)。 - • 当前解释器或虚拟环境的
site-packages 目录。
不同操作系统、Python 环境和启动方式产生的内容可能不同,因此最可靠的方法是直接打印:
import sys
for index, path in enumerate(sys.path):
print(index, repr(path))
使用 repr() 可以让空字符串显示为 ''。在某些启动方式中,'' 表示当前工作目录。
Python 会从索引 0 开始依次检查。查找 prompts 时,会在每个路径下寻找能够表示该模块的内容,例如:
prompts.py
prompts/
└── __init__.py
找到第一个匹配项后就停止继续查找。因此,sys.path 的顺序会直接影响最终导入哪个模块。
9.3 查看实际导入位置
可以通过模块的 __file__ 查看它实际来自哪个文件:
import json
import sys
print(f"当前解释器:{sys.executable}")
print(f"json 模块:{json.__file__}")
sys.executable 能确认正在使用哪个 Python 解释器,json.__file__ 能显示本次导入的 json 文件位置。少数内置模块没有对应的 .py 文件,因此不一定拥有 __file__。
如果项目目录中存在自己的 json.py,而项目目录排在标准库目录之前,import json 就可能先找到这个文件。这正是下一节“同名冲突”的原因:Python 不会根据你的意图选择模块,而是按照导入规则使用最先找到的匹配项。
不要通过随意调用 sys.path.append(...) 掩盖结构问题。遇到导入错误时,应先检查启动目录、包结构、当前解释器和依赖安装位置。
10. 避免同名冲突
不要把自己的文件命名为标准库或第三方包名称:
json.py
pathlib.py
typing.py
httpx.py
例如创建 json.py 后再写:
import json
Python 可能导入自己的文件而不是标准库,产生难以理解的错误。
还要删除重命名前生成的 __pycache__ 缓存,并重新启动解释器。
11. 循环导入
如果两个模块互相导入:
prompts.py → 导入 storage.py
storage.py → 导入 prompts.py
其中一个模块可能在尚未完成初始化时被另一个读取,导致部分成员不存在。
解决思路:
理想依赖方向:
main.py → prompts.py
main.py → storage.py
main.py → config.py
三个业务模块不需要互相导入。
12. 拆分聊天程序
prompts.py:
def build_prompt(topic: str, style: str = "简洁") -> str:
"""构造练习 Prompt。"""
cleaned_topic = topic.strip()
if not cleaned_topic:
raise ValueError("主题不能为空")
return f"请用{style}风格解释:{cleaned_topic}"
main.py:
from chat_app.prompts import build_prompt
def main() -> None:
topic = input("请输入主题:")
prompt = build_prompt(topic)
print(prompt)
if __name__ == "__main__":
main()
在 stage2 目录运行:
python -m chat_app.main
13. 导入问题排查顺序
遇到 ModuleNotFoundError 或 ImportError 时:
- 5. 使用
python -m chat_app.main。 - 8. 阅读完整 Traceback,确认实际导入了哪个文件。
动手练习
练习 1:创建包
创建 chat_app 包和 __init__.py,把 Prompt 函数移到 prompts.py,从 main.py 导入并调用。
练习 2:验证入口保护
分别:
记录两种情况下 __name__ 的值以及 main() 是否自动执行。
练习 3:制造并修复导入错误
尝试从错误目录运行 python -m chat_app.main,再回到包的父目录运行。记录错误、原因和修复方式。
随堂小测
- 2. 为什么
import module 的来源通常比星号导入清楚? - 4.
if __name__ == "__main__": 解决什么问题? - 6. 为什么推荐从包的父目录使用
python -m? - 7. 自己的文件为什么不应命名为
json.py?
参考答案
- 1. 模块是
.py 文件,包是组织多个可导入模块的目录。 - 2. 成员通过模块名访问,能看出来源并减少名称冲突。
- 3. Python 查找模块、创建模块对象并执行其顶层代码。
- 4. 让入口代码只在直接运行时执行,被导入时不启动。
- 5. 明确包边界,也可以控制包初始化和公开接口;本课程中可以为空。
- 6. 它建立正确包上下文,使绝对和相对导入更稳定。
- 7. 它可能遮蔽标准库
json,让导入指向错误文件。
本节完成检查
- • 我创建了包含
__init__.py 的 chat_app。 - • 我能使用
python -m chat_app.main 启动。