Skip to content

Python pathlib 模块:面向对象的路径操作完全指南 ​

引言:从"拼字符串"到"操作对象"的进化 ​

在 Python 里处理文件路径,很多人的第一反应是 os.path:

python
import os

path = os.path.join("data", "images", "logo.png")   # 用函数拼接
name = os.path.basename(path)                         # 取文件名
ext = os.path.splitext(path)[1]                       # 取后缀

这段代码能跑,但有几个"别扭"的地方:

  1. 路径是"字符串",不是"对象"。你没法直接 path.name 拿文件名,得记住 basename、splitext 一堆函数名;
  2. 参数顺序容易搞混。os.path.join 还好,但 os.path.exists 和 os.path.isfile 谁是谁,os.makedirs 的 exist_ok 在哪个位置,经常要查文档;
  3. 跨平台分隔符问题。Windows 用 \,Linux/macOS 用 /,手写字符串拼接时 "a\\b\\c" 这种写法既丑又容易出错。

pathlib 正是为解决这些问题而生的。它在 Python 3.4(PEP 428)正式加入标准库,核心思想只有一句话——把路径从"字符串"升级成"对象"。从此你可以像操作一个对象一样,用 p.name、p.suffix、p / "子目录" 这样直观的方式处理路径。

用一个生活化类比来理解:

  • os.path 像"一堆散装工具":改锥、扳手、钳子都摊在工具箱里,每干一件事都要先想"用哪个工具、参数怎么传";
  • pathlib 像"一个瑞士军刀对象":路径本身就是一个对象,它自带各种"技能"(方法)和"属性",想做什么直接调用它自己就行,思路连贯。

再看一眼开头的例子,用 pathlib 写是什么感觉:

python
from pathlib import Path

p = Path("data") / "images" / "logo.png"   # 用 / 拼接,像拼路径一样自然
print(p.name)    # logo.png   —— 文件名,直接是属性
print(p.suffix)  # .png       —— 后缀,直接是属性

不用记函数名,代码读起来几乎就是"人话"。下面我们就从最基础的概念开始,把 pathlib 的全部关键知识点逐个吃透。


一、基础概念:认识 Path 类与它的"家族" ​

1.1 四个类:PurePath / Path / PosixPath / WindowsPath ​

pathlib 里其实有四个核心类,但日常你99% 只需要记住 Path 一个。先看它们的分工:

类名作用是否访问磁盘
PurePath纯路径,只做字符串层面的运算(拼接、拆解),不碰磁盘否
Path具体路径,在 PurePath 基础上增加真实的文件操作是
PosixPathPath 的 POSIX(Linux/macOS)版本,路径用 /是
WindowsPathPath 的 Windows 版本,路径用 \是

一句话记忆:PurePath 负责"算路径",Path 负责"真干活"。而 PosixPath、WindowsPath 是 Path 针对不同操作系统的具体实现。

1.2 Path 会自动"选边站" ​

最关键的一点:当你写 Path(...) 时,Python 会根据你运行程序的操作系统,自动选择返回 PosixPath 还是 WindowsPath。你在 Linux 上运行得到 PosixPath,在 Windows 上运行得到 WindowsPath,代码完全不用改——这就是 pathlib 跨平台能力的根基。

python
from pathlib import Path

p = Path("data/images/logo.png")
print(type(p))   # 在 Windows 上是 <class 'pathlib.WindowsPath'>
                 # 在 Linux/macOS 上是 <class 'pathlib.PosixPath'>

1.3 创建 Path 对象 ​

创建 Path 对象有三种常见姿势,任选其一:

python
from pathlib import Path

# 方式一:直接传入一个路径字符串
p1 = Path("data/images/logo.png")

# 方式二:用多段参数,Path 会自动用正确分隔符拼接
p2 = Path("data", "images", "logo.png")

# 方式三:链式 / 拼接(最常用、最优雅)
p3 = Path("data") / "images" / "logo.png"

