五分钟学会Python 模块和包、文件
前言
这个标题把"模块""包""文件"三个词并列在一起,容易让人以为它们是同一件事的三个叫法。实际情况是:模块(module)和包(package)是导入系统里的两个概念,文件(file)是操作系统里的东西,它们只在"模块通常对应一个 .py 文件"这一点上沾边。一个包是一整个目录,它不是一个文件;而 open() 打开的文本文件跟导入系统毫无关系。分不清这三者,就会写出"为什么 import 找不到我刚建的文件"这类问题。
另一个常见误解是"包里必须有一个 __init__.py,否则不叫包"。在 Python 3.3 之后这已经不成立了:PEP 420 引入了隐式命名空间包(namespace package),没有 __init__.py 的目录也可以被导入。当然,在需要执行包初始化代码时,__init__.py 仍然是首选写法。
本文用最短的篇幅讲清导入查找规则、__name__ 的作用、包的目录结构,以及模块与文件在真正读写文件时该怎么配合。
一、模块:一个可导入的命名空间
在 Python 里,任何一个 .py 文件都是一个模块。import math 会去查找 math.py 或内置模块,执行其中的顶层代码,并把结果绑定到当前命名空间的一个名字上。第一次导入会执行模块的顶层语句,之后再导入直接复用缓存(sys.modules),不会重复执行。
import 的查找顺序大致是:内置模块 → sys.path 里的目录。而 sys.path 的开头通常是当前脚本所在目录(或交互式解释器的当前工作目录),其后是环境变量 PYTHONPATH 里的条目,最后是安装目录下的 site-packages。这就是为什么"把 a.py 和 b.py 放在同一个文件夹、在 a.py 里 import b"能直接跑通——不是魔法,是脚本目录被放进了 sys.path。
# 适用于 Python 3.8+
# 文件名 myutil.py
def greet(name):
return f"hello, {name}"
if __name__ == "__main__":
# 只有直接运行本文件时执行,被 import 时不执行
print(greet("world"))__name__ 是模块的一个属性:当文件被直接运行时它是字符串 "__main__",被导入时它是模块名(如 "myutil")。所以那句判断既是"入口开关",也是"防止导入时执行测试代码"的护栏。
二、包:带层级的一整个目录
包是模块的容器,用点号分层。myapp.utils.text 意味着目录嵌套 myapp/utils/text.py。传统写法要求每层目录有 __init__.py;PEP 420(Python 3.3 起)之后可以省略,解释器会把这样的目录当作命名空间包。
myapp/
├── __init__.py # 可选(3.3+),可为空
├── main.py
└── utils/
├── __init__.py
└── text.py包内部的模块要用相对导入互相引用,比如在 myapp/main.py 里写 from .utils import text。相对导入的"点"表示当前包层级,. 是同级,.. 是上一级。注意相对导入只在包内有效:直接 python myapp/main.py 运行会因为没有父包上下文而报 ImportError: attempted relative import with no known parent package。正确的运行方式是 python -m myapp.main,用 -m 让解释器把 myapp 当作包来加载。
| 写法 | 含义 | 何时用 |
|---|
import a.b.c | 绑定名字 a,需用 a.b.c.x | 想保留完整层级 |
from a.b import c | 直接把 c 绑到当前命名空间 | 最常用,代码更短 |
from a.b.c import func | 只导入一个函数 | 明确依赖,但注意名字冲突 |
from . import c | 包内相对导入同级模块 | 包内部模块互相引用 |
import a.b.c as abc | 起别名 | 名字太长或避免冲突 |
三、文件:跟导入系统是两回事
"文件"指的是真实的磁盘 I/O。读写的入口是内置函数 open(file, mode='r', encoding=...)。这里有个跨平台的关键点:不显式传 encoding 时,open() 用的是平台默认编码——官方文档的原话是"默认编码依赖平台,即 locale.getencoding() 返回的值"。在中文 Windows 上这通常是 cp936(GBK 系),在 Linux/macOS 上通常是 UTF-8。同一份 UTF-8 文件在 Windows 上按 cp936 去解,就会乱码或抛 UnicodeDecodeError。
因此现行做法是永远显式写 encoding="utf-8"。Python 3.10 起可以用 -X warn_default_encoding 或 sys.flags.warn_default_encoding 让这种情况发出 EncodingWarning(PEP 597),这是一种排查手段;而 PEP 686 已决定从 Python 3.15 起把 UTF-8 模式设为默认。在那之前,显式写编码是最稳妥的写法。
# 适用于 Python 3.8+
from pathlib import Path
p = Path("notes.txt")
p.write_text("第一行\n第二行\n", encoding="utf-8") # 写
content = p.read_text(encoding="utf-8") # 读
print(content, end="")
with p.open("a", encoding="utf-8") as f: # 追加
f.write("第三行\n")write_text / read_text 是 pathlib 提供的便捷方法,签名是 read_text(encoding=None, errors=None, newline=None) 与 write_text(data, encoding=None, errors=None, newline=None),底层就是 open(),所以同样需要显式传编码。
四、把三者串起来:导入自己的文件处理模块
真正写项目时,你往往会把文件处理逻辑放进一个模块,再由主程序导入。一个可运行的最小示例:
# 适用于 Python 3.8+
# 文件名 filetools.py
from pathlib import Path
def count_lines(path):
"""统计文本文件行数,显式指定编码避免跨平台乱码。"""
p = Path(path)
with p.open("r", encoding="utf-8") as f:
return sum(1 for _ in f)
if __name__ == "__main__":
print(count_lines("filetools.py"))在另一个文件 run.py 里(与 filetools.py 同目录):
# 适用于 Python 3.8+
import filetools
print(filetools.count_lines("filetools.py"))因为 run.py 所在目录在 sys.path 里,import filetools 能直接找到同目录的模块。这也解释了为什么换到别的目录运行会报 ModuleNotFoundError——sys.path 的第一项变了。
常见坑点
- ❌ 用文件名当模块名,命名出
json.py、random.py
✅ 这会把标准库模块"顶替"掉,import json 导入的是你自己的文件,引发一连串莫名其妙的错误;换名字
- ❌ 直接
python myapp/main.py却在main.py里写了from . import x
✅ 相对导入需要包上下文,改用 python -m myapp.main 运行
- ❌
open("data.txt")不写encoding,在 Windows 上读 UTF-8 文件
✅ 显式写成 open("data.txt", encoding="utf-8"),因为默认编码是平台相关的
- ❌ 以为包必须带
__init__.py,没有就不叫包
✅ PEP 420 起(Python 3.3+)目录可作隐式命名空间包;需要初始化代码时才加 __init__.py
- ❌ 在模块顶层写耗时逻辑或
print
✅ 包在 if __name__ == "__main__": 里,否则每次 import 都会执行一遍
- ❌ 两个模块互相
import,形成循环导入
✅ 把公共部分抽到第三个模块,或把导入移到函数内部(局部导入)推迟到调用时
- ❌
from mymodule import *图省事
✅ 这会污染命名空间、覆盖同名变量;显式列出需要的名字,或用 __all__ 控制导出
- ❌ 把"包"和"文件夹"混为一谈,随手把数据文件也塞进包目录
✅ 包是给导入系统用的目录结构,数据文件应放在明确的资源目录并用路径读取
总结
| 概念 | 是什么 | 关键点 |
|---|
| 模块 module | 一个 .py 文件 | 顶层代码只执行一次,结果缓存在 sys.modules |
| 包 package | 一个目录 | 3.3+ 可无 __init__.py;包内用相对导入 |
| 文件 file | 磁盘上的字节 | open() 不写 encoding 用平台默认编码,务必显式指定 |
把"模块是导入单元、包是目录、文件是 I/O 对象"这三条分清,"导入找不到模块"和"文件读出来乱码"这两类高频问题就都有了明确的排查方向:前者查 sys.path 和运行方式,后者查 encoding 参数。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)