跳到主要内容

os 与 sys 模块

ossys 是 Python 与操作系统、解释器交互的两个核心模块。os 提供"与操作系统对话"的能力——访问环境变量、目录、文件权限、进程信息;sys 提供"与解释器对话"的能力——命令行参数、模块搜索路径、标准流、退出码等。本章将系统讲解这两个模块,并对比现代写法与传统写法的取舍。

:::info 与 pathlib 的分工 路径拼接、属性访问等"纯路径操作"现代项目首选 pathlib(见后续章节)。os 仍负责路径之外的部分:环境变量、进程、权限、文件描述符等。两者并不冲突,常配合使用。 :::

os.path 与 pathlib 对比

os.path 是早期字符串风格的路径工具,pathlib 是面向对象的现代替代。新代码应优先用 pathlib,但阅读老代码时仍需了解 os.path

import os
from pathlib import Path

target = "/home/tom/project/readme.md"

# os.path 写法:函数式,返回字符串
print(os.path.basename(target)) # readme.md
print(os.path.dirname(target)) # /home/tom/project
print(os.path.splitext(target)) # ('/home/tom/project/readme', '.md')
print(os.path.join("/a", "b", "c")) # /a/b/c
print(os.path.exists(target)) # False 或 True

# pathlib 写法:面向对象,返回 Path
p = Path(target)
print(p.name) # readme.md
print(p.parent) # /home/tom/project
print(p.suffix) # .md
print(Path("/a") / "b" / "c") # /a/b/c
print(p.exists()) # False 或 True

:::tip 何时仍需 os.path

  • 维护老代码或第三方库接口要求字符串路径
  • 需要 os.path.expandvars 展开 $VAR 这种环境变量替换
  • 与 C 扩展交互时字符串更直接

其余场景一律推荐 pathlib。 :::

环境变量:os.environ

os.environ 是一个类似字典的对象,保存当前进程的所有环境变量:

import os

# 读取
print(os.environ.get("HOME", "/tmp")) # /home/tom
print(os.environ["PATH"]) # 不存在会抛 KeyError

# 安全读取(推荐)
home = os.environ.get("HOME")
if home is not None:
print("用户主目录:", home)

# 设置(仅当前进程及其子进程可见)
os.environ["MY_APP_DEBUG"] = "1"
print(os.environ["MY_APP_DEBUG"]) # 1

# 删除
del os.environ["MY_APP_DEBUG"]

# 判断是否存在
print("HOME" in os.environ) # True

getenv 与 getenvb

import os

# getenv 是 environ.get 的便捷别名
lang = os.getenv("LANG", "en_US.UTF-8")
print(lang)

# getenvb 返回 bytes,处理二进制环境变量
path_bytes = os.getenvb(b"PATH")
print(type(path_bytes)) # <class 'bytes'> 或 None

:::warning environ 修改不会影响父进程 os.environ 的修改只对当前进程及其通过 subprocess 启动的子进程生效。Python 退出后,父 shell 的环境变量不会被改变——这由操作系统决定,并非 Python 的限制。 :::

工作目录:getcwd / chdir

import os

# 获取当前工作目录
cwd = os.getcwd()
print("当前目录:", cwd)

# 切换工作目录
os.chdir("/tmp")
print("切换后:", os.getcwd())

# 切回原目录
os.chdir(cwd)

:::warning 谨慎使用 chdir os.chdir 会影响进程全局状态,包括所有相对路径的解析。多线程程序中切换工作目录极易引发竞态条件,建议用绝对路径代替切换目录。 :::

目录列举:listdir 与 scandir

listdir:简单列表

import os

# 返回目录下条目名称(不含路径)
entries = os.listdir("/tmp")
print(f"共 {len(entries)} 个条目")
print(entries[:5])

scandir:高效迭代

scandir 返回 DirEntry 对象,迭代时一次性获取名称和类型信息,性能优于 listdir + stat

import os

with os.scandir("/tmp") as it:
for entry in it:
if entry.is_file():
print(f"文件:{entry.name},大小 {entry.stat().st_size}")
elif entry.is_dir():
print(f"目录:{entry.name}")
elif entry.is_symlink():
print(f"链接:{entry.name} -> {os.readlink(entry.path)}")

:::tip scandir 比 listdir 快 scandir 在迭代时由系统直接返回条目类型,is_dir() / is_file() 不必再发起 stat 系统调用。遍历大目录时性能可提升数倍,应优先使用。 :::

创建目录:makedirs

import os

# 单层目录
os.mkdir("single_dir")

# 递归创建(类似 mkdir -p)
os.makedirs("a/b/c/d", exist_ok=True)
# exist_ok=True:目录已存在时不抛异常(Python 3.2+)

# 设置权限模式
os.makedirs("secure_dir", mode=0o700, exist_ok=True)

:::note mode 与 umask mode 参数会被当前进程的 umask 修饰,实际权限为 mode & ~umask。要精确控制权限需先调用 os.umask(0)。 :::

执行外部命令:os.system vs subprocess

import os
import subprocess