print(p1 == p2 == p3)   # True   —— 三种写法得到的路径完全相同

小贴士:即使传入的字符串里混用了 / 和 \,Path 也会智能处理。但最推荐的还是用 / 运算符,因为它在所有平台都通用,且代码最像"路径本身"。


二、核心功能:Path 的 6 大基础操作 ​

2.1 路径拼接:/ 运算符与 joinpath() ​

pathlib 用 / 运算符做路径拼接,这是它最标志性的语法。它等价于 os.path.join,但可读性高出一截:

python
from pathlib import Path

base = Path("project")
full = base / "src" / "main.py"      # 链式拼接
print(full)                           # project\src\main.py(Windows)或 project/src/main.py(Linux)

# joinpath() 是 / 的方法版本,效果完全相同
full2 = base.joinpath("src", "main.py")
print(full == full2)                  # True

一个容易踩的坑:如果 / 右边是一个绝对路径,左边的路径会被"丢弃"(这和 os.path.join 行为一致):

python
p = Path("data") / "/etc/nginx.conf"
print(p)   # /etc/nginx.conf   —— 前面的 data 被忽略了!

2.2 属性获取:把路径"拆"成零件 ​

Path 对象内置了一组属性,能直接读出路径的各个部分,不用再背 basename、splitext:

python
from pathlib import Path

p = Path("/home/user/docs/report.final.pdf")

print(p.name)        # report.final.pdf   文件名(含后缀)
print(p.stem)        # report.final       文件名(去掉最后一个后缀)
print(p.suffix)      # .pdf               最后一个后缀(含点)
print(p.suffixes)    # ['.final', '.pdf'] 所有后缀列表
print(p.parent)      # /home/user/docs    父目录
print(p.parents[0])  # /home/user/docs    等价于 parent
print(p.parents[1])  # /home/user         上上级目录(可一直往上取)
print(p.parts)       # ('/', 'home', 'user', 'docs', 'report.final.pdf')  各段元组
print(p.anchor)      # /                  根/锚点

用生活化类比:Path 对象像一颗"洋葱",parts 是把它一层层剥开,parent 是剥掉最外层(文件名),stem/suffix 是再把文件名这颗"芯"拆成"主体 + 尾巴"。

2.3 文件 / 目录的创建与删除 ​

这一组方法对应 os 里的 makedirs、mkdir、remove、rmdir,但参数名更直观:

python
from pathlib import Path

# 1. 创建空文件(若已存在则静默跳过,不报错)
f = Path("notes.txt")
f.touch(exist_ok=True)                 # 创建成功;再次运行也不会报错

# 2. 创建目录(parents=True 相当于 os.makedirs,会自动建中间层)
d = Path("a/b/c")
d.mkdir(parents=True, exist_ok=True)   # 一次性创建 a/b/c 三层目录

# 3. 删除空目录
d.rmdir()                              # 只删 c,且 c 必须为空

# 4. 删除文件(missing_ok=True 表示文件不存在也不报错,Python 3.8+)
f.unlink(missing_ok=True)

核心对比记忆:touch = 建文件,mkdir = 建目录,unlink = 删文件,rmdir = 删空目录。exist_ok / missing_ok 这两个参数让代码幂等(重复执行不会崩),写自动化脚本时尤其省心。

2.4 文件读写:一行搞定读与写 ​

pathlib 把文件的"打开 → 读写 → 关闭"封装成了几个极简方法(Python 3.5 新增),日常小文件读写不必再写 with open(...):

python
from pathlib import Path

p = Path("hello.txt")

# 写文本(自动打开、写入、关闭;文件不存在会自动创建)
p.write_text("你好,pathlib!", encoding="utf-8")

# 读文本
content = p.read_text(encoding="utf-8")
print(content)    # 你好,pathlib!

# 读写字节(处理图片、音频等二进制文件)
img = Path("logo.png")
data = img.read_bytes()        # 读取全部字节
img.write_bytes(data)          # 原样写回

