4 道练习题·预计 95 分钟·做对一题解锁下一段
各位有没有写过这样的代码?
import os
base_dir = os.path.dirname(os.path.abspath(__file__))
data_path = os.path.join(base_dir, 'data', 'sub', 'foo.txt')
if os.path.exists(data_path) and os.path.isfile(data_path):
with open(data_path, 'r', encoding='utf-8') as f:
content = f.read()
print(content)是不是看着头大?一个 os.path.dirname 套一个 os.path.abspath,再嵌一个 os.path.join,路径就这么一层一层包出来了。要打开还得 with open(...) as f:,要判断还得 os.path.exists 加 os.path.isfile,方法散落在 os 和 os.path 两个模块里,还要再加一个 open 内建函数。光是数函数名就数得我水哥头都晕。
更要命的是,路径在这套写法里只是个普通字符串。字符串就是字符串,它不知道自己代表的是文件还是目录,更不可能自己跑去判断「我存在吗」、「我是什么后缀」。所有这些操作都得拿着字符串去喂给一堆函数,函数再吐回字符串,字符串再传给下一个函数 …… 整个过程像极了车间里搬砖头的流水线。
那么有没有一种写法,让路径自己「活」起来呢?让 Path('data/foo.txt') 这种东西本身就知道自己是不是文件、能不能被读取、有什么后缀名?
有的,就是今天要聊的 pathlib。这是 Python 3.4 加进来的标准库,专门用来收拾上面这种乱糟糟的路径代码。从 Python 3.6 开始,标准库里大部分接受路径字符串的函数也都能直接吃 Path 对象,所以基本可以无痛切过来。
写到现在 Python 都 3.13 了,各位童鞋如果还在 os.path.join 一条道走到黑,那真的是亏大了。本文就跟着水哥一起,把 pathlib 这套现代 API 从头到尾捋一遍。
先来认识一下主角:
from pathlib import Path
p = Path('data/foo.txt')
print(p)
print(type(p))输出可能长这样(macOS / Linux 上):
data/foo.txt
<class 'pathlib.PosixPath'>是不是发现什么不对?我们明明是 Path('data/foo.txt'),怎么打印出来类型是 PosixPath?
这是因为 Path 是个聪明家伙,它会根据你当前的操作系统,自动给你返回对应平台的子类:
PosixPathWindowsPath各位平时基本不用关心这个,直接用 Path 就完事了,需要跨平台细节的时候再说。
除了 Path('字符串') 这种基础用法,还有两个非常常用的「快捷入口」:
from pathlib import Path
print(Path.home())
print(Path.cwd())输出:
/Users/two_water
/Users/two_water/projects/demoPath.home() 是当前用户的家目录,Path.cwd() 是「当前工作目录」(current working directory)。这两个相当于以前的 os.path.expanduser('~') 和 os.getcwd(),不过显然新写法看着更清爽,是不是?
/ 来拼接路径路径拼接是最常见的操作,没有之一。看看老写法:
import os
base = '/tmp'
data_path = os.path.join(base, 'demo', 'sub', 'foo.txt')
print(data_path)输出:
/tmp/demo/sub/foo.txt这个 os.path.join 用是好用,但写起来嘴里得念好几遍 os path join,手指都不想动了。
然后看看 pathlib 是怎么玩的:
from pathlib import Path
base = Path('/tmp')
data_path = base / 'demo' / 'sub' / 'foo.txt'
print(data_path)输出:
/tmp/demo/sub/foo.txt注意看:拼接路径用的是 / 这个操作符。是不是有点神奇?
为什么啊?因为 Path 重载了 __truediv__(也就是除法操作符),所以 Path('/tmp') / 'demo' 这种写法就被翻译成了路径拼接。这种设计简直是天才,因为路径在 URL、Linux 文件系统里用的本来就是 /,跟我们脑子里的语义完全一致。
来个对比表,看看是不是清爽多了:
| 老写法 | 新写法 |
|---|---|
os.path.join(a, b, c) | Path(a) / b / c |
os.path.join(os.path.dirname(__file__), 'data') | Path(__file__).parent / 'data' |
os.path.expanduser('~/Documents') | Path.home() / 'Documents' |
而且 / 还可以拿一个 Path 跟字符串混着用,结果都是 Path 对象:
from pathlib import Path
p1 = Path('/tmp') / 'foo'
p2 = Path('/tmp') / Path('foo')
p3 = 'foo' / Path('tmp')
print(p1)
print(p2)
print(p3)输出:
/tmp/foo
/tmp/foo
foo/tmp不管 / 左边还是右边是字符串,只要有一边是 Path,结果就还是 Path。
各位写过爬虫或者文件处理代码的话,对「拿到一个路径,我要它的文件名」、「我要它的扩展名」这种需求肯定不陌生。老写法散布在 os.path.basename、os.path.splitext 这些函数里,记起来挺麻烦。
Path 把这些都做成属性了,一个个看。
.name:完整文件名(带后缀)from pathlib import Path
p = Path('/tmp/demo/foo.txt')
print(p.name)输出:
foo.txt相当于以前的 os.path.basename(...)。
.stem:去掉后缀的「主干」from pathlib import Path
p = Path('/tmp/demo/foo.txt')
print(p.stem)输出:
foo这个就是文件名去掉扩展名的部分,特别适合用来生成「同名但换后缀」的新文件,比如 foo.txt 转 foo.json。
.suffix:扩展名(带点)from pathlib import Path
p = Path('/tmp/demo/foo.txt')
print(p.suffix)输出:
.txt注意是带点的。如果你需要不带点的,自己 [1:] 切一下就好。
那么如果是 foo.tar.gz 这种多后缀的呢?
from pathlib import Path
p = Path('archive.tar.gz')
print(p.suffix)
print(p.suffixes)输出:
.gz
.tar.gz看到了吗?.suffix 只给最后一个,.suffixes 给一个列表。
请:
pathlib 导入 Pathp = Path('/tmp/data/foo.txt').name、.suffix、.stem输出应该是:
foo.txt
.txt
foo注意:print(p.name) 输出的是 foo.txt,不是 'foo.txt'——直接打印不带引号。
推荐一个,全网最全最友好的中文 AI 编程指南: coding.stormzhang.ai