Python标准库阅读系列—OS库(二)

def makedirs(name, mode=0o777, exist_ok=False):
    head, tail = path.split(name)
    if not tail:
        head, tail = path.split(head)
    if head and tail and not path.exists(head):
        try:
            makedirs(head, exist_ok=exist_ok)
        except FileExistsError:
            # Defeats race condition when another thread created the path
            pass
        cdir = curdir
        if isinstance(tail, bytes):
            cdir = bytes(curdir, 'ASCII')
        if tail == cdir:           
            return
    try:
        mkdir(name, mode)
    except OSError:
        if not exist_ok or not path.isdir(name):
            raise

makedirs函数:递归目录创建函数。这是对mkdir的封装,其功能是一次性创建一个子目录和所有的中间目录。类似linux命令里面 mkdir -p(或–parents) 的功能。传入的 name 为文件路径(不能包含包含 pardir ,如 UNIX 系统中的 “..”); mode为数字权限默认0o777(基本没法改); exist_ok 当设为 False (默认值)时会直接抛出 FileExistsError 异常,设为 True 时会判断传入 name 是否为现有路径,若不是则抛出 FileExistsError 异常(总感觉这个设置的有点奇怪,也不清楚是不是我解读的问题)。

path.split()函数:这个函数的作用是将传入的路径进行分割,以最后一个“/”为分界线,分别赋值给head和tail。

当tail为空时再分割一次,考虑到传入路径结尾为”/“的情况。

path.exists()函数:该函数用于判断当前路径是否存在,若存在则返回True,否则返回False。在这里python利用异常捕获机制不断的递归调用maksdirs函数,直到文件路径不存在引发异常报错。相对于使用if…elif…else来判断,代码的执行效率更高了。

curdir函数:用于获取当前执行python文件的文件夹,注意获取的是当前执行python文件的文件夹,而不是python文件所在的文件夹。curdir会返回一个”.“(表示这个表示当前路径),所以此刻cdir变量内储存的是”.” 当前执行python文件的路径。

isinstance()函数:判断一个对象是否是一个已知的类型,类似 type(),但其会考虑到继承关系这一点与type()不相同。这里用于判断tail和bytes(字节,二进制数据流类型) 是否类型相同,是则返回True。当tail为nce()函数:判断一个对象是否是一个已知的类型,类似 type(),但其会考虑到继承关系这一点与type()不相同。这里用于判断tail和bytes(字节,二进制数据流类型) 是否类型相同,是则返回True。当tail为字节类型的时候,以”ASCII“编码方式将curdir对象重新创造一个bytes类对象。

这里一定要判断tail为字节类型的原因是,在Python3中bytes和str类型有着严格的区分。

Python 3最重要的新特性之一是对字符串和二进制数据流做了明确的区分。文本总是 Unicode,由 str 类型表示,二进制数据则由 bytes 类型表示。Python 3 不会以任意隐式的方式混用 str 和 bytes ,你不能拼接字符串和字节流,也无法在字节流里搜索字符串(反之亦然),也不能将字符串传入参数为字节流的函数(反之亦然)。

传入的文件路径既可能是 str 类型也可能是 bytes 类型。虽然bytes类型 ”D:\pythons\Jupyter“ 和str类型 ”D:\pythons\Jupyter“ 存储的是相同的路径,但是两者并不相同。为了使程序能正常return跳出递归,而不继续运行导致mkdir报错。可见开发这一块,细节决定成败。(tail始终存的是单个目录名啊,怎么可能和路径名相同。这里我想破脑壳都没想懂,以后弄明白了再来修改吧。这是我是根据前后文推断的,功能上因该没啥问题。)

mkdir()函数:数字权限创建name目录方法,可能会抛出异常 OSError (当name目录存在的时候),但是会被捕获。

