Python 内幕揭秘:深度刨析类 Unix 系统下的 os.path.join()

文章目录

  • 参考
  • 描述
  • os.path
      • 路径分隔符
      • os.path.join()
      • 不同实现
  • 类 Unix 下的 os.path.join()
      • os.path.join() 与 posixpath.join()
      • posixpath.join()
  • 准备工作
      • os.fspath()
      • posixpath._get_sep()
      • map()
      • startswitch() 方法及 endswitch() 方法
        • startswitch()
        • endswitch()
      • genericpath._check_arg_types()
        • genericpath 模块
        • genericpath._check_arg_types()
  • posixpath.join() 函数的源码刨析
      • posixpath.join() 的具体实现(附注释)
      • 奇怪的判断语句
        • path[:0] + sep

参考

项目 描述
Python 官方文档 https://docs.python.org/zh-cn/3/
搜索引擎 Google 、Bing
CPython 3.6 解释器源码 官方下载页面

描述

项目 描述
Windows 操作系统 Windows 10 专业版
类 Unix 操作系统 Kali Linux 2023-04-18
PyCharm 2023.1 (Professional Edition)
Python 3.10.6

os.path

os.path 模块是 Python 标准库中的一个模块,用于处理与 文件路径相关的操作,如文件路径字符串的拼接、分解、规范化。

路径分隔符

路径分隔符是用于在文件路径中 分隔不同目录层级 的特殊字符。路径分隔符是根据操作系统的约定来确定的,不同的操作系统使用不同的路径分隔符。

常见的路径分隔符有两种,正斜杠与反斜杠。

  1. 正斜杠 /
    正斜杠是在 类 Unix 操作系统中使用的路径分隔符。

  2. 反斜杠 \
    反斜杠是在 Windows 操作系统上使用的 主要 路径分隔符,在 Windows 操作系统中,你还可以使用正斜杠 / 作为路径分隔符。

os.path.join()

os.path.join() 函数是 os.path 模块中的一个常用函数,用于将多个路径片段连接起来形成一个完整的路径os.path.join 函数会根据当前操作系统的类型自动选择 合适的路径分隔符 来对路径进行拼接。

举个栗子

import os


result = os.path.join('path', 'to', 'file')
print(result)

Windows 操作系统中的执行效果

path\to\file

Linux 操作系统中的执行效果

path/to/file

不同实现

os.path.join() 函数是 Python 标准库中的一个函数,用于将多个路径组合成一个单一的路径。它可以根据操作系统的不同自动选择适当的路径分隔符(斜杠 / 或反斜杠 \)。

os.path.join()函数的实现依赖于不同的操作系统和底层文件系统。在Windows 操作系统中,os.path.join() 使用 ntpath.py 内置模块来处理路径;而在 POSIX 系统(类 Unix 系统)中,则使用 posixpath.py 内置模块来处理路径。

类 Unix 下的 os.path.join()

os.path.join() 与 posixpath.join()

POSIX 系统(类 Unix 系统)中,os.path.join() 使用 posixpath.py 内置模块来处理路径。这意味着,我们除了通过导入 os 模块来使用 os.path.join() 函数外,还可以通过导入 posixpath 直接使用 join() 函数来完成路径拼接的操作。对此,请参考如下示例:

通过 os.path.join() 实现路径拼接操作

from os.path import join


result = join('path', 'to', 'file')
print(result)

通过 posixpath.join() 实现路径拼接操作

from posixpath import join


result = join('path', 'to', 'file')
print(result)

执行效果

类 Unix 操作系统中,上述代码的执行效果一致,均为:

path/to/file

注:

  1. 并不推荐通过 from ... import join (其中,... 代表 os.pathposixpath 模块)语句直接导入 join() 函数。Python 提供了字符串对象的 join() 方法,用于将可迭代对象中的元素(可迭代对象中的元素需要为字符串)通过指定的字符串对象进行连接,如果通过 from ... import join 导入 join() 函数则容易使人将两者混淆。
    良好实践应是先将 osposixpath 模块进行导入后,再通过 os.path.join()posixpath.join() 的方式使用 join() 函数。

  2. 通过 from ... import join (其中,... 代表 os.pathposixpath 模块)语句直接导入 join() 函数并不会导致字符串对象的 join() 方法被覆盖。这是由于起路径拼接作用的 join()函数,而通过指定字符串对象将可迭代对象进行拼接的 join()方法(定义在类中的函数),Python 能够对这两者有一个很好的区分。对此,请参考如下示例:

    from os.path import join
    
    
    # 使用字符串对象的 join() 方法
    # 将可迭代对象中的元素通过指定的字符串对象进行拼接。
    arr = ['Hello', 'World']
    
    result = ' '.join(arr)
    print(result)
    
    # 通过使用 os.path 模块提供的 join() 函数将
    # 指定的多段路径进行正确的拼接。
    result = join('path', 'ro', 'file')
    print(result)
    

    执行效果

    Hello World
    path/ro/file
    

