很多人在学 Python 的前半段,都是把所有代码写在一个文件里。这样做确实适合练习,但一旦逻辑稍微变多,就会立刻暴露出复用困难、引用混乱、职责不清的问题。
这一节我们会从 import 机制切入,讲模块、包、__init__.py、导入路径和目录组织的关系,帮助你从“会写脚本”真正过渡到“会组织一个 Python 项目”。
模块与包分别是什么?
在 Python 里,一个 .py 文件就是一个模块。只要你把代码写进文件里,这个文件就天然可以被别的地方导入。
例如:
# utils.py
def format_name(name):
return name.strip().title()另一个文件就可以这样用:
from utils import format_name这就是最小单位的代码复用。
而包,可以理解成“装着多个模块的目录”。当你的代码不再适合挤在一个文件里,而是需要分目录、分职责组织时,包就出现了。
所以可以把两者关系理解成:
- 模块是单个文件级别的组织;
- 包是目录级别的组织。
这一层理解非常关键,因为 Python 项目真正的可维护性,往往不是来自某个高级语法,而是来自你是否能把不同职责拆进合适的模块和包里。
import 的几种写法
import 的本质,其实就是“把另一个模块中的名字拿过来用”。但不同写法表达的语义并不完全一样。
最基础的方式是:
import math
print(math.sqrt(9))这种写法会把整个模块名引入当前命名空间,优点是来源很清楚,你一眼就知道 sqrt 来自 math。
也可以只导入模块中的某个名字:
from math import sqrt
print(sqrt(9))这会让调用更短,但名字来源也变得没那么显式。所以到底用哪种,通常要在“简洁”和“可读”之间做平衡。
绝对导入
绝对导入指的是从项目或包的根路径出发,写完整导入路径:
from app.services.user_service import create_user这种写法的好处是路径清晰、定位稳定,尤其在中大型项目里会更好维护。你看到一行导入语句,就能知道它大概来自哪个目录层级。
因此在工程实践里,绝对导入通常是更推荐的默认选择。
相对导入
相对导入则是以当前模块所在位置为基准:
from .utils import format_name
from ..models.user import User它适合包内部模块彼此引用,而且目录关系非常明确的场景。
不过,相对导入也更容易因为目录层级变化而变难读。特别是 ... 越来越多时,阅读者需要先在脑子里倒推路径,理解成本会上升。
所以经验上可以这样看:
- 包内部局部协作、路径很近时,相对导入可接受;
- 项目整体协作、模块层级较深时,优先绝对导入。
__init__.py 的作用
很多人第一次学包结构时,都会碰到 __init__.py。早期 Python 里,一个目录想被当成包,通常需要这个文件。现在即使有些场景可以省略它,很多工程项目里依然会保留。
它至少有几个现实作用:
- 明确告诉阅读者“这是一个包目录”;
- 可以在这里暴露包级别的公共接口;
- 可以放少量初始化逻辑,但通常不建议太重。
例如:
# app/services/__init__.py
from .user_service import create_user
from .email_service import send_email这样外部就可以直接写:
from app.services import create_user不过这里要注意,__init__.py 最好承担“导出接口”的职责,而不是塞很多复杂逻辑。否则一个简单导入动作就可能触发副作用,给调试和测试带来麻烦。
Python 如何查找模块?
理解 import 时,另一个很重要的问题是:Python 到底去哪里找这些模块?
简化来看,Python 会沿着自己的模块搜索路径去查找,也就是 sys.path 中那些目录。通常包括:
- 当前执行入口所在目录;
- 标准库目录;
- 当前虚拟环境安装的第三方包目录;
- 其他被显式加入的路径。
这也解释了为什么“同样的代码,在不同启动方式下导入结果不一样”。比如直接执行脚本和用 python -m 执行模块,搜索路径上下文可能就不同。
所以,当你遇到 ModuleNotFoundError 时,不要只盯着代码本身,还要反过来问:
- 我现在是从哪里启动程序的?
- 当前项目根目录有没有进入搜索路径?
- 导入路径写的是包视角,还是脚本视角?
很多导入问题,根本不是“模块不存在”,而是“执行入口和包结构没有对齐”。
如何组织一个可维护的包结构?
模块化的终点,不是把文件拆得越碎越好,而是让职责边界清楚、导入关系自然。
一个常见的坏味道是:
- 工具函数四处乱放;
- 模型、服务、脚本混在一起;
- 一个模块既负责数据读取,又负责业务处理,还顺手打印日志。
更合理的做法通常是按职责拆分,例如:
app/
api/
services/
models/
utils/
settings/这样的结构不是为了好看,而是为了让人快速回答这些问题:
- 路由入口在哪?
- 业务逻辑在哪?
- 数据结构定义在哪?
- 通用工具在哪?
另外还有一个很重要的原则:让依赖方向尽量单向。
比如:
api可以依赖services;services可以依赖models和utils;- 但不要让底层模块反过来依赖上层入口。
一旦依赖方向乱了,后面就很容易出现循环导入、职责交叉和难以测试的问题。
所以,模块化的真正目标不是“学会 import”,而是学会用目录和依赖关系表达职责。
总结与预告
这一节我们从 import 机制切入,建立了模块、包和目录结构之间的基本心智。能不能把代码组织清楚,往往决定了一个项目是能持续演进,还是很快被自己写乱。
接下来我们会转入面向对象部分,先从类、实例、属性和方法这些最核心的概念重新建立理解。