何时用 read_text,何时用 open? 一句话原则:一次性读写整个文件 → 用 read_text/write_text;需要逐行处理、追加写入、控制缓冲等 → 用 open()。Path.open() 返回的就是标准的文件对象,用法和内置 open 完全一致:

python
p = Path("log.txt")
with p.open("a", encoding="utf-8") as f:   # 'a' 表示追加模式
    f.write("追加一行\n")

2.5 路径匹配:glob / rglob / match ​

pathlib 自带通配符匹配,用来"按名字找文件":

python
from pathlib import Path

base = Path("project")

# glob:在当前目录下匹配(不递归子目录)
for py in base.glob("*.py"):        # 只匹配 project 直属的 .py 文件
    print(py)

# rglob:递归匹配所有子目录(等价于 glob("**/*.py"))
for py in base.rglob("*.py"):       # 匹配 project 下所有层级的 .py 文件
    print(py)

# match:判断"当前这个路径"是否符合某个模式(不遍历)
p = Path("project/src/main.py")
print(p.match("**/*.py"))    # True
print(p.match("*.txt"))      # False

记忆要点:glob 只扫一层,rglob 递归扫到底(r = recursive),match 是拿一个路径问"你符不符合这个规律"。

2.6 绝对路径转换:resolve / absolute / cwd / home ​

这一组是处理"相对路径 → 绝对路径"的高频工具:

python
from pathlib import Path

# cwd:当前工作目录(等价于 os.getcwd())
print(Path.cwd())     # 例如 D:\GITCODE\www.uiyin.com

# home:用户主目录(等价于 os.path.expanduser("~"))
print(Path.home())    # 例如 C:\Users\Administrator

# resolve:解析为"干净的绝对路径"——会消除 . 和 ..,并解析符号链接
p = Path("docs/../src/main.py")
print(p.resolve())    # 得到绝对路径,且 .. 已被消除

# absolute:仅把路径"补全"成绝对路径,不消除 ..、不解析符号链接
print(p.absolute())   # 保留 .. ,只是拼上当前工作目录

resolve 与 absolute 的区别是常见考点:

  • absolute():只做"机械拼接",把当前工作目录加到相对路径前面,不管 ..、不碰符号链接;
  • resolve():做"彻底解析",会消除 ..、解析符号链接,得到真正的最终路径(默认 strict=False,路径不存在也不报错)。

一句话记忆:absolute 是"问路"(告诉你大概在哪),resolve 是"导航"(带你走到最终目的地)。


三、高级功能:判断、符号链接、比较与跨平台 ​

3.1 路径类型判断 ​

判断一个路径"是什么、存不存在",pathlib 有一整套布尔方法:

python
from pathlib import Path

p = Path("data")

print(p.exists())       # 是否存在(文件或目录都算)
print(p.is_file())      # 是否是文件
print(p.is_dir())       # 是否是目录
print(p.is_absolute())  # 是否是绝对路径
print(p.is_symlink())   # 是否是符号链接

一个隐蔽的坑:exists() 会因权限不足等原因返回 False,而不是抛异常。所以"不存在"和"没权限访问"在它眼里都算 False,真正要区分时得结合异常处理。

3.2 符号链接处理 ​

符号链接(软链接)可以理解成Windows 的"快捷方式"或"指针"——它本身不是真实文件,只是"指向"另一个文件。pathlib 处理符号链接的方法如下:

python
from pathlib import Path

# 创建一个指向 target 的符号链接 link
target = Path("real.txt")
target.write_text("我是真实文件")
link = Path("link.txt")
link.symlink_to(target)          # link.txt -> real.txt

print(link.is_symlink())         # True   它是符号链接
print(link.readlink())           # real.txt  读取出它指向的目标

# 注意:is_file() 默认会"穿透"符号链接
print(link.is_file())            # True   —— 因为它指向的目标是文件
print(link.resolve())            # 解析后得到真实文件的绝对路径

