前言
os模块是 Python 与操作系统打交道的入口:路径、目录、环境变量、进程信息、文件描述符,都从它这里走。它的特点是函数名短、参数少、但语义差异大——os.remove和os.rmdir只差几个字母,一个删文件一个删空目录,用错就是一句报错甚至误删。
几个常见误解:以为os.listdir()返回排序后的结果(不保证);以为os.walk()的dirnames可以直接拿来当完整路径用(不行,需要os.path.join);以为os.environ改了就写回系统了(只改当前进程)。
本文用「路径与环境 → 目录遍历 → 进程与系统信息」三个板块,把os模块最常用的部分配上可直接运行的示例讲清楚。示例面向 Python 3.8+。
一、路径、分隔符与环境变量
先说几个常量和函数,它们让代码能跨平台:
| 名称 | 含义 | 典型值 |
|---|
os.name | 操作系统大类 | 'posix'、'nt' |
os.sep | 路径分隔符 | '/'或'\\' |
os.pathsep | PATH 里的分隔符 | ':'或';' |
os.linesep | 行分隔符 | '\n'或'\r\n' |
os.getcwd()返回当前工作目录,os.chdir(path)切换:
# 适用于 Python 3.8+
import os
print(os.getcwd())
os.chdir("..") # 切到上级目录
print(os.getcwd())注意os.getcwd()拿到的和脚本文件所在目录不是一回事。从别处执行脚本时,工作目录是「你在哪敲的命令」,不是「脚本放在哪」。要拿脚本自身的目录,得用__file__推算。
环境变量通过os.environ这个映射对象访问:
# 适用于 Python 3.8+
import os
home = os.environ.get("HOME") or os.environ.get("USERPROFILE")
print(home)
os.environ["APP_MODE"] = "debug" # 只影响当前进程及其子进程os.environ[key]在键不存在时抛KeyError,所以一般用os.environ.get(key, default)或os.getenv(key, default)。两者差别不大:os.getenv就是os.environ.get的薄封装。
二、目录遍历:listdir、scandir、walk
os.listdir(path='.')返回目录下所有条目名的列表,不含'.'和'..',也不保证顺序。它返回的是名字,不是路径,要拼完整路径得自己os.path.join:
# 适用于 Python 3.8+
import os
for name in os.listdir("."):
full = os.path.join(".", name)
print(full, os.path.isdir(full))上面这段代码对每个条目都要额外调一次os.path.isdir,也就是一次系统调用。
os.scandir(path='.')返回的是os.DirEntry迭代器,每个entry自带name、path属性和is_file()/is_dir()/stat()方法,这些信息在扫描目录时往往一并拿到了,所以能少掉大量额外调用。它同时是上下文管理器:
# 适用于 Python 3.8+
import os
with os.scandir(".") as it:
for entry in it:
if entry.is_file() and not entry.name.startswith("."):
print(entry.name)官方文档特别提醒:os.DirEntry不适合长期保存,因为它的缓存会过期,需要最新信息时应改用os.stat(entry.path)。
os.walk(top, topdown=True, onerror=None, followlinks=False)是最常用的递归遍历工具。它对树里每个目录产出一个三元组(dirpath, dirnames, filenames):
# 适用于 Python 3.8+
import os
from os.path import join, getsize
for root, dirs, files in os.walk("project"):
print(root, "里", len(files), "个文件")
total = sum(getsize(join(root, name)) for name in files)
print(" 合计", total, "字节")
if "__pycache__" in dirs:
dirs.remove("__pycache__") # 原地裁剪,跳过该子树三件事必须记住:dirnames和filenames里都只是名字,不含路径;topdown=True时原地修改dirnames可以剪枝(topdown=False时改了没用);列表是否有序取决于文件系统,官方明确不保证。
三、进程与系统信息
os里还有一批读取进程/系统状态的小工具,写脚本时很有用:
# 适用于 Python 3.8+
import os
print("进程号", os.getpid())
print("父进程号", os.getppid())
print("CPU 核心数", os.cpu_count())os.cpu_count()返回逻辑 CPU 数量,在无法确定的平台上返回None,拿它做多进程进程池大小时要判空。CPU 密集任务用multiprocessing才能真正并行;os.cpu_count()常常就是进程数的起点。
创建目录用os.mkdir和os.makedirs:
# 适用于 Python 3.8+
import os
os.mkdir("out") # 只建一层,父目录不存在会失败
os.makedirs("a/b/c", exist_ok=True) # 逐级创建,exist_ok=True 时已存在不报错实战:统计目录树里各类文件的数量与体积
下面这段脚本把walk、路径判断和stat组合起来,输出扩展名分布:
# 适用于 Python 3.8+
import os
from collections import defaultdict
def summarize(top):
total = defaultdict(lambda: [0, 0]) # 扩展名 -> [个数, 字节数]
for root, dirs, files in os.walk(top):
dirs[:] = [d for d in dirs if not d.startswith(".")] # 剪掉隐藏目录
for name in files:
full = os.path.join(root, name)
ext = os.path.splitext(name)[1].lower() or "(无扩展名)"
try:
size = os.path.getsize(full)
except OSError:
continue # 断链、权限不足等,跳过
stat = total[ext]
stat[0] += 1
stat[1] += size
return total
if __name__ == "__main__":
for ext, (count, size) in sorted(summarize(".").items(),
key=lambda kv: -kv[1][1]):
print(f"{ext:<12} {count:>6} 个 {size:>12} 字节")这里用dirs[:] = [...]做切片赋值,是因为os.walk依赖原地修改dirnames列表对象来判断是否继续递归;直接dirs = [...]只是把局部名字重新绑定,walk看不到,剪枝就失效了。
常见坑点
1. 以为listdir返回有序结果
❌os.listdir(".")[0]当「第一个文件」用,不同文件系统结果不同。 ✅ 需要顺序就自己排:sorted(os.listdir("."))。
2. 把walk的filenames当完整路径
❌open(name)直接打开,报文件找不到。 ✅open(os.path.join(root, name))。
3. 剪枝时重新绑定而不是原地改
❌dirs = [d for d in dirs if ...],os.walk根本看不到。 ✅dirs[:] = [d for d in dirs if ...],切片赋值改的是同一个列表。
4.os.environ[key]取不存在的键
❌os.environ["PATHX"]抛KeyError。 ✅os.getenv("PATHX", "")或os.environ.get("PATHX", "")。
5. 把getcwd()当成脚本目录
❌ 用os.getcwd()拼资源路径,换个目录执行就找不到文件。 ✅ 用os.path.dirname(os.path.abspath(__file__))定位脚本自身目录。
6.os.makedirs不带exist_ok
❌ 重复执行脚本时FileExistsError直接中断。 ✅os.makedirs(path, exist_ok=True)。
7.os.cpu_count()返回None时直接用
❌Pool(os.cpu_count())在拿不到核心数的平台上会出错。 ✅os.cpu_count() or 1,给一个安全兜底。
8. 用os.remove删目录
❌os.remove("somedir"),抛IsADirectoryError(POSIX 上)或PermissionError(Windows 上)。 ✅ 删空目录用os.rmdir,删整树用shutil.rmtree。
总结
| 需求 | 函数 | 关键点 |
|---|
| 当前目录 | os.getcwd() | 是「执行位置」,不是脚本位置 |
| 列目录名 | os.listdir(path='.') | 无序、只有名字 |
| 带类型扫描 | os.scandir(path='.') | DirEntry,信息缓存会过期 |
| 递归遍历 | os.walk(top, ...) | dirnames原地改可剪枝 |
| 读环境变量 | os.getenv(key, default) | 不存在返回默认值 |
| 建目录 | os.makedirs(name, exist_ok=True) | 逐级创建 |
| 系统信息 | os.cpu_count() | 可能返回None |
os模块本身不难,难在「返回的是名字还是路径」「顺序有没有保证」「列表改的是对象还是名字」这三类细节上。把os.path.join用熟、把dirs[:]记住,绝大多数问题都能避开。