# os.system:简单但功能有限,返回退出码
ret = os.system("echo hello")
print(f"退出码:{ret}") # 0

# subprocess.run:现代推荐写法
result = subprocess.run(
["echo", "hello"],
capture_output=True,
text=True,
check=True,
)
print("stdout:", result.stdout.strip()) # hello
print("returncode:", result.returncode) # 0

:::warning 永远不要用 os.system 处理用户输入 os.system 通过 shell 执行命令,存在命令注入风险:

# 危险!filename 来自用户输入
filename = "foo; rm -rf /"
os.system(f"cat {filename}") # 灾难

正确做法是用 subprocess.run([...], shell=False),参数以列表形式传递,不经过 shell 解析:

subprocess.run(["cat", filename], check=True)

os.system 仅适合写脚本快速验证,生产代码一律用 subprocess。 :::

捕获输出与错误

import subprocess

result = subprocess.run(
["ls", "/nonexistent"],
capture_output=True,
text=True,
)
print("returncode:", result.returncode) # 非 0
print("stderr:", result.stderr) # ls: cannot access ...

sys 模块基础

sys.argv:命令行参数

import sys

# argv[0] 是脚本名,argv[1:] 是真正的参数
print("脚本名:", sys.argv[0])
print("参数列表:", sys.argv[1:])

if len(sys.argv) < 2:
print("用法:python script.py <name>")
sys.exit(1)

name = sys.argv[1]
print(f"Hello, {name}!")

:::tip 复杂 CLI 用 argparse 超过两三个参数的命令行程序,建议直接用 argparse 或第三方 typer / click,能自动生成帮助、校验类型、支持子命令。 :::

sys.exit:退出程序

import sys

def main() -> None:
if not check_something():
print("检查失败", file=sys.stderr)
sys.exit(1) # 非 0 表示异常退出
sys.exit(0) # 0 表示正常退出

# 也可传字符串,会打印到 stderr 并以 1 退出
# sys.exit("错误信息")

sys.path:模块搜索路径

import sys

# 查看模块搜索路径
for i, p in enumerate(sys.path):
print(f"[{i}] {p}")

# 临时添加搜索路径
sys.path.insert(0, "/home/tom/my_libs")

# 查找已加载的模块
print("json" in sys.modules)

:::info sys.path 的构成 sys.path 一般包含:

  1. 脚本所在目录(或交互模式下的当前目录)
  2. PYTHONPATH 环境变量中的目录
  3. 安装依赖的 site-packages 目录
  4. 标准库目录

运行时修改 sys.path 只影响当前进程,永久添加路径应使用 pip installPYTHONPATH。 :::

标准流:stdin / stdout / stderr

import sys

# 写到标准错误
print("这是一条错误信息", file=sys.stderr)

# 从标准输入读取一行
# line = sys.stdin.readline()
# print("你输入了:", line.strip())

# 重定向输出
original = sys.stdout
with open("output.log", "w", encoding="utf-8") as f:
sys.stdout = f
print("这行会写入文件")
sys.stdout = original
print("这行会打印到屏幕")

:::warning 重定向 sys.stdout 的风险 直接替换 sys.stdout 容易在异常路径上忘记还原,导致后续输出全部丢失。更安全的方式是用 contextlib.redirect_stdout 上下文管理器,它在退出时自动还原。 :::

平台与版本判断

import os
import sys
import platform

# os.name:'posix'(Linux/macOS)或 'nt'(Windows)
print("os.name:", os.name)

# sys.platform:更具体的平台标识
print("sys.platform:", sys.platform)
# linux / darwin / win32 / cygwin

# 详细平台信息
print("platform.system():", platform.system()) # Linux / Darwin / Windows
print("platform.release():", platform.release()) # 内核版本
print("platform.machine():", platform.machine()) # x86_64 / arm64

# Python 版本
print("sys.version:", sys.version)
print("sys.version_info:", sys.version_info)
print("major.minor:", f"{sys.version_info.major}.{sys.version_info.minor}")

跨平台代码模式

import os
import sys

def default_config_dir() -> str:
"""返回跨平台的用户配置目录。"""
if sys.platform == "win32":
# Windows: %APPDATA%
base = os.environ.get("APPDATA", os.path.expanduser("~"))
return os.path.join(base, "MyApp")
if sys.platform == "darwin":
# macOS: ~/Library/Application Support
home = os.path.expanduser("~")
return os.path.join(home, "Library", "Application Support", "MyApp")
# Linux/Unix: 遵循 XDG 规范
xdg = os.environ.get("XDG_CONFIG_HOME")
if xdg:
return os.path.join(xdg, "myapp")
return os.path.join(os.path.expanduser("~"), ".config", "myapp")


print("配置目录:", default_config_dir())

常用 os 函数速查

import os

# 路径相关
os.getcwd() # 当前工作目录
os.chdir(path) # 切换目录
os.listdir(path) # 列举目录
os.scandir(path) # 高效迭代目录
os.makedirs(path, exist_ok=True) # 递归创建
os.mkdir(path) # 创建单层目录
os.rmdir(path) # 删除空目录
os.remove(path) # 删除文件
os.rename(src, dst) # 重命名/移动
os.replace(src, dst) # 原子重命名(跨平台)