坑位提醒:is_file()、is_dir()、exists() 默认都会跟随符号链接(穿透到目标)。如果只想判断"链接本身"是不是链接,用 is_symlink()。Python 3.12 还新增了 is_junction()(判断 Windows 的"目录联接")。

3.3 路径比较 ​

路径之间的比较有两种完全不同的语义,务必分清:

python
from pathlib import Path

p1 = Path("a/b/c.txt")
p2 = Path("a") / "b" / "c.txt"

# 1. == 比较的是"路径字符串"是否相同(不碰磁盘)
print(p1 == p2)            # True   —— 字符串层面相同

# 2. samefile() 比较的是"是否指向磁盘上同一个文件"(Python 3.5 新增)
a = Path("data.txt")
b = Path("data.txt")
print(a.samefile(b))       # True   —— 指向同一个真实文件

# 3. is_relative_to():判断一个路径是否"在另一个路径之下"(Python 3.9 新增)
p = Path("/home/user/docs/report.pdf")
print(p.is_relative_to("/home/user"))    # True
print(p.is_relative_to("/etc"))          # False

# 4. relative_to():计算相对路径
print(p.relative_to("/home/user"))       # docs\report.pdf

关键区分:== 是"字符串层面"的比较(a/b/../c 和 a/c 在 == 眼里不相等,因为字符串不同);samefile() 是"物理层面"的比较(只要最终指向同一个文件就是 True)。

3.4 跨平台路径兼容性处理 ​

跨平台适配是 pathlib 的杀手锏。核心思路有三点:

① 用 / 运算符,永远别手写 \ 或 '\\':

python
from pathlib import Path

# 跨平台安全写法:/ 会自动变成当前系统的分隔符
p = Path("data") / "images" / "logo.png"

② Path 自动选平台实现(前面 1.2 已讲),代码无需任何 if sys.platform 判断。

③ 需要"模拟/生成"另一种平台的路径时,用 PureWindowsPath / PurePosixPath:

python
from pathlib import PureWindowsPath, PurePosixPath

# 即使你在 Linux 上运行,也能构造、分析 Windows 路径
wp = PureWindowsPath("C:/Users/Admin/file.txt")
print(wp.parts)   # ('C:\\', 'Users', 'Admin', 'file.txt')

# 这在"跨平台单元测试"里特别有用:在 Linux 上验证 Windows 路径逻辑
pp = PurePosixPath("/usr/local/bin/python")
print(pp.name)    # python

一个经典对比:os 时代写跨平台代码,常常要 os.path.join("a", "b") 且提心吊胆;pathlib 时代直接 Path("a") / "b",系统自己搞定分隔符,这就是"面向对象"带来的确定性。


四、os 模块 vs pathlib 写法对照表 ​

有 os 基础的同学,用下面这张表快速切换思维:

需求os / os.path 写法pathlib 写法
路径拼接os.path.join("a", "b")Path("a") / "b"
取文件名os.path.basename(p)Path(p).name
取后缀os.path.splitext(p)[1]Path(p).suffix
取父目录os.path.dirname(p)Path(p).parent
判断是否存在os.path.exists(p)Path(p).exists()
判断是否文件os.path.isfile(p)Path(p).is_file()
创建多层目录os.makedirs(p, exist_ok=True)Path(p).mkdir(parents=True, exist_ok=True)
列出目录内容os.listdir(p)Path(p).iterdir()
遍历匹配文件glob.glob("**/*.py", recursive=True)Path(p).rglob("*.py")
读整个文件open(p).read()Path(p).read_text()
获取当前目录os.getcwd()Path.cwd()
获取绝对路径os.path.abspath(p)Path(p).resolve()
获取用户主目录os.path.expanduser("~")Path.home()

重要提示:虽然 pathlib 更现代,但不必激进地全面替换 os。一方面 os 依然是标准库基石、性能略优;另一方面从 Python 3.6 起,Path 实现了 os.PathLike 协议,绝大多数 os 函数和内置 open() 都能直接接受 Path 对象,所以两者可以无缝混用:

