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

Python 增强提案

PEP 355 – Path - 面向对象的 文件系统路径

作者:
Björn Lindqvist <bjourne at gmail.com>
状态:
已拒绝
类型:
标准跟踪
创建:
2006年1月24日
Python 版本:
2.5
历史记录:


目录

拒绝通知

此 PEP 已被拒绝(以这种形式)。提出的路径类是最终的万能工具;但认为将使用路径的所有功能都作为单个类上的方法来实现是一种反模式。(例如,为什么不使用 open()?或者 execfile()?)从 str 继承是一个特别糟糕的主意;许多字符串操作应用于路径时毫无意义。此 PEP 已经存在很长时间了,虽然讨论不时会重新燃起,但现在是结束此 PEP 的痛苦的时候了。一个不太牵强的提案可能会更容易被接受。

摘要

此 PEP 描述了一个新的类 Path,该类将添加到 os 模块中,用于以面向对象的方式处理路径。“弱”弃用各种相关函数也将在本文中讨论并推荐。

背景

此 PEP 中表达的想法并非最近才出现,而是多年来在 Python 社区中一直争论不休的话题。许多人认为,os.path 模块中提供的用于操作文件路径的 API 不够完善。Just van Rossum 在 2001 年的 python-dev 上提出了第一个关于 Path 对象的提案 [2]。2003 年,Jason Orendorff 发布了“path 模块”的 1.0 版本,这是第一个使用对象表示路径的公开实现 [3]

path 模块很快变得非常流行,并且人们多次尝试将 path 模块包含到 Python 标准库中;[4][5][6][7]

此 PEP 总结了人们对 path 模块提出的想法和建议,并建议将修改后的版本包含到标准库中。

动机

处理文件系统路径是任何编程语言中的一项常见任务,在像 Python 这样的高级语言中非常常见。需要对这项任务提供良好的支持,因为

  • 几乎每个程序都使用路径来访问文件。一项如此频繁执行的任务,理应尽可能直观且易于执行。
  • 它使 Python 成为更优秀的复杂 shell 脚本替换语言。

目前,Python 有大量的不同函数散布在六个模块中用于处理路径。这使得新手和经验丰富的开发人员难以选择正确的方法。

Path 类相较于当前的常用做法提供了以下增强功能

  • 一个“统一”的对象提供了之前所有函数的功能。
  • 可继承性 - Path 对象可以扩展以支持除文件系统路径之外的其他路径。程序员不需要学习新的 API,而是可以重用他们对 Path 的了解来处理扩展类。
  • 将所有相关功能集中在一个地方,更容易学习正确的方法,因为无需在许多不同的模块中搜索正确的函数。
  • Python 是一种面向对象的语言。就像文件、日期时间和套接字一样,路径也是对象,它们不仅仅是传递给函数的字符串。Path 对象本身就是一个 Pythonic 的理念。
  • Path 利用了属性。属性使代码更具可读性
    if imgpath.ext == 'jpg':
        jpegdecode(imgpath)
    

    优于

    if os.path.splitexit(imgpath)[1] == 'jpg':
        jpegdecode(imgpath)
    

基本原理

以下几点总结了设计

  • Path 从字符串继承,因此所有期望字符串路径名的代码都不需要修改,并且不会破坏任何现有代码。
  • 可以通过使用类方法 Path.cwd 创建 Path 对象,也可以通过用表示路径的字符串实例化类来创建,或者通过使用默认构造函数来创建,该构造函数等效于 Path(".")
  • Path 提供了常见的路径名操作、模式扩展、模式匹配和其他高级文件操作,包括复制。基本上,Path 提供了所有与路径相关的内容,除了文件内容的操作,因为文件对象更适合于此。
  • 通过不实例化特定于系统的函数来处理平台不兼容性。

规范

此类定义了以下公共接口(文档字符串已从参考实现中提取,并为了简洁起见进行了缩短;有关更多详细信息,请参阅参考实现)

