本文目录
项目从单个 main.py 长成一摞文件时,最先撞上的往往不是语法,而是 import 报错:ModuleNotFoundError: No module named 'utils'。解释器找模块有固定规则——文件名、目录层级、sys.path 里登记过的搜索路径。弄懂「模块 vs 包」和 import 的几种写法,比死记目录模板有用。
可以把 import 想成一张地图:每个 .py 是一个地点,包是带入口的文件夹,sys.path 是「允许搜索的街道列表」。名字解析失败,不是 Python 坏了,而是当前进程里这张地图没把你的文件画进去。
模块:一个 .py 文件就是一个模块
utils.py 被 import 时,模块名通常是 utils(不含 .py)。模块顶层定义的函数、类、变量,成为该模块命名空间里的名字:
# utils.py
def slugify(text: str) -> str:
return text.strip().lower().replace(" ", "-")
MAX_LEN: int = 80# main.py
import utils
print(utils.slugify("Hello World")) # hello-world
print(utils.MAX_LEN)模块在进程内只初始化一次:多个文件 import utils,共享同一个模块对象(单例缓存放在 sys.modules 里)。因此模块顶层的副作用代码(读配置、连数据库)会在第一次 import 时执行,后续 import 直接复用缓存。
import 的几种常见形式
import json # 绑定整个模块对象
from pathlib import Path # 只引入 Path 到当前命名空间
from collections import deque as dq # 别名,避免与本地名冲突
import utils.format as fmt # 子模块,要求 utils 是包from package import * 会把 __all__ 列出的名字导入当前命名空间,可读性差、易冲突,库代码里少见,应用脚本也不推荐。
相对 import 只在包内部使用,以 . 表示当前包、以 .. 表示上级包:
# myapp/services/report.py
from ..models.user import User # 上一级包的 models.user
from .export import render_csv # 同包下的 export 模块脚本直接 python report.py 时,相对 import 常失败——因为该文件不被视为包的一部分。包内模块应通过 python -m myapp.services.report 或从包外 import 来启动。
包:带 init 的目录
目录要成为 包,通常放 __init__.py(3.3+ 有隐式 namespace package,但显式 __init__.py 仍是项目里最清晰的做法)。包名对应目录名:
myapp/
__init__.py
models/
__init__.py
user.py
services/
__init__.py
auth.py
main.py# myapp/models/user.py
class User:
def __init__(self, name: str) -> None:
self.name = name# myapp/main.py
from myapp.models.user import User
u = User("Ada")__init__.py 在包被第一次 import 时执行,常用来聚合子模块的公开 API:
# myapp/models/__init__.py
from myapp.models.user import User
__all__ = ["User"]这样外部可以 from myapp.models import User,而不必记深层路径。__all__ 还约束 from myapp.models import * 导出哪些名字——日常仍建议显式 import。
sys.path:解释器去哪找模块
sys.path 是一个路径列表。import 时,解释器按顺序在这些目录里查找包或模块:
import sys
for entry in sys.path[:5]:
print(entry)默认包含:stdlib 的 zip/目录、site-packages(第三方与已安装包)、以及启动方式决定的「首项」——用 python path/to/main.py 时,常把 main.py 所在目录加进去;用 python -m myapp.main 时,常把当前工作目录或项目根加进去。同一份代码,换启动命令,path 首项可能变,这就是「IDE 能跑、命令行报错」的常见根因。
若源码在 src/myapp/ 布局,应让 src 或安装后的包根在 path 里:
# 开发期 editable 安装,稳定 import myapp
pip install -e .或临时设置环境变量 PYTHONPATH=src(团队项目更推荐可安装包,少依赖手工环境变量)。
循环 import 与延迟 import
A.py import B,B 又 import A,可能在模块体尚未执行完时访问未定义的名字。缓解手段:
- 把共享类型、常量抽到第三模块
common.py; - 把 import 挪进函数体内(延迟到调用时才加载);
- 重构层级,让依赖单向流动(models 不 import views)。
def build_report() -> str:
from myapp.services.export import render_csv # 延迟 import
return render_csv([])组织习惯
按职责拆文件:配置、模型、I/O、CLI 各成模块;文件名小写加下划线,与模块名一致。包内 import 优先绝对路径(from myapp.models import User),相对 import 留给包内部子模块互引。
读陌生仓库时,从入口(main.py、__main__.py、CLI 命令)沿 import 链往外画一张小地图:谁依赖谁、哪一层不该反向依赖上层。比从头扫 every .py 快得多,也更容易发现「本该是包却散成一堆平铺脚本」的结构债。
if __name__ == "__main__" 与可执行模块
脚本直接运行时,__name__ 为 "__main__";被 import 时为模块名。常用模式是把「仅命令行执行」的逻辑放进 guard:
# myapp/main.py
def main() -> None:
print("run app")
if __name__ == "__main__":
main()包目录下加 __main__.py 后,可以 python -m myapp 启动,而不必记 main.py 的路径——此时 myapp 作为包被运行,__main__.py 充当入口。
命名空间与 import * 的代价
每个模块有自己的全局命名空间 dict;import 把绑定放进当前模块的 dict。from foo import bar 只在当前模块增加名字 bar,不自动引入 foo 的其他属性——除非你再 import foo 或继续 from import。
import * 会批量复制名字,破坏「名字从哪来」的可追踪性,也易与本地变量冲突。库作者用 __all__ 声明公开面;应用代码仍应 from myapp.models import User 这种显式写法。
常见 import 报错对照
| 报错 / 现象 | 常见原因 |
|---|---|
ModuleNotFoundError | 目录不在 sys.path;拼写错;未安装包 |
ImportError: attempted relative import with no known parent package | 直接运行包内文件,未 -m |
循环 import 时属性为 None 或缺失 | 模块体执行顺序问题,需拆依赖或延迟 import |
| IDE 能跳转、终端不能跑 | 两套 python / PYTHONPATH 不一致 |
小结
模块是一个 .py 文件及其命名空间;包是带 __init__.py 的目录树。import 把模块或名字绑进当前命名空间;sys.path 决定搜索起点。工程上优先绝对 import、可安装布局、venv 内一致的启动方式(python -m ...)。地图画清楚,模块化才是减复杂度,而不是多出一层找不到的文件。
src 布局与测试里的 import
常见 src layout 把包放在 src/myapp/,测试在 tests/,根目录放配置。好处是:未安装时不容易「误 import 到源码旁一份同名模块」;安装后 import myapp 与发布行为一致。
project/
pyproject.toml
src/
myapp/
__init__.py
core.py
tests/
test_core.py测试文件里通常 from myapp.core import foo,前提是开发机已 pip install -e .。纯靠把 src 加进 PYTHONPATH 也能跑 pytest,但和新同事 onboarding 文档不一致时,又会出现「我这边能 import、你那边不能」的分叉。
pytest 发现测试模块时,当前目录与已安装包都会在 sys.path 里占一席。若根目录恰好有与标准库或第三方同名的 .py(比如手滑写了 json.py),会 shadow 真模块——报错往往离奇,排查时要记得「本地文件名也是 import 地图的一部分」。
拆分模块的尺度
单文件几百行尚可;上千行且职责混杂就该拆。经验信号:同一文件里既有 HTTP 路由又有 SQL 又有 PDF 导出——读者 import 时不知道「该从哪拿哪个概念」。拆成 models/、services/、api/ 后,每层 import 方向尽量单向:api → services → models,models 不反向 import api。
过碎也有成本:每个函数一个文件,import 链比业务还长。以「读一个功能要打开几个文件、名字是否自解释」为标尺,比数行数更有用。模块化服务于可读与可测,不是文件系统炫技。
重新导出与「公开面」
大包常在顶层 __init__.py 做 re-export,对外暴露稳定 API,内部模块随意重构:
# myapp/__init__.py
from myapp.models.user import User
from myapp.services.auth import login
__all__ = ["User", "login"]外部写 from myapp import User,不必知道 User 实际定义在 models/user.py。这是有意设计的抽象边界:文档与示例只提公开名,子模块路径可以随重构改变。维护者改内部路径时,只要 __init__.py 的 re-export 不变,下游 import 就不碎。
与之相对,深层 import(from myapp.models.user import User)在内部重构时更容易大面积改 call site。库作者倾向 re-export;应用内部模块之间可以用深层 import 换更明确的依赖图——团队自定规范即可,关键是全项目一致。
动手拆模块前,可以先画一张依赖箭头图:谁 import 谁、有没有环。环不一定立刻爆炸,但往往是循环 import 与测试难写的先兆。把「数据定义」放在内层、「I/O 与框架胶水」放在外层,是多数 Web 与 CLI 项目能直接套用的分层直觉。
python -m package.module 与直接 python package/module.py 的差别,本质是「这个文件在不在包的上下文里」。文档示例、CI 脚本、可安装 CLI 入口应统一一种启动方式,并在 README 写死。
模块化没有唯一标准目录树,但有可验证规则:import 能解析、依赖尽量单向、公开面稳定。遇到 ModuleNotFoundError 先查 sys.path 与启动命令,而不是盲目改包名。
__init__.py 可以是空文件,仅用来标记「这是包」;也可以承担聚合导出与包级初始化逻辑。无论哪种,包 与 模块 的区分在于目录层级与 import 路径,不在于文件行数多少。
sys.path 是理解 import 失败的钥匙:首项随启动方式变化,site-packages 总在列表里。把 path 打印出来对照目录结构,比反复改名更有效。团队协作时在 README 写明推荐的 python -m ... 启动命令,可减少「我这边能 import」类问题。
import 解析的是字符串路径,不是文件系统绝对路径——同名模块在不同目录只会加载先匹配到的那一个。import 语句本身不耗时,但错误的模块边界会在每次改功能时反复收税——这张地图值得在重构前花十分钟画好。