Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

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 提出了一个协议,用于表示文件系统路径的类能够提供 strbytes 表示。还提议对 Python 的标准库进行更改,以在适当的地方利用此协议,从而便于在历史上只接受 str 和/或 bytes 文件系统路径的情况下使用路径对象。目标是促进用户向丰富的路径对象迁移,同时提供一种与期望 strbytes 的代码轻松协作的方式。

基本原理

在 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 的路径对象的讨论,可以在 python-ideas 邮件列表存档 [1] 2016 年 3 月和 4 月的多个主题中找到,以及 python-dev 邮件列表存档 [2] 2016 年 4 月的讨论中找到。

提案

此提案分为两部分。一部分是关于对象声明和支持公开文件系统路径表示的协议提案。另一部分涉及对 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__() 方法,该方法将返回路径的 strbytes 表示。 str 表示是首选的低级路径表示,因为它是人类可读的,也是人们历史上表示路径的方式。

标准库变更

预计 Python 标准库中当前接受文件系统路径的大多数 API 都将进行适当更新以接受路径对象(无论这需要代码还是仅仅更新文档都会有所不同)。但是,下面提到的模块值得特别详细说明,因为它们要么具有使能够使用路径对象成为可能的基本更改,要么涉及 API 的添加/删除。

内置函数

open() [5] 将更新为接受路径对象,并继续接受 strbytes

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] 函数将更新为接受路径对象。由于这两个函数分别将其参数强制转换为 bytesstr,因此它们将被更新为如果存在 __fspath__() 则调用它,将路径对象转换为 strbytes 表示,然后执行适当的强制转换操作,就像 __fspath__() 的返回值是该强制转换函数的原始参数一样。

os.fspath() 的添加,os.fsencode()/os.fsdecode() 的更新,以及 pathlib.PurePath 的当前语义提供了获取所需路径表示的必要语义。对于路径对象,可以使用 pathlib.PurePath/Path。要获取不带任何强制转换的 strbytes 表示,可以使用 os.fspath()。如果需要 str 并且应假定 bytes 的编码是默认文件系统编码,则应使用 os.fsdecode()。如果需要 bytes 表示并且任何字符串都应使用默认文件系统编码进行编码,则使用 os.fsencode()。本 PEP 建议尽可能使用路径对象,必要时回退到字符串路径,并最后才使用 bytes

另一种看待这个问题的方式是将其视为文件系统路径表示的层次结构(从最高到最低级别):path → str → bytes。所讨论的函数和类都可以接受同一层次结构级别的对象,但它们在是否将对象提升或降级到另一个级别方面有所不同。pathlib.PurePath 类可以将 str 提升为路径对象。os.fspath() 函数可以将路径对象降级为 strbytes 实例,具体取决于 __fspath__() 的返回值。os.fsdecode() 函数将路径对象降级为字符串或将 bytes 对象提升为 stros.fsencode() 函数将路径或字符串对象降级为 bytes。没有提供直接将路径对象降级为 bytes 并绕过字符串降级的功能。

DirEntry 对象 [8] 将获得一个 __fspath__() 方法。它将返回与 DirEntry 实例的 path 属性中当前找到的值相同的值。

协议 抽象基类将以 os.PathLike 的名称添加到 os 模块中。

os.path

os.path [9] 的各种路径操作函数将更新为接受路径对象。对于接受字节和字符串的多态函数,它们将更新为简单地使用 os.fspath()

在导致此 PEP 的讨论中,有人建议 os.path 不应使用“显式优于隐式”的论点进行更新。其想法是,由于 __fspath__() 本身是多态的,因此让处理 os.path 的代码显式地从路径对象中提取路径表示可能更好。还有一个考虑是,将支持添加得如此深入到低级 OS API 中将导致代码神奇地支持路径对象而无需任何文档更新,这可能导致在不起作用时(项目作者不知情的情况下)出现潜在的抱怨。

但本 PEP 认为在此情况下“实用胜于纯粹”。为了促进向支持路径对象的过渡,最好使过渡尽可能容易,而不是担心项目对路径对象意外/未记录的鸭子类型支持。

也有人建议 os.path 函数可以在紧密循环中使用,并且检查或调用 __fspath__() 的开销会太高。在这种情况下,只有路径消耗型 API 会直接更新,而像 os.path 中的路径操作型 API 则保持不变。这将要求库作者如果执行任何路径操作,则更新其代码以支持路径对象,但如果库代码直接传递路径,则库无需更新。然而,本 PEP 和 Guido 认为,这是一个不必要的担忧,性能仍然可以接受。

pathlib

pathlib.PurePathpathlib.Path 的构造函数将更新为接受 PathLike 对象。 PurePathPath 都将继续不接受 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 中更改的任务列表:

  1. 从 pathlib 中移除 path 属性(已完成
  2. 移除 pathlib 的临时状态(已完成
  3. 添加 os.PathLike代码文档 已完成)
  4. 添加 PyOS_FSPath()代码文档 已完成)
  5. 添加 os.fspath() (已完成)
  6. 更新 os.fsencode() (已完成)
  7. 更新 os.fsdecode() (已完成)
  8. 更新 pathlib.PurePathpathlib.Path (已完成)
    1. 添加 __fspath__()
    2. 为构造函数添加 os.PathLike 支持
  9. __fspath__() 添加到 DirEntry (已完成)
  10. 更新 builtins.open() (已完成)
  11. 更新 os.path (已完成)
  12. 为“path-like”添加 术语表 条目 (已完成)
  13. 更新 “新特性” (已完成)

被拒绝的想法

协议方法的其他名称

在导致本 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 使得可以使用 surrogateescape 处理程序用 str 表示所有文件系统路径。因此,最好强制推广使用 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

最后修改:2025-02-01 08:59:27 GMT