class Path(str):

    # Special Python methods:
    def __new__(cls, *args) => Path
        """
        Creates a new path object concatenating the *args.  *args
        may only contain Path objects or strings.  If *args is
        empty, Path(os.curdir) is created.
        """
    def __repr__(self): ...
    def __add__(self, more): ...
    def __radd__(self, other): ...

    # Alternative constructor.
    def cwd(cls): ...

    # Operations on path strings:
    def abspath(self) => Path
        """Returns the absolute path of self as a new Path object."""
    def normcase(self): ...
    def normpath(self): ...
    def realpath(self): ...
    def expanduser(self): ...
    def expandvars(self): ...
    def basename(self): ...
    def expand(self): ...
    def splitpath(self) => (Path, str)
        """p.splitpath() -> Return (p.parent, p.name)."""
    def stripext(self) => Path
        """p.stripext() -> Remove one file extension from the path."""
    def splitunc(self): ...  # See footnote [1]
    def splitall(self): ...
    def relpath(self): ...
    def relpathto(self, dest): ...

    # Properties about the path:
    parent => Path
        """This Path's parent directory as a new path object."""
    name => str
        """The name of this file or directory without the full path."""
    ext => str
        """
        The file extension or an empty string if Path refers to a
        file without an extension or a directory.
        """
    drive => str
        """
        The drive specifier.  Always empty on systems that don't
        use drive specifiers.
        """
    namebase => str
        """
        The same as path.name, but with one file extension
        stripped off.
        """
    uncshare[1]

    # Operations that return lists of paths:
    def listdir(self, pattern = None): ...
    def dirs(self, pattern = None): ...
    def files(self, pattern = None): ...
    def walk(self, pattern = None): ...
    def walkdirs(self, pattern = None): ...
    def walkfiles(self, pattern = None): ...
    def match(self, pattern) => bool
        """Returns True if self.name matches the given pattern."""

    def matchcase(self, pattern) => bool
        """
        Like match() but is guaranteed to be case sensitive even
        on platforms with case insensitive filesystems.
        """
    def glob(self, pattern):

    # Methods for retrieving information about the filesystem
    # path:
    def exists(self): ...
    def isabs(self): ...
    def isdir(self): ...
    def isfile(self): ...
    def islink(self): ...
    def ismount(self): ...
    def samefile(self, other): ...  # See footnote [1]
    def atime(self): ...
        """Last access time of the file."""
    def mtime(self): ...
        """Last-modified time of the file."""
    def ctime(self): ...
        """
        Return the system's ctime which, on some systems (like
        Unix) is the time of the last change, and, on others (like
        Windows), is the creation time for path.
        """
    def size(self): ...
    def access(self, mode): ...  # See footnote [1]
    def stat(self): ...
    def lstat(self): ...
    def statvfs(self): ...  # See footnote [1]
    def pathconf(self, name): ...  # See footnote [1]

    # Methods for manipulating information about the filesystem
    # path.
    def utime(self, times) => None
    def chmod(self, mode) => None
    def chown(self, uid, gid) => None # See footnote [1]
    def rename(self, new) => None
    def renames(self, new) => None

    # Create/delete operations on directories
    def mkdir(self, mode = 0777): ...
    def makedirs(self, mode = 0777): ...
    def rmdir(self): ...
    def removedirs(self): ...

    # Modifying operations on files
    def touch(self): ...
    def remove(self): ...
    def unlink(self): ...

    # Modifying operations on links
    def link(self, newpath): ...
    def symlink(self, newlink): ...
    def readlink(self): ...
    def readlinkabs(self): ...

    # High-level functions from shutil
    def copyfile(self, dst): ...
    def copymode(self, dst): ...
    def copystat(self, dst): ...
    def copy(self, dst): ...
    def copy2(self, dst): ...
    def copytree(self, dst, symlinks = True): ...
    def move(self, dst): ...
    def rmtree(self, ignore_errors = False, onerror = None): ...

    # Special stuff from os
    def chroot(self): ...  # See footnote [1]
    def startfile(self): ...  # See footnote [1]

