引言
这是第七章「文件与异常处理」的第 6 篇,也是”读写文件”这条主线的收尾。前 5 篇解决的是”文件内容怎么读写、路径怎么表示、出错怎么办”;本篇把镜头拉远,处理文件和目录之外的系统级操作:当前工作目录在哪里、怎么遍历整棵目录树、怎么批量创建和重命名目录、怎么读写环境变量。这些能力都集中在标准库的 os 模块里。
一个常见的困惑是:“路径操作用 pathlib 还是 os?“答案在第 4 篇已经铺垫了一半:路径本身用 pathlib,系统交互用 os。pathlib 的 Path 对象专注”描述一个路径”,而 os 模块提供的是”操作系统进程级操作”——切换目录、列出目录、创建目录、读写环境变量,这些会改变进程状态或读取进程信息的操作,正是 os 的领地。本篇将从五个方面展开:getcwd/chdir 的工作目录管理、listdir/walk 的目录遍历、mkdir/makedirs 与 rename/remove 的目录文件操作、os.environ 的环境变量读写、os.path 的字符串路径工具,最后给出 os 与 pathlib 的选型标准。前置知识:第 4 篇的 pathlib、第 5 篇的 OSError 家族(本篇所有操作失败都抛这一类异常)。
概念与原理
os 模块的定位:进程视角的操作系统接口
os(operating system)模块是 Python 与操作系统之间的进程级接口。它提供的函数不是”计算某个值”,而是”让操作系统帮我们做一件事”:改变工作目录、创建目录、删除文件、读取环境变量。理解这一点,就能明白它的两个设计特征。
第一个特征是全局状态。os.chdir() 改变的是整个进程的工作目录(cwd,current working directory),而不是某个对象的状态——它影响的是此后所有相对路径的解析基准。一个进程只有一个 cwd,os.getcwd() 读到的就是它。第二个特征是直接调用系统服务。os.listdir() 直接调用操作系统的目录枚举,os.mkdir() 直接创建目录,因此它们的行为与平台绑定:Windows 上 mkdir 的字符编码、权限模型、错误码都与 POSIX 有差异。这正是 pathlib 无法取代 os 的地方——pathlib 不提供任何”修改系统状态”的操作。
环境变量的本质:进程的私有副本
环境变量(environment variables)是操作系统向进程传递配置的经典机制。进程启动时,操作系统把一组 key=value 字符串拷贝给进程;os.environ 就是这个拷贝的字典视图。三个要点决定了它的行为边界:
其一,修改只影响当前进程。 在 Python 里 os.environ['X'] = '1' 改的是本进程的副本,不会写回系统配置,也不会影响父进程或兄弟进程;关闭 Python 后一切消失。其二,值是字符串。 环境变量没有类型概念,os.environ 里所有的 value 都是 str,存 True 需要自己转字符串,读出来再自己解析。其三,读写都有便捷形式。 os.environ.get('KEY', 默认值) 或 os.getenv('KEY') 读取不会因键不存在而抛 KeyError。Windows 上设置环境变量的常用场景包括 TZ(时区)、代理配置、路径注入等,脚本里读取它们启动时的值必须用 os.environ。
目录遍历的两种形态:listdir 的浅枚举与 walk 的深度优先
目录遍历有两个层面。os.listdir() 只枚举一层,返回名字列表,不区分文件与子目录——判断类型需要自己逐个 os.path.isdir(),而且顺序不保证(依赖文件系统实现),打印或比较前要先排序。os.walk() 则从根目录开始深度优先遍历整棵树:每次 yield 三元组 (dirpath, dirnames, filenames),dirnames 里的子目录会作为下一次遍历的起点(这正是深度优先展开的机制)。os.walk 内部正是用 listdir 递归实现的,它把”递归遍历目录树”这个常见需求封装成了生成器,配合 dirnames 的增删还能控制遍历范围(比如剪枝跳过某个子目录)。
操作与实现
工作目录:getcwd 与 chdir
os.getcwd() 返回当前工作目录,os.chdir(path) 切换工作目录。工作目录是进程级状态,切换会影响后续所有相对路径的解析——因此用完必须切回,否则程序后续的读写全部换了基准。一个容易忽略的 Windows 细节:不能删除”当前工作目录”所在的目录,实测如下:
import os, tempfile
old = os.getcwd() # 先记住原工作目录
print('启动时工作目录:', old)
with tempfile.TemporaryDirectory() as td:
os.chdir(td) # 切换到临时目录
os.mkdir('docs') # 建目录
os.mkdir('src')
open('README.md', 'w', encoding='utf-8').close()
print('chdir 后 cwd == 临时目录:', os.getcwd() == td)
print('listdir 当前目录:', sorted(os.listdir('.')))
print('listdir 指定路径:', sorted(os.listdir('docs')))
os.chdir(old) # 用完切回原目录,否则 Windows 无法删除临时目录
print('块结束后 cwd 已恢复:', os.getcwd() == old)
启动时工作目录: C:\AI博客\blog-app
chdir 后 cwd == 临时目录: True
listdir 当前目录: ['README.md', 'docs', 'src']
listdir 指定路径: []
块结束后 cwd 已恢复: True
listdir 输出的是名字列表;. 表示当前目录,传绝对路径也能用。sorted() 在这里不是装饰而是必需品——Windows 的 NTFS 返回顺序按实现决定,不排序的话输出会抖动。TemporaryDirectory 块结束时要自动清理目录,若当时 cwd 还留在临时目录里,Windows 会抛 PermissionError(winerror 32),这就是脚本里必须 os.chdir(old) 的原因(第 5 篇的”文件被占用”陷阱在目录上的翻版)。
递归遍历目录树:os.walk
os.walk 适合”统计项目里所有文件、批量重命名、递归搜索”等场景。它生成 (dirpath, dirnames, filenames) 三元组,dirpath 是当前目录的绝对/相对路径(与其传入参数一致),dirnames 和 filenames 是当前层的子目录名与文件名列表:
import os, tempfile
with tempfile.TemporaryDirectory() as td:
os.makedirs(os.path.join(td, 'docs', 'zh'))
os.makedirs(os.path.join(td, 'docs', 'en'))
os.makedirs(os.path.join(td, 'src'))
for rel in ('README.md', 'docs/index.md', 'docs/zh/guide.md', 'docs/en/api.md', 'src/main.py'):
p = os.path.join(td, rel)
with open(p, 'w', encoding='utf-8') as f:
f.write('')
for dirpath, dirnames, filenames in os.walk(td):
print(' 目录:', os.path.relpath(dirpath, td))
for name in sorted(filenames):
print(' 文件:', name)
目录: .
文件: README.md
目录: docs
文件: index.md
目录: docs\en
文件: api.md
目录: docs\zh
文件: guide.md
目录: src
文件: main.py
输出呈现了深度优先的访问顺序:先 docs,再进入 docs\en、docs\zh,最后回到 src。os.walk 默认先序遍历(topdown=True),dirnames 里出现的每个子目录都会依次被展开(你在循环里删改 dirnames 可以控制展开哪些子目录)。注意 dirnames 顺序同样不保证,需要稳定输出就自己排序。与第 4 篇 Path.rglob('*.py') 相比:os.walk 更底层、拿到的是完整路径三元组,适合需要”边遍历边处理目录本身”的场景;rglob 更简洁,适合”只要匹配文件”的场景。
创建与删除:mkdir、makedirs、rename、remove
目录和文件的生命周期管理由四个函数承担。os.mkdir 创建单层目录,os.makedirs 递归创建多层;os.rename 改名/移动,os.remove 删除文件(目录要用 os.rmdir/shutil.rmtree):
import os, tempfile
with tempfile.TemporaryDirectory() as td:
# makedirs 递归创建,exist_ok 让重复调用不报错
os.makedirs(os.path.join(td, '2026', '09'), exist_ok=True)
os.makedirs(os.path.join(td, '2026', '09'), exist_ok=True)
print('makedirs 重复创建 + exist_ok: OK')
# 普通 mkdir 遇到已存在目录会抛 FileExistsError
d = os.path.join(td, '2026')
try:
os.mkdir(d)
except FileExistsError as e:
print('mkdir 已存在:', type(e).__name__)
# mkdir 不递归:父目录不存在时会抛 FileNotFoundError
try:
os.mkdir(os.path.join(td, 'a', 'b'))
except FileNotFoundError as e:
print('mkdir 父目录缺失:', type(e).__name__)
# rename 与 remove
src = os.path.join(td, '2026', '09', 'log.txt')
open(src, 'w', encoding='utf-8').close()
dst = os.path.join(td, '2026', '09', 'log-renamed.txt')
os.rename(src, dst)
print('rename 后:', os.path.basename(dst), '存在 =', os.path.exists(dst))
os.remove(dst)
print('remove 后文件存在 =', os.path.exists(dst))
makedirs 重复创建 + exist_ok: OK
mkdir 已存在: FileExistsError
mkdir 父目录缺失: FileNotFoundError
rename 后: log-renamed.txt 存在 = True
remove 后文件存在 = False
三个行为要点与第 5 篇的 OSError 家族直接呼应:mkdir 遇已存在目录抛 FileExistsError(所以”保证目录存在就创建”必须用 makedirs(..., exist_ok=True));mkdir 不递归,父目录缺失抛 FileNotFoundError;rename 返回 None、不报”成功”信号,验证得靠自己 os.path.exists。remove 对目录会抛 PermissionError(Windows)或 IsADirectoryError(POSIX)——删目录记得用 os.rmdir(仅空目录)或 shutil.rmtree(递归删除,第 20 章《路径、系统与运维》有专场)。
环境变量读写:os.environ 与 os.getenv
环境变量是进程配置的入口:读取启动参数、临时配置、凭据路径都靠它。读写演示:
import os, tempfile
with tempfile.TemporaryDirectory() as td:
# 写入环境变量只影响当前进程
os.environ['TZ'] = 'Asia/Shanghai'
print('env 写入后:', os.environ['TZ'])
print('getenv 读取:', os.getenv('TZ'))
print('不存在的键默认值:', os.getenv('NO_SUCH_KEY', '默认值'))
# 遍历全部环境变量
names = sorted(k for k in os.environ if k.startswith('PATH') or k == 'TZ')
for k in names:
print(' ', k, '=', os.environ[k][:60])
del os.environ['TZ']
print('删除后:', os.getenv('TZ'))
env 写入后: Asia/Shanghai
getenv 读取: Asia/Shanghai
不存在的键默认值: 默认值
PATH = C:\Users\Administrator\.local\bin;C:\Users\Administrator\bin
PATHEXT = .COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.PY;.P
TZ = Asia/Shanghai
删除后: None
用法小结:读用 os.environ.get(key, default)(或 os.getenv,两者等价,get 的默认值参数同样支持),直接 os.environ[key] 访问不存在的键会抛 KeyError;写用 os.environ['KEY'] = value,删除用 del os.environ['KEY']。keys()/items() 可以遍历全部变量。工业界最常见的坑是把 os.environ 当持久配置——它是进程私有的,脚本退出即消失,真正的持久化配置(.env 文件、系统设置)是另一套机制,第 8 篇《实战:配置文件的读取、校验与默认值》会系统处理配置读取。
os.path:面向字符串的路径工具(与 pathlib 对应)
os.path 是 os 模块里的字符串路径工具箱。虽然第 4 篇推荐新代码用 pathlib,但 os.path 仍是量级巨大的存量代码的标配,且它有一项 pathlib 没有的能力:直接在字符串上做路径运算,需要与只接受字符串的第三方库/API 对接时非常方便:
import os, tempfile
p = os.path.join('docs', '2026', 'report.txt') # 用 os.sep 拼接
print('join:', p)
print('basename:', os.path.basename(p))
print('dirname:', os.path.dirname(p))
print('splitext:', os.path.splitext(p))
with tempfile.TemporaryDirectory() as td:
real = os.path.join(td, 'docs')
os.mkdir(real)
print('exists:', os.path.exists(real), '| isdir:', os.path.isdir(real), '| isfile:', os.path.isfile(real))
print('abspath:', os.path.abspath(p))
join: docs\2026\report.txt
basename: report.txt
dirname: docs\2026
splitext: ('docs\\2026\\report', '.txt')
exists: True | isdir: True | isfile: False
abspath: C:\AI博客\blog-app\docs\2026\report.txt
与第 4 篇的对照表:os.path.join ↔ / 运算符,os.path.basename ↔ .name,os.path.dirname ↔ .parent,os.path.splitext ↔ .stem/.suffix(splitext 一次返回两部分),os.path.exists/isdir/isfile ↔ 同名方法,os.path.abspath ↔ .absolute()(resolve 更重,会展开符号链接)。注意 basename 对结尾带分隔符的路径返回空字符串、splitext 只切最后一个点——这些边界行为与 pathlib 相同,属于”路径工具的共同约定”。
os 与 pathlib 的选型:现代代码的默认与例外
综合第 4 篇与本篇,选型标准可以收敛为一条主线:默认 pathlib,系统状态操作交给 os。
- 路径的描述与运算(拼接、取名字、遍历、读写文本):用
pathlib.Path,代码更短、类型更安全、跨平台更省心。 - 进程级系统操作(
chdir、getcwd、environ、walk、mkdir/rename):用os,因为这些能力pathlib没有(Path.mkdir存在,但chdir、环境变量是os独有的)。 - 与遗留代码/第三方库对接(它们只接受字符串路径):用
os.path或把Path转回字符串(str(p)或os.fspath(p))。
判断口诀:“这个操作改变的是进程状态还是描述一个路径?” 描述路径 → pathlib;改变进程/系统状态 → os。两者会在同一个函数里和谐共处(例如 os.path.join(os.getcwd(), str(p.name))),边界清晰即可。
易错点与陷阱
陷阱一:chdir 后忘记切回,Windows 删除目录失败
os.chdir(td) 之后不切回,TemporaryDirectory 清理时 Windows 会因目录是当前工作目录而抛 PermissionError(winerror 32),长期运行的服务还会让后续所有相对路径解析错误。规范写法是进入时保存 old = os.getcwd()、退出前 os.chdir(old),配合 try/finally 保证异常路径也能恢复;更稳妥的是减少 chdir 的使用——用绝对路径传参,就不需要切换工作目录。
陷阱二:mkdir 与 makedirs 混用,或漏掉 exist_ok
os.mkdir('a/b') 在 a 不存在时抛 FileNotFoundError,目录已存在时抛 FileExistsError——两个异常都来自 OSError 家族(第 5 篇)。想”保证目录树存在”必须用 os.makedirs(path, exist_ok=True);而 exist_ok=True 只能容忍”整条路径已存在”的完整情形,路径中间某层是文件时仍会报错并给出明确信息。注意 Path.mkdir 的默认行为相同(不递归、不 exist_ok),两个 API 要记同一套规则。
陷阱三:listdir/walk 的输出顺序不稳定
Windows 的文件系统枚举顺序没有跨版本保证,直接打印或处理顺序敏感逻辑(比如”按文件名顺序合并日志”)会得到随机的顺序。处理前一律 sorted(...):对 listdir 排名字,对 walk 的 filenames/dirnames 先排序再使用。同理,os.rename 在 Windows 上目标已存在时抛 FileExistsError,需要覆盖语义时用 os.replace(第 5 篇安全写入已用过)——这是文档明确标注的跨平台差异。
陷阱四:把 os.environ 当持久配置,或忘记环境变量是字符串
os.environ['DB_URL'] = '...' 只对当前进程生效,进程退出即丢失;而读 os.environ['KEY'] 会因键不存在抛 KeyError——统一用 .get(key, default) 规避。同时记住 value 全是字符串:os.environ['PORT'] = 8080 会抛 TypeError(期望 str),读出来做计算前记得 int(os.environ.get('PORT', '8080')) 这种显式转换。
小结
os 模块是 Python 与操作系统的进程级接口,与 pathlib 形成清晰分工:路径描述归 pathlib,系统状态操作归 os。本篇覆盖了工作目录(getcwd/chdir,注意用完切回)、目录遍历(listdir 浅枚举、walk 深度优先递归)、目录与文件生命周期(mkdir/makedirs/rename/remove,异常行为与第 5 篇的 OSError 家族一致)、环境变量(os.environ 是进程私有副本,读用 .get、写要字符串、退出即失)、字符串路径工具(os.path 的 join/basename/dirname/splitext,与 pathlib 方法一一对应并服务遗留代码)。文件读写的主线到此收束,第七章的后半程将把这一切组合起来:第 7 篇用 CSV/JSON 处理结构化文件,第 8 篇做配置文件的读取与校验,第 9 篇用增量读取实现日志监控。
练习与思考题
- 写一个函数
tree_stats(root: str) -> dict,用os.walk统计root下的目录总数、文件总数以及文件总大小(用os.path.getsize,注意异常处理),在临时目录里造一棵小树验证。 - 用
os.makedirs(path, exist_ok=True)实现”保证目录存在”的幂等函数ensure_dir(p);再对比os.mkdir的两种失败(父目录缺失、目录已存在)分别抛什么异常,用except OSError捕获并区分打印。 - 编写脚本:设置
os.environ['MY_FLAG'] = 'on',用os.getenv读取并打印;然后解释为什么另一个 Python 进程(重新启动解释器)读不到这个值。 - 思考题:把本篇的
os.walk遍历改写为pathlib.Path.rglob实现(第 4 篇),统计两种写法各自的代码量;再举一个rglob无法直接完成、必须用os.walk的场景(提示:需要在遍历中修改目录结构或控制剪枝)。