posixpath.join()

类 Unix 系统中,os.path.join() 的本质是 posixpath.join(),因此,如果需要深入研究 os.path.join() 函数的行为,你需要对 posixpath.join() 函数的源码进行探索。

posixpath.join() 的源码如下

# Join pathnames.
# Ignore the previous parts if a part is absolute.
# Insert a '/' unless the first part is empty or already ends in '/'.

def join(a, *p):
    """Join two or more pathname components, inserting '/' as needed.
    If any component is an absolute path, all previous path components
    will be discarded.  An empty last part will result in a path that
    ends with a separator."""
    a = os.fspath(a)
    sep = _get_sep(a)
    path = a
    try:
        if not p:
            path[:0] + sep  #23780: Ensure compatible data type even if p is null.
        for b in map(os.fspath, p):
            if b.startswith(sep):
                path = b
            elif not path or path.endswith(sep):
                path += b
            else:
                path += sep + b
    except (TypeError, AttributeError, BytesWarning):
        genericpath._check_arg_types('join', a, *p)
        raise
    return path

准备工作

os.fspath()

os.fspath() 接受一个对象作为实参,并尝试返回表示 文件系统路径字符串字节串 对象。如果传递给 os.fspath() 函数的是 strbytes 类型的对象,则该对象将被原样返回。否则实参对象的 __fspath__() 方法将被调用,如果 __fspath__() 方法返回的不是一个 strbytes 类型的对象,则该方法将抛出 TypeError 异常。

举个栗子

from os import fspath


class MyPath:
    def __fspath__(self):
        return '/path/to/file'


result = fspath(MyPath())
print(result)

print(fspath('Hello World'))
print(fspath(b'Hello World'))

执行效果

/path/to/file
Hello World
b'Hello World'

注:

该函数在 Python 3.6 及以上版本可用,在使用该函数前,请检查你所使用的 Python 版本。

posixpath._get_sep()

posixpath._get_sep()

def _get_sep(path):
    if isinstance(path, bytes):
        return b'/'
    else:
        return '/'

posixpath._get_sep() 函数接受一个参数 path。如果 path 是 bytes 类型,则函数返回 bytes 类型的路径分隔符 b'/';如果 path 是 str 类型,则函数返回字符串类型的路径分隔符 '/'

注:

在 Python 中,以 单个下划线开头 的函数或方法通常被视为 内部实现细节,不是 公共 API 的一部分。这意味着它们不受官方支持,不建议直接使用,并且在未来的 Python 版本中可能发生更改。

map()

map(self, func, *iterables)

在 Python 中,map() 是一个内置函数,用于将一个函数应用于一个或多个可迭代对象的每个元素。该函数将返回一个新的迭代器对象,其中包含应用函数后的结果。

举个栗子

# 传递给 map() 函数中的第一个可迭代对象中的元素将赋予 element1
# 传递给 map() 函数中的第二个可迭代对象中的元素将赋予 element2
def double(element1, element2):
    return element1 + element2


arr = list('Hello World')
arr1 = reversed(arr)

result = map(double, arr, arr1)
# 返回一个迭代器对象
print(result)

for r in result:
    print(r)

执行效果

<map object at 0x00000137BFB8BF10>
Hd
el
lr
lo
oW
  
Wo
ol
rl
le
dH

startswitch() 方法及 endswitch() 方法

在 Python 中,startswith()endswith()是字符串对象的内置方法,用于检查字符串是否具有指定的前缀或后缀。

startswitch()

startswith(prefix[, start[, end]])

startswitch() 方法用于检查字符串是否以指定的前缀 prefix 开始,该方法具有 startend 两个可选参数,用于指定要检查的 子字符串 的起始和结束 索引
startswitch() 方法的返回值为布尔类型,如果字符串以指定的 前缀 开始,则返回 True,否则将返回 False

endswitch()

endswith(suffix[, start[, end]])

endswitch() 方法用于检查字符串是否以指定的前缀 suffix 结尾,该方法具有 startend 两个可选参数,用于指定要检查的 子字符串 的起始和结束 索引
endswitch() 方法的返回值为布尔类型,如果字符串以指定的 后缀 结尾,则返回 True,否则将返回 False

genericpath._check_arg_types()

genericpath 模块

genericpath 模块是 Python 个内置模块 os.path 中的一个子模块,该模块提供了一些 用于处理路径的通用函数和工具

genericpath 模块中定义的函数主要用于路径处理的 通用 操作,不涉及特定的操作系统。这些函数可以在不同的操作系统上使用,因为它们不依赖于特定的路径分隔符或操作系统特定的文件系统规则。

genericpath._check_arg_types()

posixpath._get_sep() 函数相同,genericpath._check_arg_types() 也是一个 名义上的私有函数(用于内部实现,但可供外部直接使用),不建议直接使用。