使用 Path 类替换旧函数

在本节中,“a ==> b”表示 b 可以用作 a 的替代。

在以下示例中,我们假设 Path 类已使用 from path import Path 导入。

  • 替换 os.path.join
    os.path.join(os.getcwd(), "foobar")
    ==>
    Path(Path.cwd(), "foobar")
    
    os.path.join("foo", "bar", "baz")
    ==>
    Path("foo", "bar", "baz")
    
  • 替换 os.path.splitext
    fname = "Python2.4.tar.gz"
    os.path.splitext(fname)[1]
    ==>
    fname = Path("Python2.4.tar.gz")
    fname.ext
    

    或者如果想要两个部分

    fname = "Python2.4.tar.gz"
    base, ext = os.path.splitext(fname)
    ==>
    fname = Path("Python2.4.tar.gz")
    base, ext = fname.namebase, fname.extx
    
  • 替换 glob.glob
    lib_dir = "/lib"
    libs = glob.glob(os.path.join(lib_dir, "*s.o"))
    ==>
    lib_dir = Path("/lib")
    libs = lib_dir.files("*.so")
    

弃用

将此模块引入标准库需要“弱”弃用一些现有的模块和函数。这些模块和函数使用非常广泛,因此无法真正弃用,例如生成 DeprecationWarning。此处“弱弃用”仅表示文档中的注释。

下表列出了应弃用的现有功能。

Path 方法/属性 弃用函数
normcase() os.path.normcase()
normpath() os.path.normpath()
realpath() os.path.realpath()
expanduser() os.path.expanduser()
expandvars() os.path.expandvars()
parent os.path.dirname()
name os.path.basename()
splitpath() os.path.split()
drive os.path.splitdrive()
ext os.path.splitext()
splitunc() os.path.splitunc()
__new__() os.path.join(), os.curdir
listdir() os.listdir() [fnmatch.filter()]
match() fnmatch.fnmatch()
matchcase() fnmatch.fnmatchcase()
glob() glob.glob()
exists() os.path.exists()
isabs() os.path.isabs()
isdir() os.path.isdir()
isfile() os.path.isfile()
islink() os.path.islink()
ismount() os.path.ismount()
samefile() os.path.samefile()
atime() os.path.getatime()
ctime() os.path.getctime()
mtime() os.path.getmtime()
size() os.path.getsize()
cwd() os.getcwd()
access() os.access()
stat() os.stat()
lstat() os.lstat()
statvfs() os.statvfs()
pathconf() os.pathconf()
utime() os.utime()
chmod() os.chmod()
chown() os.chown()
rename() os.rename()
renames() os.renames()
mkdir() os.mkdir()
makedirs() os.makedirs()
rmdir() os.rmdir()
removedirs() os.removedirs()
remove() os.remove()
unlink() os.unlink()
link() os.link()
symlink() os.symlink()
readlink() os.readlink()
chroot() os.chroot()
startfile() os.startfile()
copyfile() shutil.copyfile()
copymode() shutil.copymode()
copystat() shutil.copystat()
copy() shutil.copy()
copy2() shutil.copy2()
copytree() shutil.copytree()
move() shutil.move()
rmtree() shutil.rmtree()

Path 类弃用了整个 os.pathshutilfnmatchglob。还弃用了 os 的很大一部分。

已解决的问题

自从此 PEP 首次出现在 python-dev 上以来,一些有争议的问题已得到解决

  • 已删除 __div__() 方法。重载 /(除法)运算符可能是“过于神奇”,并使路径连接看起来像是除法。如果 BDFL 愿意,该方法始终可以稍后重新添加。取而代之的是,__new__() 获得了一个 *args 参数,该参数既接受 Path 也接受字符串对象。*argsos.path.join() 连接,后者用于构造 Path 对象。这些更改使有问题的 joinpath() 方法过时,该方法已被删除。
  • 方法和属性 getatime()/atimegetctime()/ctimegetmtime()/mtimegetsize()/size 彼此重复。这些方法和属性已合并为 atime()ctime()mtime()size()。它们不是属性的原因是,它们有可能意外地发生变化。以下示例不能保证始终通过断言
    p = Path("foobar")
    s = p.size()
    assert p.size() == s
    