# 环境与进程
os.environ # 环境变量字典
os.getenv(key, default) # 读取环境变量
os.getpid() # 当前进程 ID
os.getppid() # 父进程 ID
os.getuid() # 用户 ID(仅 Unix)
os.getlogin() # 登录用户名

# 文件描述符
os.open(path, flags) # 底层打开
os.read(fd, n) # 底层读
os.write(fd, data) # 底层写
os.close(fd) # 关闭

实战:跨平台项目清理脚本

下面实现一个跨平台、可配置的项目清理脚本,综合运用 ossysplatform,扫描并清理 Python 项目中的临时文件:

import os
import sys
import shutil
from pathlib import Path


# 跨平台识别需要清理的目录名
JUNK_DIRS = {"__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache"}
JUNK_EXTENSIONS = {".pyc", ".pyo", ".log"}
JUNK_FILES = {".DS_Store", "Thumbs.db"}


def human_size(num: int) -> str:
"""把字节数转换为人类可读的字符串。"""
for unit in ("B", "KB", "MB", "GB"):
if num < 1024:
return f"{num:.1f} {unit}"
num /= 1024
return f"{num:.1f} TB"


def scan_junk(root: Path) -> list[Path]:
"""递归扫描目录,找出所有垃圾文件和目录。"""
junk: list[Path] = []
for dirpath, dirnames, filenames in os.walk(root):
current = Path(dirpath)

# 收集垃圾目录(不进入它们内部继续扫描)
for d in list(dirnames):
if d in JUNK_DIRS:
junk.append(current / d)
dirnames.remove(d) # 阻止 os.walk 进一步遍历

# 收集垃圾文件
for f in filenames:
full = current / f
if f in JUNK_FILES or full.suffix in JUNK_EXTENSIONS:
junk.append(full)
return junk


def clean_project(root: str | Path, dry_run: bool = False) -> int:
"""清理项目中的临时文件,返回释放的字节数。"""
root = Path(root)
if not root.is_dir():
print(f"错误:{root} 不是目录", file=sys.stderr)
return 0

print(f"扫描目录:{root}")
print(f"平台:{sys.platform},dry_run={dry_run}")
print("-" * 50)

junk = scan_junk(root)
total_size = 0

for path in junk:
try:
if path.is_dir():
size = sum(f.stat().st_size for f in path.rglob("*") if f.is_file())
if dry_run:
print(f"[DRY] 目录 {path} ({human_size(size)})")
else:
shutil.rmtree(path)
print(f"[OK] 删除目录 {path} ({human_size(size)})")
total_size += size
else:
size = path.stat().st_size
if dry_run:
print(f"[DRY] 文件 {path} ({human_size(size)})")
else:
path.unlink(missing_ok=True)
print(f"[OK] 删除文件 {path} ({human_size(size)})")
total_size += size
except OSError as e:
print(f"[ERR] 跳过 {path}: {e}", file=sys.stderr)

print("-" * 50)
print(f"共处理 {len(junk)} 项,释放 {human_size(total_size)}")
return total_size


if __name__ == "__main__":
# 从命令行读取项目目录
if len(sys.argv) < 2:
target = "."
else:
target = sys.argv[1]

# 先试运行
clean_project(target, dry_run=True)
print()
# 实际执行
# clean_project(target, dry_run=False)

输出示例(在示例项目下运行):

扫描目录:.
平台:linux,dry_run=True
--------------------------------------------------
[DRY] 目录 ./__pycache__ (12.3 KB)
[DRY] 目录 ./subdir/__pycache__ (4.1 KB)
[DRY] 文件 ./app.log (256.0 B)
[DRY] 文件 ./Thumbs.db (0.0 B)
--------------------------------------------------
共处理 4 项,释放 16.7 KB

:::tip os.walk 与 Path.walk 的选择 Python 3.12+ 可以用 Path(root).walk() 代替 os.walk,返回的目录是 Path 对象更顺手。但 os.walk 修改 dirnames 阻止递归的特性在两套 API 中都支持,处理"剪枝"逻辑时仍然简洁。 :::

小结

  • 路径操作优先用 pathlibos.path 仅用于维护老代码或字符串接口。
  • os.environ 读写环境变量,修改只影响当前进程及子进程;用 getenv 提供默认值更安全。
  • os.getcwd / os.chdir 操作工作目录,多线程中慎用 chdir
  • 遍历大目录用 os.scandir,性能优于 listdir + stat;递归遍历用 os.walk 或 3.12+ 的 Path.walk
  • 执行外部命令一律用 subprocess.runos.system 有命令注入风险。
  • sys.argv 读取命令行参数;复杂 CLI 用 argparse
  • sys.path 控制模块搜索路径,sys.modules 查看已加载模块。
  • sys.stdin / stdout / stderr 是三个标准流,重定向时优先用 contextlib.redirect_*
  • os.name / sys.platform / platform.system 三者配合判断运行平台。

下一节将学习 datetime 模块,处理日期、时间、时区与时间戳。