genericpath._check_arg_types() 函数的源码如下:

def _check_arg_types(funcname, *args):
    hasstr = hasbytes = False
    for s in args:
        if isinstance(s, str):
            hasstr = True
        elif isinstance(s, bytes):
            hasbytes = True
        else:
            raise TypeError(f'{funcname}() argument must be str, bytes, or '
                            f'os.PathLike object, not {s.__class__.__name__!r}') from None
    if hasstr and hasbytes:
        raise TypeError("Can't mix strings and bytes in path components") from None

os.path 内部,该函数常用于检查一个函数的一个或多个参数是否是以 bytesstr 类型表示的文件系统路径。若 genericpath._check_arg_types() 函数中的可迭代对象 args 中存在除 bytesstr 类型的元素或是同时存在 bytesstr 类型的元素,该函数将抛出 TypeError 异常。

posixpath.join() 函数的源码刨析

posixpath.join() 的具体实现(附注释)

def join(a, *p):
    # 通过 fspath 将 a 转换为 str 或 bytes
    # 类型表示的文件系统路径。
    a = os.fspath(a)

    # a 属于 str 或 bytes 中的哪一种类型,返回
    # 该类型所对应的路径分隔符,'/' 或 b'/' 。
    sep = _get_sep(a)

    # path 表示结果路径,即路径拼接的结果值
    path = a

    try:

        # 这个判断语句恕在下不能理解,(╯°□°)╯︵ ┻━┻
        if not p:
            path[:0] + sep

        # 对可变参数 p 中的每一个元素应用 os.fspath 函数
        for b in map(os.fspath, p):

            # 判断 b 是否以路径分隔符为前缀。
            # 若是,则将 path 清空并赋值为 b
            if b.startswith(sep):
                path = b

            elif not path or path.endswith(sep):
                # 判断 path 是否为空或以路径分隔符结尾。
                # 若是,则将 path 与 b 直接进行拼接。
                path += b
            else:
                # 若上述条件都不满足,则将路径分隔符作为
                # b 的前缀并将结果拼接至 path 中
                path += sep + b
                
    except (TypeError, AttributeError, BytesWarning):
        # 尝试使用 genericpath._check_arg_types() 函数
        # 判断产生异常错误的原因,以输出适当的错误信息帮助用户排错。
        genericpath._check_arg_types('join', a, *p)

        # 若 genericpath._check_arg_types() 函数
        # 为检测到错误产生的原因并将其抛出,则抛出截获到的异常错误
        raise

    # 返回路径拼接的结果值
    return path

奇怪的判断语句

posixpath.join() 函数的源代码中,下面的这个判断语句显得有些多余。

if not p:
    path[:0] + sep #23780: Ensure compatible data type even if p is null.

path[:0] + sep

if 中的 path[:0] + sep 语句并未将拼接的结果进行保存,这是因为列表对象的 切片操作 返回的是一个新的列表对象,它是原始列表的一个子集。修改这个切片实际上是在修改新创建的列表对象,而不是原始列表。那么,path[:0] + sep 的作用是什么?

观察 path[:0] + sep 语句旁边的注释 #23780: Ensure compatible data type even if p is null.,翻译翻译得到:#23780: 即使 p 为空,也要确保数据类型兼容。。也就是说, path[:0] + sep 能够保证 path[:0] 的数据类型为 strbytes 中的其中一种。让我们对此验证一番:

string = 'Hello World'
bytes_string = b'Hello World'
arr = [1, 2, 3]

# 即使 [:0] 无法从序列中获取到任何元素
# 但 [:0] 仍将返回一个空字符串、空字节串或空列表等。
print(string[:0])
print(bytes_string[:0])
print(arr[:0])
print(type(string[:0]))
print(type(bytes_string[:0]))
print(type(arr[:0]))

# arr[:0] + '/' 的结果并不会保存在 arr 中
# 但,当两着进行加法操作时,若两者的类型不支持进行
# 加法操作,则 Python 将抛出 TypeError 异常错误。
try:
    arr[:0] + '/'
except TypeError:
    print('TypeError')

执行效果


b''
[]
<class 'str'>
<class 'bytes'>
<class 'list'>
TypeError

结果表明 path[:0] + sep 将在两者不支持作为加法操作符的操作数时产生 TypeError 异常,并且产生的异常错误将被 posixpath.join() 中的 except (TypeError, AttributeError, BytesWarning) 所捕获。这对 path[:0] 的数据类型是 strbytes 提供了保障。但令人匪夷所思的是,os.fspath(a) 就足以保证 a 的数据类型为 strbytes 中的一种。

def join(a, *p):
	a = os.fspath(a)
	sep = _get_sep(a)
	path = a
	
	try:
	    if not p:
	        path[:0] + sep

你可能感兴趣的:(Python,UNIX,Like,源码刨析,类,Unix,Python,os,posixpath)