未解决的问题

已省略 Jason Orendorff 的 path 模块的一些功能

  • 用于打开路径的函数 - 内置的 open() 处理得更好。
  • 读取和写入整个文件的函数 - 最好由文件对象自身的 read()write() 方法处理。
  • 一个 chdir() 函数可能值得包含。
  • 需要制定一个弃用计划。 Path 应该实现多少功能?它应该在何时弃用多少现有功能?
  • 名称显然必须是“path”或“Path”,但它应该放在哪里?在它自己的模块中还是在 os 中?
  • 由于 Path 继承自 strunicode,因此以下非魔术公共方法可在 Path 对象上使用
    capitalize(), center(), count(), decode(), encode(),
    endswith(), expandtabs(), find(), index(), isalnum(),
    isalpha(), isdigit(), islower(), isspace(), istitle(),
    isupper(), join(), ljust(), lower(), lstrip(), replace(),
    rfind(), rindex(), rjust(), rsplit(), rstrip(), split(),
    splitlines(), startswith(), strip(), swapcase(), title(),
    translate(), upper(), zfill()
    

    在 python-dev 上,人们争论过这种继承是否合理。大多数参与讨论的人认为,大多数字符串方法在文件系统路径的上下文中没有意义——它们只是累赘。另一个在 python-dev 上提出的观点是,继承自字符串非常方便,因为它允许代码“正常工作”与 Path 对象,而无需为它们进行适配。

    其中一个问题是,在 Python 层面,没有办法使一个对象“足够像字符串”,以便可以将其传递给内置函数 open()(以及其他期望字符串或缓冲区的内置函数),除非该对象继承自 strunicode。因此,要避免继承自字符串,需要更改 CPython 的核心。

此新模块试图替换的函数和模块(os.pathshutilfnmatchglobos 的部分)预计将在未来的 Python 版本中长期可用,以保持向后兼容性。

参考实现

目前,Path 类被实现为标准库模块 fnmatchglobosos.pathshutil 的一个薄包装器。本 PEP 的目的是在这些模块被弃用时,将功能从上述模块迁移到 Path

有关更多详细信息和实现,请参阅

示例

在本节中,“a ==> b”表示 b 可以用作 a 的替代。

  • 使目录中所有 Python 文件可执行
    DIR = '/usr/home/guido/bin'
    for f in os.listdir(DIR):
        if f.endswith('.py'):
            path = os.path.join(DIR, f)
            os.chmod(path, 0755)
    ==>
    for f in Path('/usr/home/guido/bin').files("*.py"):
        f.chmod(0755)
    
  • 删除 Emacs 备份文件
    def delete_backups(arg, dirname, names):
        for name in names:
            if name.endswith('~'):
                os.remove(os.path.join(dirname, name))
    os.path.walk(os.environ['HOME'], delete_backups, None)
    ==>
    d = Path(os.environ['HOME'])
    for f in d.walkfiles('*~'):
        f.remove()
    
  • 查找文件的相对路径
    b = Path('/users/peter/')
    a = Path('/users/peter/synergy/tiki.txt')
    a.relpathto(b)
    
  • 将路径拆分为目录和文件名
    os.path.split("/path/to/foo/bar.txt")
    ==>
    Path("/path/to/foo/bar.txt").splitpath()
    
  • 列出当前目录树中的所有 Python 脚本
    list(Path().walkfiles("*.py"))
    

参考文献和脚注

[1] 方法不能保证在所有平台上都可用。


来源:https://github.com/python/peps/blob/main/peps/pep-0355.rst

上次修改时间:2023-09-09 17:39:29 GMT