python
import os
from pathlib import Path

p = Path("data.txt")
print(os.path.exists(p))   # True   —— os 函数也能直接吃 Path 对象

五、一线开发高频实战场景 ​

5.1 批量处理指定格式的文件 ​

场景:把某个目录下所有 .jpg 图片批量重命名,加一个统一前缀。

python
from pathlib import Path

src_dir = Path("images")

# 遍历目录下所有 .jpg 文件(不递归子目录)
for img in src_dir.glob("*.jpg"):
    # with_stem 用于"只改文件名、保留后缀"(Python 3.9 新增)
    new_name = img.with_stem(f"preview_{img.stem}")
    img.rename(new_name)
    print(f"{img.name} -> {new_name.name}")

若需递归处理所有子目录的图片,把 src_dir.glob("*.jpg") 换成 src_dir.rglob("*.jpg") 即可。with_stem 比手动拼 str(p.parent / ("preview_" + p.stem + p.suffix)) 优雅得多,也不易出错。

5.2 项目资源路径的跨平台适配 ​

场景:项目中要读取一个固定的资源文件(如配置文件、字体),需要兼容 Windows 和 Linux 的部署环境。

python
from pathlib import Path

# 以"当前脚本所在目录"为基准定位资源,而不是依赖运行时的 cwd
BASE_DIR = Path(__file__).resolve().parent          # 脚本所在目录(绝对路径)

CONFIG = BASE_DIR / "config" / "app.ini"
FONT = BASE_DIR / "assets" / "fonts" / "default.ttf"

def load_config():
    # 无论部署到 Windows 还是 Linux,路径都能正确解析
    return CONFIG.read_text(encoding="utf-8")

print(load_config())

为什么这样写靠谱:Path(__file__).resolve().parent 锚定的是代码文件自身的位置,不随"用户在哪个目录运行程序"而变化,是资源定位的黄金写法。配合 / 运算符,彻底告别 "..\\assets\\fonts" 这类脆弱的相对路径。

5.3 临时文件路径管理 ​

场景:程序运行中需要临时文件/临时目录,用完自动清理,不污染项目。

python
import tempfile
from pathlib import Path

# 方式一:临时目录(with 结束后自动删除整个目录)
with tempfile.TemporaryDirectory() as tmp:
    tmp_path = Path(tmp)                        # 转成 Path 对象,享受面向对象 API
    tmp_file = tmp_path / "cache.txt"
    tmp_file.write_text("临时数据", encoding="utf-8")
    print(tmp_file.read_text())                 # 临时数据

# 退出 with 后,临时目录已被系统自动清理

# 方式二:临时文件(只返回文件对象,可用 .name 拿到路径)
with tempfile.NamedTemporaryFile(suffix=".log", delete=True) as f:
    print(Path(f.name).name)    # 形如 tmpXXXX.log

要点:pathlib 本身不生成临时路径,但它与标准库 tempfile 是黄金搭档——tempfile 负责"创建 + 自动清理",Path 负责"优雅地操作路径"。把临时目录 tmp 包一层 Path(tmp),后续代码就和普通目录操作一模一样。

5.4 遍历目录树筛选符合条件的文件 ​

场景:递归查找某个项目中所有"超过 100KB 的 .log 日志文件"。

python
from pathlib import Path

project = Path("project")

# 方式一:rglob 递归 + 条件过滤(兼容性最好,推荐日常使用)
big_logs = [
    f for f in project.rglob("*.log")
    if f.is_file() and f.stat().st_size > 100 * 1024
]

for f in big_logs:
    print(f"{f}  ->  {f.stat().st_size} 字节")

# 方式二:Python 3.12 新增的 Path.walk()(类似 os.walk,但更面向对象)
# for root, dirs, files in project.walk():
#     for name in files:
#         fp = root / name
#         if fp.suffix == ".log" and fp.stat().st_size > 100 * 1024:
#             print(fp)

