8.python中的模块与文件操作
本文将带你完整梳理 Python 工程化最核心的四大板块:模块与包、文件操作与 JSON、路径管理,以及异常处理。
第一部分:模块与包
在 Python 中,每一个 .py 文件就是一个单独的模块,而包含 __init__.py 文件的文件夹就是一个包。
1. 导入 (Import) 的四种方法
假设有一个 tools.py 文件,里面有一个 calculate() 函数。
import tools:最推荐的写法。调用时使用tools.calculate(),来源清晰明确。import tools as tl:起别名。适合模块名过长的情况(如import matplotlib.pyplot as plt)。from tools import calculate:直接导入特定函数,调用时直接写calculate()。注意:如果当前文件也有同名函数,会引发命名冲突。from tools import *:最不推荐的初学者陷阱。它会把目标模块里的所有内容倒进当前空间,极易导致代码污染和难以排查的 Bug。
2. 模块的“通行证”:if __name__ == "__main__":
几乎所有专业的 Python 脚本底部都有这一行。它的作用就是区分是否是当前这个模块在被运行,如果是的话才会执行这个语句下的代码,一般可以写点测试代码在这里面或者是控制执行一个main()方法中的流程。
每个模块都有一个内置属性 __name__:
- 当脚本被直接运行时,
__name__的值为"__main__"。 - 当脚本被导入 (import) 时,
__name__的值是模块名(即文件名)。
3. __init__.py:包的“总服务台”
在pycharm中,可以在一个文件夹下直接创建一个python软件包,_init_.py 就会出现在这个包文件加下。
在一个包文件夹中,__init__.py 扮演着接口控制器的角色:
-
白名单机制 (
__all__):在__init__.py中定义比如下面这个,在__init__中写下第一行代码,外部使用from package import *的时候 就只能导入func_a,func_b这两个模块。__all__ = ["func_a", "func_b"]` -
命名空间提升:通过在
__init__.py中提前执行from .sub_folder.tool import func,用户可以直接通过import package调用深层功能,无需关心复杂的内部层级。如下所示
#不写的时候 import package.sub_folder.tool package.sub_folder.tool.func() #包的内部层级暴露给了使用者,如果以后重构改变 sub_folder 或 tool 的位置,所有使用的地方都要改 #而如果写了,用户可以直接使用最顶层的包名来使用func函数 import package package.func() #或者 from package import func func()
4. 绝对导入和相对导入
- 绝对导入:以项目根目录为起点的完整路径(如
from my_app.models.user import User)。就像完整的 GPS 地址,永远准确。强烈建议全部使用绝对导入。 - 相对导入:以当前文件为起点的路径(如
from .utils import func)。就像邻里指路,极易引发ImportError: attempted relative import with no known parent package(特别是当你直接运行子文件时),因为有时别人运行这个模块的时候文件的路径变了。
第二部分:文件操作与 JSON
1. with 上下文管理器
open(‘文件路径’,‘权限’,encoding = ‘编码格式’)可以打开一个文件夹,之后就用他来读取文件中的内容
而使用 with open() 可以确保无论程序是否报错,文件最终都会被安全关闭释放。
with open("data.txt", "r", encoding="utf-8") as f:
content = f.read()
注意:只要处理包含中文的文本文件,务必加上 encoding="utf-8" 防止乱码。
2. 读写模式 (Mode) 与组合写法
r(只读):默认模式,文件必须存在。w(覆盖写):文件存在则清空重写,不存在则创建。a(追加写):在文件末尾追加内容。- 功能增强组合 (
+/b):r+:可读可写,不会清空文件。适合修改中间内容,但需要配合f.seek(位置的数字)拨动文件指针。如果没用f.seek的话会从最开始修改,覆盖后面的内容(比如原来的111222被覆盖后变成aaa222)w+:可读可写。注意陷阱:一打开就会立刻清空旧内容。a+:可读可写。写入操作永远在文件最末尾发生。感觉这个最常用……rb/wb:用于处理图片、音频等非文本(二进制)文件。
举几个例子
# 写模式 (w+):清空后写入新内容
with open("output.txt", "w+", encoding="utf-8") as f:
f.write("直接清空之前的内容\n")
# 追加模式 (a+):在文件末尾添加内容
with open("output.txt", "a", encoding="utf-8") as f:
f.write("这行会被追加到文件末尾,不会动之前的\n")
# 读写模式 (r+):先读后写,需要移动指针
with open("output.txt", "r+", encoding="utf-8") as f:
old = f.read() # 读取全部内容
f.seek(0) # 将指针移回开头
f.write("【新增开头】\n" + old) # 在开头插入文字
#感觉挺鸡肋
3. 数据交换通用语言:JSON (JavaScript Object Notation)
JSON 是目前互联网上最流行的数据交换格式。它之所以在 Python 中如此受欢迎,是因为它的结构与 Python 的字典 (dict) 和列表 (list) 几乎一模一样。
为了彻底掌握 JSON,我们首先要记住一个口诀:“S” 代表 String (字符串)。
(1) 四大核心方法:带 S 与不带 S 的区别
在 json 模块中,操作分为“面向文件”和“面向内存字符串”两类:
| 类别 | 方法 | 作用 | 场景 |
|---|---|---|---|
| 处理文件 (不带 S) | json.dump(data, f) | 将数据写入文件 | 保存游戏存档、配置文件 |
json.load(f) | 从文件读取数据 | 程序启动时加载用户设置 | |
| 处理字符串 (带 S) | json.dumps(data) | 将数据转成 JSON 字符串 | 向网页发送数据、打印调试 |
json.loads(str) | 将 JSON 字符串转回字典 | 解析从 API 获取的文本 |
(2) 详细参数解析
在调用 dump 或 dumps 时,为了让生成的 JSON 文件“既好看又好读”,有两个参数是必加的:
indent=4:让数据自动缩进 4 个空格。如果不加,所有数据会挤成一行,人类极难阅读。ensure_ascii=False:处理中文必加! 默认情况下,JSON 会将中文转义为\u6211这样的字符编码。加上这一句,中文才能正常显示。
代码示例:
import json
data = {"name": "张三", "role": "初学者", "score": 100}
# 存为文件
with open("user.json", "w", encoding="utf-8") as f:
json.dump(data, f, indent=4, ensure_ascii=False)
# 转为字符串
json_str = json.dumps(data, ensure_ascii=False)
print(json_str) # 输出: {"name": "张三", "role": "初学者", "score": 100}
(3) 对象存储陷阱 (The Object Trap)
这是最让初学者抓狂的地方:JSON 并不认识你自定义的类。
如果你定义了一个 User 类并试图直接 dump 它的实例,Python 会报错:TypeError: Object of type User is not JSON serializable。
- 原因:JSON 是一种标准协议,它只认识
dict,list,str,int,float,bool,None。它不知道如何把你的User对象拆解成文本。 - 解决方案:使用
obj.__dict__。这个内置属性会自动将对象的所有属性提取成一个字典。
代码示例:
class User:
def __init__(self, name, id):
self.name = name
self.id = id
u1 = User("Bob", 1)
# 错误做法:json.dump(u1, f) -> 报错
# 正确做法:
with open("user_data.json", "w") as f:
json.dump(u1.__dict__, f) # 存入的是 {'name': 'Bob', 'id': 1}
(4) 存储多个对象:为什么不能用 a+?
如果你想存 100 个用户,千万不要用 a+ 模式循环执行 json.dump。
- 错误现象:使用
a+连续写入两个对象,文件内容会变成:{"id": 1}{"id": 2}。 - 致命后果:这不是合法的 JSON 格式(缺少逗号,也没有外层中括号)。当你用
json.load()读取时,程序会直接崩溃。
正确的“多对象存储”逻辑:
你应该把所有对象放入一个列表中,然后整体写入。
users = [User("Alice", 1), User("Bob", 2), User("Cindy", 3)]
# 1. 序列化:将对象列表转为字典列表
data_list = [u.__dict__ for u in users]
# 2. 写入:一次性写入一个大列表
with open("all_users.json", "w") as f:
json.dump(data_list, f, indent=4)
# 3. 读取并“复活”:读回来的只是字典,需要手动转回对象
with open("all_users.json", "r") as f:
raw_data = json.load(f) # 此时 raw_data 是列表,里面装的是字典
# 实例化还原
reconstructed_users = [User(d['name'], d['id']) for d in raw_data]
核心总结
- 进出文件用
dump/load,处理字符串用dumps/loads。 - 存对象必用
__dict__,把对象“降级”为字典。 - 多对象必用
[]列表包裹,千万别用a+强行拼接。 - 读取回来的数据是**“死的” (字典)**,如果需要调用类方法,必须重新执行
User(...)实例化。
第三部分:路径处理
在管理系统文件时,我们经历了从“字符串拼接”到“对象化管理”的跨越。处理路径最核心的挑战在于:Windows 使用反斜杠 \,而 Mac 和 Linux 使用正斜杠 /。如果你的代码里写死了路径字符串,换个电脑可能就崩了。
1. 传统的 os 模块
在 Python 3.4 之前,os 及其子模块 os.path 是处理文件的唯一标准。它将路径视为字符串,通过各种函数对字符串进行切割、拼接和判断。
(1) 基础管理命令:你的“终端指令” Python 版
这些方法直接对应我们在命令行(CMD/Terminal)中常用的指令。
import os
# 1. 获取当前工作目录 (Equivalent to 'pwd')
cwd = os.getcwd()
print(f"当前 Python 运行的位置是: {cwd}")
# 2. 列出目录下的文件和文件夹 (Equivalent to 'ls' or 'dir')
# "." 代表当前目录,也可以传入绝对路径
items = os.listdir(".")
print(f"当前目录下有: {items}")
# 3. 递归创建文件夹
# mkdir 只能建一层;makedirs 可以直接建出 'a/b/c' 这种深层结构
if not os.path.exists("backup/2026/march"):
os.makedirs("backup/2026/march")
print("多级目录创建成功!")
# 4. 重命名与删除
# os.rename("old_name.txt", "new_name.txt")
# os.remove("temp.tmp") # 删除文件
# os.rmdir("empty_folder") # 只能删除空文件夹
(2) 路径拼接
绝对不要手动用加号拼接路径:path = "data/" + "user.json"。
因为 Windows 用反斜杠 \,而 Linux/Mac 用正斜杠 /。
必须使用 os.path.join():它会根据运行代码的系统,自动塞进正确的斜杠。
# 稳健的写法
dir_name = "configs"
file_name = "database.yaml"
full_path = os.path.join(dir_name, file_name)
print(full_path)
# Windows 输出: configs\database.yaml
# Mac/Linux 输出: configs/database.yaml
(3) 核心重难点:os.walk() 深度遍历
如果你要在一个巨大的文件夹(比如你的“我的文档”)里寻找所有的 .mp4 文件,os.walk() 是效率最高的工具。它会像剥洋葱一样,一层一层深入所有的子文件夹。
它的运行机制:
os.walk(path) 会返回一个生成器,每次循环它都会给你一个包含 3 个变量 的元组:
root(当前目录路径):当前正在扫描的这一层文件夹的路径。dirs(子文件夹列表):当前路径下所有的文件夹名字(列表)。files(文件列表):当前路径下所有的文件名字(列表)。
实战案例:全盘搜索特定文件并计算大小
假设我们要找当前目录下所有的 .txt 文件:
import os
search_dir = "." # 从当前目录开始找
for root, dirs, files in os.walk(search_dir):
# root: 字符串,当前文件夹路径
# dirs: 列表,当前文件夹下的子目录
# files: 列表,当前文件夹下的所有文件
print(f" 正在进入目录: {root}")
for filename in files:
if filename.endswith(".txt"):
# 必须把 root 和 filename 拼接起来,才是文件的完整路径
full_path = os.path.join(root, filename)
file_size = os.path.getsize(full_path) # 获取文件字节大小
print(f" 发现目标: {filename} ({file_size} bytes)")
- 全自动:它会自动进入子文件夹的子文件夹,直到穷尽所有角落。
- 分工明确:它通过三个变量帮你分好了类。如果你只想对文件夹操作,就遍历
dirs;如果只想处理文件,就遍历files。
(4) os.path 常用小工具
| 方法 | 作用 | 例子 |
|---|---|---|
os.path.abspath(path) | 获取绝对路径 | 把 test.txt 变成 C:\Users\Bob\test.txt |
os.path.split(path) | 拆分目录和文件名 | 把 /a/b.txt 拆成 ('/a', 'b.txt') |
os.path.exists(path) | 判断路径是否存在 | 返回 True 或 False |
os.path.isfile/isdir | 判断是文件还是文件夹 | 过滤掉文件夹,只处理文件时必用 |
2. pathlib 模块
pathlib 将路径从“死字符串”变成了“活对象”。它不仅更好看,而且功能更聚合。
(1) 魔法拼接符号 /
pathlib 重载了除法运算符,让拼接路径像在资源管理器里点鼠标一样直观:
from pathlib import Path
# 极其优雅的拼接方式
base_path = Path("home")
user_path = base_path / "bob" / "scripts" / "run.py"
print(user_path) # 输出会自动适配系统斜杠
(2) 路径属性拆解
假设我们有一个路径对象 p = Path("C:/Downloads/study.tar.gz"),你可以像剥洋葱一样获取它的信息:
| 属性 | 结果 | 说明 |
|---|---|---|
p.name | 'study.tar.gz' | 完整的文件名 |
p.stem | 'study.tar' | 剥掉最后一层后缀后的名字 |
p.suffix | '.gz' | 最后一个后缀 |
p.parent | Path('C:/Downloads') | 父级目录 |
p.exists() | True/False | 路径是否存在(方法) |
(3) 一气呵成的文件操作
pathlib 集成了简单的读写功能,小文件处理甚至不需要 open():
p = Path("hello.txt")
# 一行写入文本 (自动处理 open 和 close)
p.write_text("Hello Pathlib!", encoding="utf-8")
# 一行读取文本
content = p.read_text(encoding="utf-8")
# 快速改名并移动
# p.rename(p.parent / "new_name.txt")
3. os 和 pathlib 对比
我们要实现同一个功能:检查 data 文件夹是否存在,不存在就创建,然后拼接出内部 result.json 的绝对路径。
老派写法 (os):
import os
folder = "data"
if not os.path.exists(folder):
os.mkdir(folder)
file_path = os.path.abspath(os.path.join(folder, "result.json"))
现代写法 (pathlib):
from pathlib import Path
p = Path("data")
p.mkdir(exist_ok=True) # exist_ok=True 代表存在也不报错,一行顶三行
file_path = (p / "result.json").resolve() # resolve() 获取绝对路径
路径处理
- Windows 的“转义”:在 Windows 字符串路径里,
\会被误认为转义符(比如\n是换行)。- 对策:使用
r"C:\Users\Name"(原始字符串)或者直接用pathlib。
- 对策:使用
- 绝对路径 vs 相对路径:
./data是相对路径(相对于你运行代码时的位置)。C:/data是绝对路径。- 建议:在大型项目中,尽量先用
.resolve()或os.path.abspath()将路径转为绝对路径,避免因为运行位置不同而找不到文件。
- 删库跑路警告:
os.remove()和os.rmdir()是永久删除,不会经过回收站。在执行删除代码前,务必先print一下路径确认没搞错!
第四部分:异常处理
在进行文件读写和系统交互时,报错(如文件丢失、权限不足)是家常便饭。一套完整的异常处理逻辑包含 5 个关键动作:
def process_config_data(file_path):
try:
# 1. try :存放高风险操作
print(f"正在尝试打开文件: {file_path}")
f = open(file_path, 'r')
data = f.read().strip()
result = 100 / int(data)
except FileNotFoundError:
# 2. except :处理文件不存在的情况
print("错误:找不到指定的配置文件!")
except ZeroDivisionError:
# 针对不同类型的错误,可以有多个
print("错误:文件中的数字不能为零!")
except Exception as e:
# 捕获其他未知错误
print(f"发生了意外错误: {e}")
else:
# 3. else :只有 try 成功了才会运行
print(f"数据处理成功!计算结果为: {result}")
finally:
# 4. finally :无论成败,都要关门(释放资源)
if 'f' in locals() and not f.closed:
f.close()
print("文件句柄已安全关闭。")
# 5. raise :在业务逻辑处手动触发
def set_age(age):
if age < 0:
raise ValueError("年龄不能为负数!") # 哪怕语法没错,业务错了也要报警
return age
更多推荐


所有评论(0)