当捕获OSError后,判断exist_ok属性和name是否为已存在目录(path.isdir()函数功能,若存在则返回True)。如果exist_ok为False或者name为不存在的目录,则抛出 FiloExistsError 异常(前面利用异常检测机制进行的递归可以用上了)。

raise函数:单独一个 raise。该语句引发当前上下文中捕获的异常(比如在 except 块中),或默认引发 RuntimeError 异常。在这里会抛出 FiloExistsError 异常。

下面是我绘制的一个流程图,可能因为我对python异常处理机制的不够理解,存在一定的误差,不过大体上应该是相似的。(请忽视我那智障的递归表示,赣)

def removedirs(name):
    rmdir(name)
    head, tail = path.split(name)
    if not tail:
        head, tail = path.split(head)
    while head and tail:
        try:
            rmdir(head)
        except OSError:
            break
        head, tail = path.split(head)

removedirs()函数:递归删除目录函数。先对传入的目录删除一次,如果删除失败会由rmdir引发报错。然后对目录进行拆分,排除传入目录最后以“/”结尾情况。然后向前删除空目录,当删除的目录不是空目录时报错退出。显然该函数只能用于向前删除空目录至非空目录为止,用到的地方大概不会多了。

rmdir(path)函数:移除(删除)目录 path。如果目录不存在或不为空,则会分别抛出 FileNotFoundError 或 OSError 异常。

def renames(old, new):
    head, tail = path.split(new)
    if head and tail and not path.exists(head):
        makedirs(head)
    rename(old, new)
    head, tail = path.split(old)
    if head and tail:
        try:
            removedirs(head)
        except OSError:
            pass

__all__.extend(["makedirs", "removedirs", "renames"])

renames()函数:递归重命名目录或文件函数。采用先创建后删除的方式。old :要重命名的目录,new :文件或目录的新名字。甚至可以是包含在目录中的文件,或者完整的目录树。

该函数先拆分新目录,然后调用 makedirs()函数(前面讲过的)创建目录。再调用raname()函数将 old 重命名为 new 。再拆分old,调用 removedirs() 函数(前面讲过的)移除原目录。