性能小提示:rglob("*.log") 底层就是惰性生成器,配合列表推导式做条件过滤,不会一次性把所有文件读进内存,遍历超大目录树也不怕。stat().st_size 可拿到文件大小(字节),判断文件类型、大小、修改时间都靠 stat()。


六、知识点总结 ​

pathlib 的核心,可以浓缩成一张"能力地图":

能力维度关键 API一句话说明
创建路径Path(...)、Path.cwd()、Path.home()造一个路径对象
拼接路径/、joinpath()用运算符像拼路径一样拼
拆解路径name / stem / suffix / parent / parts / anchor把路径拆成零件(全是属性)
创建 / 删除touch / mkdir / unlink / rmdir带 exist_ok/missing_ok,幂等省心
读写文件read_text / write_text / read_bytes / write_bytes / open一行读、一行写
匹配文件glob / rglob / match按通配符找文件
绝对路径resolve / absolute相对路径转绝对,注意两者区别
类型判断exists / is_file / is_dir / is_absolute / is_symlink问"它是什么"
符号链接symlink_to / is_symlink / readlink处理"快捷方式/指针"
比较路径== / samefile / is_relative_to / relative_to字符串比较 vs 物理比较
跨平台Path 自动选型、PureWindowsPath、/ 运算符一次编写,处处运行

新增 API 引入版本速记:

  • Python 3.4:pathlib 正式入库(PEP 428),Path / PurePath 等基础类;
  • Python 3.5:read_text / write_text / read_bytes / write_bytes / touch / samefile;
  • Python 3.6:Path 实现 os.PathLike 协议,可被 os 函数和 open() 直接接受;
  • Python 3.8:unlink(missing_ok=True);
  • Python 3.9:is_relative_to / with_stem / readlink;
  • Python 3.10:hardlink_to(取代已弃用的 link_to);
  • Python 3.12:walk / is_junction。

一句核心心法:pathlib 把路径从"字符串"变成了"对象",让你的代码从"背一堆函数名"进化到"直觉式地操作路径"。能接受 Path 对象的地方,就大胆传 Path;需要更底层控制或极致性能时,再退回 os。


七、常见问题排查 ​

Q1:Path("a/b/c").mkdir() 报 FileNotFoundError? ​

原因:mkdir 默认 parents=False,如果父目录 a/b 不存在,就会报错。

解决:加 parents=True,让它像 os.makedirs 一样自动创建中间层目录。

python
Path("a/b/c").mkdir(parents=True, exist_ok=True)

Q2:rglob("*.py") 怎么一个文件都没找到? ​

排查顺序:

  1. 确认起始目录本身存在(Path("project").exists());
  2. 确认文件名大小写是否对得上(通配符在部分系统区分大小写);
  3. 确认 rglob 的 * 不跨目录,若要匹配任意层级需用 **/*.py(rglob 内部已帮你加了 **,所以 rglob("*.py") 等价 glob("**/*.py"),但直接写 glob("*.py") 只匹配一层)。

Q3:p == q 返回 False,但看起来是同一个文件? ​

原因:== 比较的是路径字符串,不是物理文件。a/b/../c.txt 和 a/c.txt 字符串不同,所以 == 为 False。

解决:想判断"是否同一个物理文件",用 samefile():

python
print(Path("a/b/../c.txt").samefile(Path("a/c.txt")))   # True(若文件真实存在)

Q4:为什么我删文件用了 remove() 结果报错? ​

原因:Path 对象没有 remove() 方法,删除文件要用 unlink()。

python
Path("old.txt").unlink(missing_ok=True)   # 正确;Path.remove() 不存在

Q5:is_file() 和 exists() 返回了 False,但文件明明在? ​

常见原因:权限不足。exists()/is_file() 在遇到权限错误时不会抛异常,而是直接返回 False(is_file 对"权限错误"这类 OS 错误还会向上抛出,但 exists 会吞掉)。此时应检查当前用户对路径的读取权限,或改用带异常处理的 stat() 来精确判断。