在 Python 里处理文件路径,过去基本围绕 os.path 和字符串拼接展开:拼接要 join、取文件名要 basename、判断存在要 exists,写起来冗长,而且路径始终只是一个普通字符串,没有任何语义。从 Python 3.4 开始,标准库提供了 pathlib,把路径抽象成一个有属性和方法的 Path 对象,拼接、分解、读写、遍历都能挂在对象上调用。本文按“创建—分解—变换—读写—遍历—迁移”的顺序,把 pathlib 的核心用法和与 os.path 的对应关系整理成可直接套用的写法。
一、从字符串路径切换到 Path 对象
先看两种风格的差别,同样的“拼接路径—取文件名—判断存在—读内容”流程:
- import os
- from pathlib import Path
- # 旧方式:全是字符串操作
- path_str = os.path.join("folder", "subfolder", "file.txt")
- basename = os.path.basename(path_str)
- if os.path.exists(path_str):
- with open(path_str) as f:
- content = f.read()
- # pathlib 方式:面向对象
- path = Path("folder") / "subfolder" / "file.txt"
- basename = path.name
- if path.exists():
- content = path.read_text()
复制代码
这里体现了两点变化:路径拼接从函数调用变成运算符重载(/),读取文件从 open 上下文管理器变成 read_text() 方法。注意默认编码会跟随系统,中文文本建议显式传 encoding。
二、创建 Path 对象与 / 运算符
Path 构造函数接收字符串,相对路径和绝对路径都可以,Windows 的反斜杠路径用原始字符串即可:
- from pathlib import Path
- p1 = Path("folder/file.txt") # 相对路径
- p2 = Path("/home/user/documents") # 绝对路径(Linux)
- p3 = Path(r"C:\Users\name\docs") # 绝对路径(Windows)
- # 用 / 拼接,这是 pathlib 最常用的特性
- base = Path("project")
- src = base / "src" / "main.py" # project/src/main.py
- print(src)
- data_dir = Path("data")
- file_path = data_dir / "2024" / "report.json"
- print(file_path)
- # 常用入口
- cwd = Path.cwd() # 当前工作目录
- home = Path.home() # 用户主目录
- print(f"当前目录: {cwd}")
- print(f"主目录: {home}")
复制代码
需要强调的是 Path 对象是不可变的:/ 运算、with_name()、with_suffix() 都不会修改原对象,而是返回新的 Path。这一点对链式调用和函数传参很友好,不会因为某一步操作意外改掉上游持有的路径。
- p = Path("base")
- p2 = p / "sub" # 创建新 Path,p 不变
- p3 = p.with_name("new_base") # 创建新 Path,p 不变
- print(p) # base
- print(p2) # base/sub
- print(p3) # new_base
复制代码
三、路径属性与逐段分解
拿到一个 Path 后,文件名、后缀、父目录等信息都是属性,不需要再调函数解析:
- p = Path("/home/user/documents/report.txt")
- print(p) # /home/user/documents/report.txt
- print(p.name) # report.txt
- print(p.suffix) # .txt
- print(p.suffixes) # ['.txt']
- print(p.stem) # report
- print(p.parent) # /home/user/documents
- print(p.parent.parent) # /home/user
- print(p.parts) # ('/', 'home', 'user', 'documents', 'report.txt')
- print(p.anchor) # /(Linux)或 C:\(Windows)
- print(p.is_absolute()) # True
- print(Path("a/b").is_absolute()) # False
- print(p.is_relative_to("/home")) # True(Python 3.9+)
复制代码
suffixes 返回的是列表,用于处理 report.tar.gz 这类多重后缀;parent 可以连续取值,便于由文件定位到上级目录;anchor 表示路径的锚点,跨平台脚本用它判断是根路径还是盘符。is_relative_to() 是 Python 3.9 才加入的方法,低版本环境需注意兼容。
四、路径变换:改名、改后缀、相对化与解析
- p = Path("/home/user/documents/report.txt")
- print(p.with_name("summary.txt"))
- # /home/user/documents/summary.txt
- print(p.with_suffix(".csv"))
- # /home/user/documents/report.csv
- v2 = p.with_stem("report_v2") # Python 3.9+
- print(v2) # /home/user/documents/report_v2.txt
- base = Path("/home/user")
- print(p.relative_to(base)) # documents/report.txt
- abs_path = Path(".").resolve()
- print(f"绝对路径: {abs_path}")
复制代码
with_name 改文件名、with_suffix 改扩展名、with_stem 改主干名,三者都只返回新对象,适合批量生成输出文件路径。relative_to 用于把绝对路径转成相对基准目录的写法,但要求路径确实位于该基准之下。resolve 会把相对路径转成绝对路径并解析符号链接,这也是路径比较的前提:
- a = Path("/home/user/docs")
- b = Path("/home/user/docs/../docs").resolve()
- print(a == b) # 字符串层面比较为 False;两边都 resolve 后为 True
复制代码
Path 的比较按路径语义进行,但含 .. 或符号链接时仍需先 resolve 才能得到一致结果。
五、文件读写:一行搞定
text 与 bytes 的读写都直接封装在 Path 上,JSON 也可以借道 text 接口:
- from pathlib import Path
- import json
- p = Path("test_pathlib.txt")
- p.write_text("Hello, pathlib!\n这是第二行", encoding="utf-8")
- content = p.read_text(encoding="utf-8")
- print(content)
- # 二进制
- # p.write_bytes(b"binary data")
- # data = p.read_bytes()
- data = {"name": "张三", "age": 25}
- json_path = Path("data.json")
- json_path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
- loaded = json.loads(json_path.read_text(encoding="utf-8"))
- print(loaded)
复制代码
write_text / read_text 适合中小文件,大文件仍然要用 open 流式处理,避免一次性读入内存。写 JSON 时 ensure_ascii=False 保证中文不被转义,encoding="utf-8" 保证跨平台一致。
六、存在性判断、目录创建与删除
- print(p.exists())
- print(p.is_file())
- print(p.is_dir())
- print(p.stat().st_size) # 字节数
- new_dir = Path("new_pathlib_dir/sub")
- new_dir.mkdir(parents=True, exist_ok=True)
- new_dir.joinpath("test.txt").touch()
- p.unlink() # 删除文件
- json_path.unlink()
- new_dir.joinpath("test.txt").unlink()
- new_dir.rmdir() # 删除空目录
- new_dir.parent.rmdir()
复制代码
mkdir 的 parents=True 会自动补齐多级父目录,exist_ok=True 让“目录已存在”不再抛异常,这两个参数几乎是脚本初始化的标配。touch() 创建空文件或更新时间戳;unlink() 删文件、rmdir() 删空目录,非空目录会报错,删非空目录需自行递归处理。目录遍历用 iterdir,模式匹配用 glob / rglob:
- cwd = Path(".")
- py_files = list(cwd.glob("*.py"))
- print([f.name for f in py_files])
- all_py = list(cwd.rglob("*.py")) # 等价于 cwd.glob("**/*.py")
- print(f"递归找到 {len(all_py)} 个 Python 文件")
复制代码
glob 只扫当前层,rglob 递归所有子目录,直接返回 Path 对象,后续取 name、读内容都不必再拼字符串。
七、os.path 到 pathlib 对照表
迁移老代码时按下面的对应关系替换即可:
- # os.path.join(a, b) -> Path(a) / b
- # os.path.basename(p) -> Path(p).name
- # os.path.dirname(p) -> Path(p).parent
- # os.path.splitext(p)[1] -> Path(p).suffix
- # os.path.exists(p) -> Path(p).exists()
- # os.path.isdir(p) -> Path(p).is_dir()
- # os.path.isfile(p) -> Path(p).is_file()
- # os.path.abspath(p) -> Path(p).resolve()
- # os.path.getsize(p) -> Path(p).stat().st_size
- # os.listdir(p) -> Path(p).iterdir()
- # os.makedirs(p) -> Path(p).mkdir(parents=True)
- # open(p, "r") -> Path(p).open("r")
- # glob.glob("*.py") -> Path(".").glob("*.py")
复制代码
迁移时有两个细节值得留意:一是 os.listdir 返回字符串列表,iterdir 返回 Path 迭代器,遍历后取名的写法要相应调整;二是 os.path.abspath 只是补全为绝对路径,而 resolve 还会解析符号链接和 ..,两者行为并不完全等价,涉及软链的目录要确认结果是否符合预期。
八、使用建议
新项目可以直接以 pathlib 为主:用 / 拼接、用属性取文件名与后缀、用 read_text / write_text 读写、用 glob / rglob 检索、用 mkdir(parents=True, exist_ok=True) 建目录,代码量比 os.path 明显更短也更好读。维护老代码时不必全量重写,新增逻辑用 Path、与 os.path 交界处用 str(path) 转换即可平滑过渡。需要注意 with_stem、is_relative_to 属于 Python 3.9 起提供的接口,若项目需支持更低版本,应改用 with_name 组合 stem/suffix 或自行做前缀判断。 |