rename(src,dst)函数:将文件或目录 src 重命名为 dst。如果 dst 已存在,则一定情况下将会操作失败,并抛出 OSError 的子类。(这里省略了一部分,如果想要了解更多,请移步python3.9官方文档

我认为有必要再 rename() 函数那增加一个异常捕获机制,因为这个函数本身是先调用 makedirs() 创建新目录,然后再重命名,最后再调用 removedirs() 函数删除原路径。(不太清楚这个rename的意义何在,等想通了在完整表述。)rename是会引发异常的,如果在运行过程中发生异常,虽然程序终止了,但是 makedirs() 创建的目录最为中间件却是存在了的。同时,这个函数本身并不安全,它可以在禁用 mkdir 的情况下,用来创建文件夹。

def walk(top, topdown=True, onerror=None, followlinks=False):
    sys.audit("os.walk", top, topdown, onerror, followlinks)
    return _walk(fspath(top), topdown, onerror, followlinks)

def _walk(top, topdown, onerror, followlinks):
    dirs = []
    nondirs = []
    walk_dirs = []
    try:
        scandir_it = scandir(top)
    except OSError as error:
        if onerror is not None:
            onerror(error)
        return
    with scandir_it:
        while True:
            try:
                try:
                    entry = next(scandir_it)
                except StopIteration:
                    break
            except OSError as error:
                if onerror is not None:
                    onerror(error)
                return
            try:
                is_dir = entry.is_dir()
            except OSError:
                is_dir = False
            if is_dir:
                dirs.append(entry.name)
            else:
                nondirs.append(entry.name)
            if not topdown and is_dir:
                # Bottom-up: recurse into sub-directory, but exclude symlinks to
                # directories if followlinks is False
                if followlinks:
                    walk_into = True
                else:
                    try:
                        is_symlink = entry.is_symlink()
                    except OSError:
                        is_symlink = False
                    walk_into = not is_symlink
                if walk_into:
                    walk_dirs.append(entry.path)
    if topdown:
        yield top, dirs, nondirs
        islink, join = path.islink, path.join
        for dirname in dirs:
            new_path = join(top, dirname)
            if followlinks or not islink(new_path):
                yield from _walk(new_path, topdown, onerror, followlinks)
    else:
        for new_path in walk_dirs:
            yield from _walk(new_path, topdown, onerror, followlinks)
        yield top, dirs, nondirs

__all__.append("walk")

python3.9的 os.walk() 函数实现与前面python版本存在些许不同,3.9中将大量内容写到了另一个os._walk() 的函数中,进行了一次重写。

os.walk(top, topdown=True, onerror=None, followlinks=False)函数:生成目录树中的文件名,方式是按上->下或下->上顺序浏览目录树。对于以 top 为根的目录树中的每个目录(包括 top 本身),它都会生成一个三元组 (dirpath, dirnames, filenames)。

top:需要遍历的目录的地址,返回一个三元组(dirpath, dirnames, filenames)。

dirpath:指当前遍历的这个文件夹本身的地址,字符串

dirnames:一个list,内容是top文件子目录名称组成的列表。

filennames:一个list,是该文件中非目录文件名称组成的列表。

topdown:默认为True,可选。当为True时优先遍历根目录,否则先遍历子目录(感觉像深度遍历)。无论 topdown 为何值,在生成目录及其子目录的元组之前,都将检索全部子目录列表。

onerror:需要一个callable对象,可选。当walk需要异常时调用。默认将忽略 scandir() 调用中的错误。

followlinks:walk() 默认不会递归进指向目录的符号链接。可以在支持符号链接的系统上将followlinks 设置为 True,以访问符号链接指向的目录。也就是当其值为True时会遍历目录下的快捷方式(linux 下是软连接 symbolic link )实际所指的目录(默认关闭),如果为 False,则优先遍历 top 的子目录。

sys.audit():引发一个审计事件并触发任何激活的审计钩子。(暂时不明白这个地方的作用,不过这个应该和walk重构有很大原因)

fspath()函数:返回路径的文件系统表示。确保传入的是 str 或 bytes 类型的字符串。如果不是将抛出 TypeError 异常。

walk() 先借助 sys.audit() 触发监听,以实现多次调用 walk()。然后再返回 _walk() 函数的结果。

_walk()函数:传入参数与walk相同。是对先前版本 walk() 函数的大部分功能的重新整合。

scandir(path)函数:返回一个 os.DirEntry 对象的迭代器,它们对应于由 path 指定目录中的条目。在这里使用它获取top的DirEntry迭代器对象,储存在scandir_it中。它非常轻巧方便。例如用scandir函数遍历当前目录。

在Python 3.5版本中,新添加了os.scandir()方法,它是一个目录迭代方法。你可以在PEP 471中找到关于它的一些内容。在Python 3.5中,os.walk是使用os.scandir来实现的,根据Python 3.5宣称,它在POSIX系统中,执行速度提高了3到5倍;在Windows系统中,执行速度提高了7到20倍。它返回的是一个DirEntry对象,用它的name属性获取到文件名,以及is_dir方法判断是文件还是目录。

程序通过is_dir()等方法判断归类到不同的列表中,然后再根据topdown参数的值决定是否递归到子目录,如果followlinks为False则将符号链接排除。注意符号链接并不是快捷方式,两者功能类似,但并不相同。

符号链接与快捷方式:快捷方式(shortcut)是一种功能上类似符号链接的文件对象,但与符号链接有本质的不同。快捷方式是普通的文件(拥有扩展名 .lnk),而非符号,因此,快捷方式可以被复制、移动、更改(某些特殊的快捷方式无法更改所有信息)或删除。快捷方式可以指向文件、文件夹或其他任何系统中合法的位置(包括控制面板、桌面等)。

当 scandir() 函数触发异常后,判断 onerror 是否为空,如果非空(有callable对象),就将异常作为参数传给 callable 对象进行处理。

由于scandir_it是一个迭代器,所以使用with语句进行上下文管理。(for的执行时间太长了,不如with简洁)。其中 entry.is_dir() 表示是否为文件夹,返回 True 或者 False ;entry.name 返回当前对象名称;entry.is_symlink() 表示对象是否为符号链接,返回 True 或者 False ;entry.path 返回当前对象的地址。

_walk函数中对迭代对象处理的一部分程序的流程图(排列有点乱)。可以供大家理清一下思路。

再判断_walk是否优先遍历top目录,这一段的作用就是以递归的方式对根或者子目录进行访问。到这里我们可以很明显的看出 walk() 函数其实是对 scandir() 函数的一个封装,牺牲了其的效率以换取更大的便利性。我认为,当在开发中使用时,应直接使用 scandir() 函数,毕竟它是用C写的。

再说回来,回到上文中提到以递归的方式对根或者子目录进行访问。首先是判断是否优先遍历根目录,若是则先调用 yield 产生三个generator。

(简单地讲,yield 的作用就是把一个函数变成一个 generator (生成器),带有 yield 的函数不再是一个普通函数,Python 解释器会将其视为一个 generator。也就是将 top、dirs、nondirs 转化成generator。)

islink, join = path.islink, path.join :表示将 path.islink(path)(判断目录path是否存在)保存到 islink 中,将 path.join(path,new)(将path与new连接起来)保存到join中。

然后再从 dirs 列表中取出目录名,将其和它们的根目录连接起来,形成新的链接。再确定运行访问符号链接或者不是已存在路径,调用 yields from 语句迭代调用处理 _walk()。

若是优先访问子目录,就从 walk_dirs 列表中取出子目录路径, 调用 yields from 语句迭代调用处理 _walk()。调用完后再调用 yields 将 top、dirs、nondirs 标记为生成器对象。

关于 yields from 语句的解读,大家可以去看这篇文章:深入理解Python的yield from语法

if {open, stat} <= supports_dir_fd and {scandir, stat} <= supports_fd:

    def fwalk(top=".", topdown=True, onerror=None, *, follow_symlinks=False, dir_fd=None):
        sys.audit("os.fwalk", top, topdown, onerror, follow_symlinks, dir_fd)
        if not isinstance(top, int) or not hasattr(top, '__index__'):
            top = fspath(top)
        if not follow_symlinks:
            orig_st = stat(top, follow_symlinks=False, dir_fd=dir_fd)
        topfd = open(top, O_RDONLY, dir_fd=dir_fd)
        try:
            if (follow_symlinks or (st.S_ISDIR(orig_st.st_mode) and
                                    path.samestat(orig_st, stat(topfd)))):
                yield from _fwalk(topfd, top, isinstance(top, bytes),
                                  topdown, onerror, follow_symlinks)
        finally:
            close(topfd)

    def _fwalk(topfd, toppath, isbytes, topdown, onerror, follow_symlinks):
        scandir_it = scandir(topfd)
        dirs = []
        nondirs = []
        entries = None if topdown or follow_symlinks else []
        for entry in scandir_it:
            name = entry.name
            if isbytes:
                name = fsencode(name)
            try:
                if entry.is_dir():
                    dirs.append(name)
                    if entries is not None:
                        entries.append(entry)
                else:
                    nondirs.append(name)
            except OSError:
                try:
                    if entry.is_symlink():
                        nondirs.append(name)
                except OSError:
                    pass

        if topdown:
            yield toppath, dirs, nondirs, topfd

        for name in dirs if entries is None else zip(dirs, entries):
            try:
                if not follow_symlinks:
                    if topdown:
                        orig_st = stat(name, dir_fd=topfd, follow_symlinks=False)
                    else:
                        assert entries is not None
                        name, entry = name
                        orig_st = entry.stat(follow_symlinks=False)
                dirfd = open(name, O_RDONLY, dir_fd=topfd)
            except OSError as err:
                if onerror is not None:
                    onerror(err)
                continue
            try:
                if follow_symlinks or path.samestat(orig_st, stat(dirfd)):
                    dirpath = path.join(toppath, name)
                    yield from _fwalk(dirfd, dirpath, isbytes,
                                      topdown, onerror, follow_symlinks)
            finally:
                close(dirfd)

        if not topdown:
            yield toppath, dirs, nondirs, topfd

    __all__.append("fwalk")

接下来的 fwalk() 函数与上面的 walk() 函数行为完全一样,除了它产生的是 4 元组 (dirpath, dirnames, filenames, dirfd),并且它支持 dir_fd(也就是是指向目录 dirpath 的文件描述符)。我就不多加以解释了,即使存在的一部分不同,也是很小的。

值得一提的是在 .nt (也就是Windows) 平台下没有 fwalk() 函数,但是当我用pycharm进行编辑的时候是有提示 fwalk() 函数的,这也算是一个小坑吧。jupyter就没有这个补全。

def execl(file, *args):
    execv(file, args)

def execle(file, *args):
    env = args[-1]
    execve(file, args[:-1], env)

def execlp(file, *args):
    execvp(file, args)

def execlpe(file, *args):
    env = args[-1]
    execvpe(file, args[:-1], env)

def execvp(file, args):
    _execvpe(file, args)

def execvpe(file, args, env):
    _execvpe(file, args, env)

__all__.extend(["execl","execle","execlp","execlpe","execvp","execvpe"])

def _execvpe(file, args, env=None):
    if env is not None:
        exec_func = execve
        argrest = (args, env)
    else:
        exec_func = execv
        argrest = (args,)
        env = environ

    if path.dirname(file):
        exec_func(file, *argrest)
        return
    saved_exc = None
    path_list = get_exec_path(env)
    if name != 'nt':
        file = fsencode(file)
        path_list = map(fsencode, path_list)
    for dir in path_list:
        fullname = path.join(dir, file)
        try:
            exec_func(fullname, *argrest)
        except (FileNotFoundError, NotADirectoryError) as e:
            last_exc = e
        except OSError as e:
            last_exc = e
            if saved_exc is None:
                saved_exc = e
    if saved_exc is not None:
        raise saved_exc
    raise last_exc

os.exec*() :这几个函数就不一一具体说明了。这些函数都是对应的C API的python实现。

这些函数都将执行一个新程序,以替换当前进程。它们没有返回值。在 Unix 上,新程序会加载到当前进程中,且进程号与调用者相同。过程中的错误会被报告为 OSError 异常。 当前进程会被立即替换。打开的文件对象和描述符都不会刷新,因此如果这些文件上可能缓冲了数据,则应在调用 exec* 函数之前使用 sys.stdout.flush() 或 os.fsync() 刷新它们。具体解释请移步官方文档。我这就不多加赘述了。

os.exec*()都只是posix系统的直接映射,所以os.execl的第一个参数 “/usr/bin/python “是程序的可执行文件,而其他的都是program argument,就是c中int main(int argc,char** argv)中的argv。


而python的sys.argv应是c中argv的[1:],所以os.execl中的第二个参数 “python “对于python程序test.py不可见而且没有用。 实际上os.execl的第二个参数也就是int main(int argc,char** argv)中的argv[0]可以是任意的,它本质上是提供给c程序作为main()函数的第一个参数使用。

————————————————————————————————————————————————————————————————————————————————————————

总结:好累,真不明白当初大佬是怎么读完的,当初还想三篇博客写完,做梦去吧。(文章中摘录了部分大佬的博客内容,希望大佬不要介意,大佬的博客。)后面的内容就要自己一个个推断了,希望能够更新完吧。每一次阅读源代码就增长了很多知识,这个学习方法是满不错的。

留下评论