PEP 519 – 添加文件系统路径协议
- 作者:
- Brett Cannon <brett at python.org>,Koos Zevenhoven <k7hoven at gmail.com>
- 状态:
- 最终
- 类型:
- 标准轨迹
- 创建:
- 2016年5月11日
- Python版本:
- 3.6
- 历史记录:
- 2016年5月11日,2016年5月12日,2016年5月13日
- 决议:
- Python-Dev消息
摘要
本PEP建议一个协议,用于表示文件系统路径的类,以便能够提供str
或bytes
表示形式。还建议对Python的标准库进行更改,以便在适当的地方利用此协议,以促进在历史上仅接受str
和/或bytes
文件系统路径的地方使用路径对象。目标是促进用户向丰富的路径对象迁移,同时提供一种简单的方法来处理期望str
或bytes
的代码。
基本原理
历史上,在Python中,文件系统路径表示为字符串或字节。这种表示形式的选择源于C自己的决定,即将文件系统路径表示为const char *
[3]。虽然对于文件系统路径来说,这是一种完全可用的格式,但它不一定是最佳的。问题在于,虽然所有文件系统路径都可以表示为字符串或字节,但并非所有字符串或字节都表示文件系统路径。这可能导致出现问题,例如任何字符串都像文件系统路径一样,无论它是否真正表示路径。
为了帮助将文件系统路径的表示形式从字符串和字节提升到更丰富的对象表示形式,pathlib模块 [4]在Python 3.4中通过PEP 428被临时引入。虽然有些人认为它比字符串和字节更适合表示文件系统路径,但它一直缺乏采用。通常,低采用率的关键问题是标准库缺乏支持。这种缺乏支持要求pathlib的用户通过调用str(path)
手动将路径对象转换为字符串,许多人发现这容易出错。
将路径对象转换为字符串的一个问题来自这样一个事实,即获取路径字符串表示的唯一通用方法是将对象传递给str()
。当盲目地这样做时,这可能是一个问题,因为几乎所有Python对象都有一些字符串表示形式,无论它们是否是路径,例如str(None)
将给出一个builtins.open()
[5]将乐于使用它来创建新文件的结果。
使整个情况更加复杂的是DirEntry
对象 [8]。虽然路径对象具有可以使用str()
提取的表示形式,但DirEntry
对象反而公开了一个path
属性。路径对象、DirEntry
以及任何其他第三方路径库之间没有通用接口,这已经成为一个问题。需要一个解决方案,允许任何表示路径的对象声明它是一个路径,以及一种提取所有路径对象都可以支持的低级表示形式的方法。
因此,本PEP建议引入一个新的协议,供表示文件系统路径的对象遵循。提供协议允许显式地表明哪些对象表示文件系统路径,以及提取低级表示形式的方法,该表示形式可用于仅支持字符串或字节的旧版API。
关于导致本PEP的路径对象的讨论可以在2016年3月和4月期间的python-ideas邮件列表存档 [1]以及2016年4月期间的python-dev邮件列表存档 [2]的多个线程中找到。
提案
本提案分为两部分。一部分是关于对象声明和提供支持以公开文件系统路径表示形式的协议的提案。另一部分涉及对Python标准库进行更改以支持新协议。这些更改也将导致pathlib模块取消其临时状态。
协议
以下抽象基类定义了将对象视为路径对象的协议
import abc
import typing as t
class PathLike(abc.ABC):
"""Abstract base class for implementing the file system path protocol."""
@abc.abstractmethod
def __fspath__(self) -> t.Union[str, bytes]:
"""Return the file system path representation of the object."""
raise NotImplementedError
表示文件系统路径的对象将实现__fspath__()
方法,该方法将返回路径的str
或bytes
表示形式。str
表示形式是首选的低级路径表示形式,因为它对人类可读,并且是人们历史上用来表示路径的方式。
标准库更改
预计Python标准库中目前接受文件系统路径的大多数API将被适当地更新以接受路径对象(无论这是否需要代码或仅仅是文档更新)。但是,下面提到的模块值得详细说明,因为它们要么具有增强使用路径对象能力的基本更改,要么涉及API的添加/删除。
内置函数
open()
[5]将更新为接受路径对象,并继续接受str
和bytes
。
os
将添加fspath()
函数,其语义如下
import typing as t
def fspath(path: t.Union[PathLike, str, bytes]) -> t.Union[str, bytes]:
"""Return the string representation of the path.
If str or bytes is passed in, it is returned unchanged. If __fspath__()
returns something other than str or bytes then TypeError is raised. If
this function is given something that is not str, bytes, or os.PathLike
then TypeError is raised.
"""
if isinstance(path, (str, bytes)):
return path
# Work from the object's type to match method resolution of other magic
# methods.
path_type = type(path)
try:
path = path_type.__fspath__(path)
except AttributeError:
if hasattr(path_type, '__fspath__'):
raise
else:
if isinstance(path, (str, bytes)):
return path
else:
raise TypeError("expected __fspath__() to return str or bytes, "
"not " + type(path).__name__)
raise TypeError("expected str, bytes or os.PathLike object, not "
+ path_type.__name__)
os.fsencode()
[6]和os.fsdecode()
[7]函数将更新为接受路径对象。由于这两个函数都将其参数强制转换为bytes
和str
,因此它们将更新为在存在时调用__fspath__()
以将路径对象转换为str
或bytes
表示形式,然后执行其相应的强制转换操作,就好像__fspath__()
的返回值是问题的强制转换函数的原始参数一样。
添加os.fspath()
、更新os.fsencode()
/os.fsdecode()
以及pathlib.PurePath
的当前语义提供了获取所需路径表示形式所需的语义。对于路径对象,可以使用pathlib.PurePath
/Path
。要获取str
或bytes
表示形式而无需任何强制转换,则可以使用os.fspath()
。如果需要str
并且应假设bytes
的编码为默认文件系统编码,则应使用os.fsdecode()
。如果需要bytes
表示形式并且应使用默认文件系统编码对任何字符串进行编码,则使用os.fsencode()
。本PEP建议尽可能使用路径对象,并在必要时回退到字符串路径,并尽可能使用bytes
。
另一种看待这一点的方法是作为文件系统路径表示形式的层次结构(从最高到最低级别):路径→str→bytes。正在讨论的函数和类都可以接受同一层次结构上的对象,但它们在是否将对象提升或降级到另一个级别方面有所不同。pathlib.PurePath
类可以将str
提升为路径对象。os.fspath()
函数可以将路径对象降级为str
或bytes
实例,具体取决于__fspath__()
返回的内容。os.fsdecode()
函数将路径对象降级为字符串或将bytes
对象提升为str
。os.fsencode()
函数将路径或字符串对象降级为bytes
。没有函数提供一种方法可以直接将路径对象降级为bytes
,同时绕过字符串降级。
DirEntry
对象 [8]将获得一个__fspath__()
方法。它将返回与DirEntry
实例的path
属性上当前找到的值相同的值。
将在os
模块中添加协议ABC,名称为os.PathLike
。
os.path
os.path
[9]的各种路径操作函数将更新为接受路径对象。对于接受字节和字符串的多态函数,它们将更新为仅使用os.fspath()
。
在撰写此 PEP 期间,有人建议不要使用“显式优于隐式”的论点来更新os.path
。其想法是,由于__fspath__()
本身就是多态的,因此最好让与os.path
一起工作的代码显式地从路径对象中提取路径表示。此外,还考虑了在底层 OS API 中添加如此深层的支持会导致代码在不需要任何文档更新的情况下神奇地支持路径对象,从而在代码无法工作时(项目作者不知情的情况下)可能导致投诉。
但此 PEP 认为在这种情况下“实用性胜于纯粹性”。为了帮助促进对路径对象的支持,最好尽可能地简化过渡,而不是担心项目对路径对象的意外/未记录的鸭子类型支持。
也有人建议os.path
函数可能在紧密循环中使用,并且检查或调用__fspath__()
的开销过高。在这种情况下,只有使用路径的 API 会被直接更新,而操作路径的 API(例如os.path
中的 API)将保持不变。这将要求库作者更新他们的代码以支持路径对象(如果他们执行任何路径操作),但如果库代码直接传递路径,则库不需要更新。然而,此 PEP 和 Guido 认为这是一个不必要的担忧,并且性能仍然可以接受。
pathlib
pathlib.PurePath
和pathlib.Path
的构造函数将更新为接受PathLike
对象。PurePath
和Path
都将继续不接受bytes
路径表示,因此,如果__fspath__()
返回bytes
,它将引发异常。
将删除path
属性,因为此 PEP 使其变得冗余(它尚未包含在任何已发布的 Python 版本中,因此不是向后兼容性问题)。
C API
C API 将获得一个等效于os.fspath()
的函数。
/*
Return the file system path representation of the object.
If the object is str or bytes, then allow it to pass through with
an incremented refcount. If the object defines __fspath__(), then
return the result of that method. All other types raise a TypeError.
*/
PyObject *
PyOS_FSPath(PyObject *path)
{
_Py_IDENTIFIER(__fspath__);
PyObject *func = NULL;
PyObject *path_repr = NULL;
if (PyUnicode_Check(path) || PyBytes_Check(path)) {
Py_INCREF(path);
return path;
}
func = _PyObject_LookupSpecial(path, &PyId___fspath__);
if (NULL == func) {
return PyErr_Format(PyExc_TypeError,
"expected str, bytes or os.PathLike object, "
"not %S",
path->ob_type);
}
path_repr = PyObject_CallFunctionObjArgs(func, NULL);
Py_DECREF(func);
if (!PyUnicode_Check(path_repr) && !PyBytes_Check(path_repr)) {
Py_DECREF(path_repr);
return PyErr_Format(PyExc_TypeError,
"expected __fspath__() to return str or bytes, "
"not %S",
path_repr->ob_type);
}
return path_repr;
}
向后兼容性
没有明确的向后兼容性问题。除非某个对象偶然已经定义了__fspath__()
方法,否则没有理由期望现有的代码中断或期望其语义被隐式更改。
希望支持路径对象和 Python 3.6 之前的 Python 版本以及os.fspath()
不存在的库可以使用path.__fspath__() if hasattr(path, "__fspath__") else path
的习惯用法。
实现
这是此 PEP 提议在 Python 3.6 中更改的内容的任务列表。
- 从 pathlib 中删除
path
属性(已完成) - 删除 pathlib 的临时状态(已完成)
- 添加
os.PathLike
(代码和文档已完成) - 添加
PyOS_FSPath()
(代码和文档已完成) - 添加
os.fspath()
(已完成 <已完成) - 更新
os.fsencode()
(已完成) - 更新
os.fsdecode()
(已完成) - 更新
pathlib.PurePath
和pathlib.Path
(已完成)- 添加
__fspath__()
- 在构造函数中添加对
os.PathLike
的支持
- 添加
- 向
DirEntry
添加__fspath__()
(已完成) - 更新
builtins.open()
(已完成) - 更新
os.path
(已完成) - 添加“路径式”的术语表条目(术语表)(已完成)
- 更新“新增功能”(已完成)
被拒绝的想法
协议方法的其他名称
在讨论导致此 PEP 的过程中提出了各种名称,包括__path__
、__pathname__
和__fspathname__
。最终,人们似乎倾向于__fspath__
,因为它在不显得过长的情况下具有明确性。
分离的str/bytes方法
有一段时间,有人建议__fspath__()
只返回字符串,并引入另一个名为__fspathb__()
的方法来返回字节。其想法是,通过使__fspath__()
不具有多态性,可以更轻松地处理潜在的字符串或字节表示。但普遍共识是返回字节的情况很可能很少见,并且 os 模块中的各种函数是比直接调用__fspath__()
更好的抽象。
提供path
属性
为了帮助解决pathlib.PurePath
没有继承自str
的问题,最初有人建议引入一个path
属性来镜像os.DirEntry
提供的功能。但最终,确定协议可以提供相同的结果,而不会直接公开大多数人永远不需要直接交互的 API。
仅让__fspath__()
返回字符串
导致此 PEP 的许多讨论都围绕着__fspath__()
是否应该是多态的并返回bytes
和str
,或者只返回str
。对于这种观点,普遍的看法是bytes
难以使用,因为它们本质上缺乏有关其编码的信息,并且PEP 383使得可以使用str
和surrogateescape
处理程序来表示所有文件系统路径。因此,最好强制使用str
作为高级路径对象的底层路径表示。
最终,我们决定使用bytes
来表示路径的做法不会消失,因此应该在某种程度上支持它们。希望人们会倾向于使用像 pathlib 这样的路径对象,这将使人们远离直接使用bytes
。
通用字符串编码机制
有一段时间,我们讨论过开发一个通用机制来提取对象的字符串表示,该表示具有语义意义(__str__()
不一定返回任何超出可能对调试有帮助的语义意义的内容)。最终,认为除了此 PEP 正在以特定方式解决的问题之外,它缺乏令人信服的需求。
让__fspath__成为属性
我们曾简要考虑过让__fspath__
成为一个属性而不是方法。这被拒绝有两个原因。第一,从历史上看,协议一直以“魔术方法”而不是“魔术方法和属性”的方式实现。第二,不能保证路径对象的底层表示将被预先计算,如果属性被实现为属性,则可能会误导用户认为后台没有进行昂贵的计算。
这也间接地与引入path
属性以实现相同目的的想法相关。然而,这个想法还存在一个额外的问题,即意外地让任何具有path
属性的对象满足协议的鸭子类型。为协议引入一个新的魔术方法可以有效地避免任何意外地选择加入协议。
提供特定的类型提示支持
我们曾考虑过提供一个通用的typing.PathLike
类,它允许例如typing.PathLike[str]
指定返回字符串表示的路径对象的类型提示。虽然可能会有益,但其实用性被认为太小,不值得添加类型提示类。
这也消除了在typing
模块中添加一个表示所有可接受的路径表示类型的联合的类的愿望,因为这可以通过typing.Union[str, bytes, os.PathLike]
轻松地表示,并且希望用户会慢慢地只转向路径对象。
提供os.fspathb()
有人建议,为了镜像例如os.getcwd()
/os.getcwdb()
的结构,os.fspath()
只返回str
,并引入另一个名为os.fspathb()
的函数,该函数只返回bytes
。这被拒绝了,因为*b()
函数的用途与查询文件系统相关,在查询文件系统时需要获取原始字节。由于此 PEP 不直接处理文件系统上的数据(但可能处理),因此认为这种区别是不必要的。还认为,仅对字节的需求不会普遍到需要以os.fsencode()
将提供类似功能的方式进行专门支持。
从实例调用__fspath__()
本 PEP 的早期草案中,os.fspath()
调用 path.__fspath__()
而不是 type(path).__fspath__(path)
。更改是为了与 Python 中其他魔法方法的解析方式保持一致。
鸣谢
感谢所有参与与本 PEP 相关讨论的人,这些讨论涵盖了 python-ideas 和 python-dev 邮件列表。特别感谢 Stephen Turnbull 对本 PEP 早期草案提供的直接反馈。还要特别感谢 Koos Zevenhoven 和 Ethan Furman,他们不仅对本 PEP 早期草案提供了反馈,还帮助推动了这两个邮件列表中关于此主题的整体讨论。
参考文献
版权
本文档已进入公有领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0519.rst