PEP 428 – pathlib 模块 – 面向对象的文件系统路径
- 作者:
- Antoine Pitrou <solipsis at pitrou.net>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2012年7月30日
- Python 版本:
- 3.4
- 发布历史:
- 2012年10月5日
- 决议:
- Python-Dev 消息
摘要
本PEP提议将第三方模块 pathlib 纳入标准库。该提议根据 PEP 411 中所述的临时标签进行。因此,API更改可以在PEP流程中进行,也可以在被标准库接受后(直到临时标签被移除)进行。
该库旨在提供一个简单的类层次结构,用于处理文件系统路径以及用户对其进行的常见操作。
实施
本提案的实现正在 pathlib 的 Mercurial 仓库 的 pep428 分支中进行跟踪。
为什么选择面向对象的API
使用专用类来表示文件系统路径的理由,与日期、时间或IP地址等其他无状态对象的理由相同。Python一直在慢慢地从严格复制C语言的API转向为各种常见功能提供更好、更有用的抽象。即使本PEP未被接受,未来某一天标准库也很可能会采用其他形式的文件系统处理抽象。
实际上,许多人会更喜欢使用 datetime 模块提供的高级对象来处理日期和时间,而不是使用数字时间戳和 time 模块API。此外,使用专用类可以默认启用所需的行为,例如Windows路径的大小写不敏感性。
提案
类层次结构
pathlib 模块实现了一个简单的类层次结构
+----------+
| |
---------| PurePath |--------
| | | |
| +----------+ |
| | |
| | |
v | v
+---------------+ | +-----------------+
| | | | |
| PurePosixPath | | | PureWindowsPath |
| | | | |
+---------------+ | +-----------------+
| v |
| +------+ |
| | | |
| -------| Path |------ |
| | | | | |
| | +------+ | |
| | | |
| | | |
v v v v
+-----------+ +-------------+
| | | |
| PosixPath | | WindowsPath |
| | | |
+-----------+ +-------------+
此层次结构沿两个维度划分路径类
- 路径类可以是纯粹的(pure)或具体的(concrete):纯粹类只支持不需要执行任何实际I/O的操作,这些操作是大多数路径操作;具体类支持纯粹类的所有操作,以及执行I/O的操作。
- 根据其所表示的操作系统路径类型,路径类属于给定类型。pathlib 实现了两种类型:Windows路径用于Windows系统中体现的文件系统语义,POSIX路径用于其他系统。
任何纯粹类都可以在任何系统上实例化:例如,您可以在Windows下操作 PurePosixPath 对象,在Unix下操作 PureWindowsPath 对象等。但是,具体类只能在匹配的系统上实例化:实际上,在Unix下使用 WindowsPath 对象执行I/O会容易出错,反之亦然。
此外,还有两个基类,它们也充当依赖于系统的工厂:PurePath 将根据操作系统实例化 PurePosixPath 或 PureWindowsPath。类似地,Path 将实例化 PosixPath 或 WindowsPath。
预计在大多数情况下,使用 Path 类是足够的,这就是为什么它的名字最短。
不与内置类型混淆
在本提案中,路径类不派生自内置类型。这与一些其他派生自 str 的 Path 类提案形成对比。它们也不假装实现序列协议:如果您希望路径表现为序列,则必须查找一个专用属性(parts 属性)。
不继承自 str 的主要原因是为了防止意外地将表示路径的字符串与不表示路径的字符串进行操作,例如 path + an_accident。由于字符串操作不一定导致有效或预期的文件系统路径,因此通过不继承 str 来避免与字符串的意外操作,从而实现“显式优于隐式”。一位Python核心开发人员的 博客文章 更详细地阐述了这一特定设计决策背后的原因。
不变性
路径对象是不可变的,这使得它们可哈希,并防止了一类编程错误。
理智的行为
os.path 的功能很少被重用。许多 os.path 函数由于向后兼容性而与令人困惑或完全错误的行为绑定(例如,os.path.abspath() 在不首先解析符号链接的情况下简化“..”路径组件的事实)。
比较
相同类型的路径是可比较和可排序的,无论是否纯粹
>>> PurePosixPath('a') == PurePosixPath('b')
False
>>> PurePosixPath('a') < PurePosixPath('b')
True
>>> PurePosixPath('a') == PosixPath('a')
True
比较和排序Windows路径对象时,不区分大小写
>>> PureWindowsPath('a') == PureWindowsPath('A')
True
不同类型的路径总是比较不相等,且不可排序
>>> PurePosixPath('a') == PureWindowsPath('a')
False
>>> PurePosixPath('a') < PureWindowsPath('a')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unorderable types: PurePosixPath() < PureWindowsPath()
路径与内置类型(如 str)的实例以及任何其他类型比较不相等,且不可排序。
有用的表示法
API试图提供有用的表示法,同时避免魔法。一些例子:
>>> p = Path('/home/antoine/pathlib/setup.py')
>>> p.name
'setup.py'
>>> p.suffix
'.py'
>>> p.root
'/'
>>> p.parts
('/', 'home', 'antoine', 'pathlib', 'setup.py')
>>> p.relative_to('/home/antoine')
PosixPath('pathlib/setup.py')
>>> p.exists()
True
纯路径API
PurePath API 的理念是提供一系列一致的、有用的路径操作,而不是像 os.path 那样暴露一堆混乱的函数。
定义
首先是一些约定
- 所有路径都可以有驱动器和根目录。对于 POSIX 路径,驱动器总是空的。
- 相对路径既没有驱动器也没有根目录。
- 如果 POSIX 路径有根目录,则它是绝对路径。如果 Windows 路径同时有驱动器 和 根目录,则它是绝对路径。Windows UNC 路径(例如
\\host\share\myfile.txt)总是包含驱动器和根目录(这里分别是\\host\share和\)。 - 如果路径有驱动器 *或* 根目录,则称其为锚定路径。其锚定是驱动器和根目录的连接。在 POSIX 下,“锚定”与“绝对”相同。
构造
我们将一并介绍构造和连接,因为它们暴露出相似的语义。
构造路径最简单的方法是传递其字符串表示
>>> PurePath('setup.py')
PurePosixPath('setup.py')
多余的路径分隔符和 "." 组件被消除
>>> PurePath('a///b/c/./d/')
PurePosixPath('a/b/c/d')
如果您传递多个参数,它们将自动连接
>>> PurePath('docs', 'Makefile')
PurePosixPath('docs/Makefile')
连接语义与 os.path.join 类似,锚定路径会忽略之前连接的组件的信息
>>> PurePath('/etc', '/usr', 'bin')
PurePosixPath('/usr/bin')
然而,对于Windows路径,驱动器会根据需要保留。
>>> PureWindowsPath('c:/foo', '/Windows')
PureWindowsPath('c:/Windows')
>>> PureWindowsPath('c:/foo', 'd:')
PureWindowsPath('d:')
此外,路径分隔符被规范化为平台默认值
>>> PureWindowsPath('a/b') == PureWindowsPath('a\\b')
True
多余的路径分隔符和 "." 组件会被消除,但 ".." 组件不会
>>> PurePosixPath('a//b/./c/')
PurePosixPath('a/b/c')
>>> PurePosixPath('a/../b')
PurePosixPath('a/../b')
多个前导斜杠的处理方式取决于路径类型。在 Windows 路径中它们总是被保留(因为 UNC 表示法)
>>> PureWindowsPath('//some/path')
PureWindowsPath('//some/path/')
在 POSIX 中,它们会被折叠,除非恰好有两个前导斜杠,这是 POSIX 规范中 路径名解析 的一个特殊情况(这也对于 Cygwin 兼容性是必需的)
>>> PurePosixPath('///some/path')
PurePosixPath('/some/path')
>>> PurePosixPath('//some/path')
PurePosixPath('//some/path')
不带任何参数调用构造函数会创建一个指向逻辑“当前目录”的路径对象(不查找其绝对路径,这是具体路径上 cwd() 类方法的职责)
>>> PurePosixPath()
PurePosixPath('.')
表示
要表示一个路径(例如,将其传递给第三方库),只需在其上调用 str()
>>> p = PurePath('/home/antoine/pathlib/setup.py')
>>> str(p)
'/home/antoine/pathlib/setup.py'
>>> p = PureWindowsPath('c:/windows')
>>> str(p)
'c:\\windows'
要强制使用正斜杠的字符串表示,请使用 as_posix() 方法
>>> p.as_posix()
'c:/windows'
要获取字节表示(在Unix系统下可能有用),请在其上调用 bytes(),它内部使用 os.fsencode()
>>> bytes(p)
b'/home/antoine/pathlib/setup.py'
要将路径表示为 file: URI,请调用 as_uri() 方法
>>> p = PurePosixPath('/etc/passwd')
>>> p.as_uri()
'file:///etc/passwd'
>>> p = PureWindowsPath('c:/Windows')
>>> p.as_uri()
'file:///c:/Windows'
路径的 repr() 总是使用正斜杠,即使在 Windows 下也是如此,以提高可读性并提醒用户正斜杠是可行的
>>> p = PureWindowsPath('c:/Windows')
>>> p
PureWindowsPath('c:/Windows')
属性
每个路径都提供了几个简单的属性(每个属性都可以为空)
>>> p = PureWindowsPath('c:/Downloads/pathlib.tar.gz')
>>> p.drive
'c:'
>>> p.root
'\\'
>>> p.anchor
'c:\\'
>>> p.name
'pathlib.tar.gz'
>>> p.stem
'pathlib.tar'
>>> p.suffix
'.gz'
>>> p.suffixes
['.tar', '.gz']
派生新路径
连接
一个路径可以使用 / 运算符与另一个路径连接
>>> p = PurePosixPath('foo')
>>> p / 'bar'
PurePosixPath('foo/bar')
>>> p / PurePosixPath('bar')
PurePosixPath('foo/bar')
>>> 'bar' / p
PurePosixPath('bar/foo')
与构造函数一样,可以指定多个路径组件,无论是折叠的还是分开的
>>> p / 'bar/xyzzy'
PurePosixPath('foo/bar/xyzzy')
>>> p / 'bar' / 'xyzzy'
PurePosixPath('foo/bar/xyzzy')
还提供了一个具有相同行为的 joinpath() 方法
>>> p.joinpath('Python')
PurePosixPath('foo/Python')
更改路径的最终组件
with_name() 方法返回一个新路径,其名称已更改。
>>> p = PureWindowsPath('c:/Downloads/pathlib.tar.gz')
>>> p.with_name('setup.py')
PureWindowsPath('c:/Downloads/setup.py')
如果路径没有实际名称,则会引发 ValueError。
>>> p = PureWindowsPath('c:/')
>>> p.with_name('setup.py')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "pathlib.py", line 875, in with_name
raise ValueError("%r has an empty name" % (self,))
ValueError: PureWindowsPath('c:/') has an empty name
>>> p.name
''
with_suffix() 方法返回一个后缀已更改的新路径。但是,如果路径没有后缀,则会添加新后缀。
>>> p = PureWindowsPath('c:/Downloads/pathlib.tar.gz')
>>> p.with_suffix('.bz2')
PureWindowsPath('c:/Downloads/pathlib.tar.bz2')
>>> p = PureWindowsPath('README')
>>> p.with_suffix('.bz2')
PureWindowsPath('README.bz2')
使路径相对
relative_to() 方法计算一个路径相对于另一个路径的相对差异。
>>> PurePosixPath('/usr/bin/python').relative_to('/usr')
PurePosixPath('bin/python')
如果方法无法返回有意义的值,则会引发 ValueError。
>>> PurePosixPath('/usr/bin/python').relative_to('/etc')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "pathlib.py", line 926, in relative_to
.format(str(self), str(formatted)))
ValueError: '/usr/bin/python' does not start with '/etc'
类序列访问
parts 属性返回一个元组,提供对路径组件的只读序列访问。
>>> p = PurePosixPath('/etc/init.d')
>>> p.parts
('/', 'etc', 'init.d')
Windows 路径将驱动器和根目录作为一个单一的路径组件处理
>>> p = PureWindowsPath('c:/setup.py')
>>> p.parts
('c:\\', 'setup.py')
(将它们分开是错误的,因为 C: 不是 C:\\ 的父级)。
parent 属性返回路径的逻辑父级
>>> p = PureWindowsPath('c:/python33/bin/python.exe')
>>> p.parent
PureWindowsPath('c:/python33/bin')
parents 属性返回路径逻辑祖先的不可变序列
>>> p = PureWindowsPath('c:/python33/bin/python.exe')
>>> len(p.parents)
3
>>> p.parents[0]
PureWindowsPath('c:/python33/bin')
>>> p.parents[1]
PureWindowsPath('c:/python33')
>>> p.parents[2]
PureWindowsPath('c:/')
查询
is_relative() 如果路径是相对的(参见上文定义),则返回 True,否则返回 False。
is_reserved() 如果 Windows 路径是诸如 CON 或 NUL 等保留路径,则返回 True。对于 POSIX 路径,它始终返回 False。
match() 将路径与 glob 模式进行匹配。它对单个部分进行操作,并从右侧开始匹配
>>> p = PurePosixPath('/usr/bin')
>>> p.match('/usr/b*')
True
>>> p.match('usr/b*')
True
>>> p.match('b*')
True
>>> p.match('/u*')
False
此行为符合以下预期
- 一个简单的模式,例如“*.py”,只要最后一部分匹配,就能匹配任意长的路径,例如“/usr/foo/bar.py”。
- 也可以使用更长的模式进行更复杂的匹配,例如“/usr/foo/*.py”匹配“/usr/foo/bar.py”。
具体路径API
除了纯粹API的操作外,具体路径还提供额外的、实际访问文件系统以查询或修改信息的方法。
构造
类方法 cwd() 创建一个以绝对形式指向当前工作目录的路径对象
>>> Path.cwd()
PosixPath('/home/antoine/pathlib')
文件元数据
stat() 返回文件的 stat() 结果;类似地,lstat() 返回文件的 lstat() 结果(如果文件是符号链接,则不同)。
>>> p.stat()
posix.stat_result(st_mode=33277, st_ino=7483155, st_dev=2053, st_nlink=1, st_uid=500, st_gid=500, st_size=928, st_atime=1343597970, st_mtime=1328287308, st_ctime=1343597964)
高级方法有助于检查文件类型
>>> p.exists()
True
>>> p.is_file()
True
>>> p.is_dir()
False
>>> p.is_symlink()
False
>>> p.is_socket()
False
>>> p.is_fifo()
False
>>> p.is_block_device()
False
>>> p.is_char_device()
False
文件所有者和组名(而不是数字ID)通过相应的方法进行查询
>>> p = Path('/etc/shadow')
>>> p.owner()
'root'
>>> p.group()
'shadow'
路径解析
resolve() 方法使路径变为绝对路径,并在此过程中解析任何符号链接(类似于 POSIX realpath() 调用)。它是唯一会移除 ".." 路径组件的操作。在 Windows 上,此方法还会负责返回规范路径(具有正确的大小写)。
目录遍历
简单的(非递归的)目录访问是通过调用 iterdir() 方法实现的,该方法返回一个子路径的迭代器。
>>> p = Path('docs')
>>> for child in p.iterdir(): child
...
PosixPath('docs/conf.py')
PosixPath('docs/_templates')
PosixPath('docs/make.bat')
PosixPath('docs/index.rst')
PosixPath('docs/_build')
PosixPath('docs/_static')
PosixPath('docs/Makefile')
这允许通过列表推导进行简单过滤
>>> p = Path('.')
>>> [child for child in p.iterdir() if child.is_dir()]
[PosixPath('.hg'), PosixPath('docs'), PosixPath('dist'), PosixPath('__pycache__'), PosixPath('build')]
还提供了简单和递归的globbing
>>> for child in p.glob('**/*.py'): child
...
PosixPath('test_pathlib.py')
PosixPath('setup.py')
PosixPath('pathlib.py')
PosixPath('docs/conf.py')
PosixPath('build/lib/pathlib.py')
文件打开
open() 方法提供了一个类似于内置 open() 方法的文件打开API。
>>> p = Path('setup.py')
>>> with p.open() as f: f.readline()
...
'#!/usr/bin/env python3\n'
文件系统修改
提供了几个常见的文件系统操作作为方法:touch(), mkdir(), rename(), replace(), unlink(), rmdir(), chmod(), lchmod(), symlink_to()。可以提供更多操作,例如 shutil 模块的一些功能。
拟议API的详细文档可以在 pathlib 文档中找到。
讨论
除法运算符
在关于路径连接运算符的 投票 中,除法运算符首先脱颖而出。 pathlib 的最初版本使用方括号(即 __getitem__)代替。
joinpath()
joinpath() 方法最初名为 join(),但许多人反对,因为它可能与 str.join() 混淆,后者具有不同的语义。因此,它被重命名为 joinpath()。
大小写敏感性
Windows 用户认为文件系统路径不区分大小写,并期望路径对象遵守这一特性,尽管在某些极少数情况下,一些外部文件系统挂载在 Windows 下可能区分大小写。
引用一位评论者的话,
“如果在 Windows 上 glob("*.py") 未能找到 SETUP.PY,那将是一场可用性灾难”。——Paul Moore 在 https://mail.python.org/pipermail/python-dev/2013-April/125254.html
版权
本文档已进入公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0428.rst