目录

第1~22行 模块整体注释、nt与posix

第24~46行 模块引入、_exists方法、_get_exports_list方法

第48~97行 根据系统不同导入不同的方法和属性

第100~185行 ?[1]

第188~193行 定义三个枚举变量

第195~228行 makedirs——创建多级目录

第230~250行 removedirs——删除多级目录

第252~278行 renames——重命名目录或文件

第280~421行 walk——目录树生成器

未完待续……


os模块包含了一些与操作系统相关的函数接口,而且它是支持跨平台的,它封装了 nt.py(windows) 和 posix.py (类Unix)两个模块的接口,而后面两个模块是由C语言实现的、直接和系统交互的底层接口。也就是说os模块能够处理平台间的差异问题,使得编写好的程序无需做任何改动就能在不同的平台上运行。如果想要查看os模块的所有内容,可以使用dir(os)方法查看。

可以看到os模块中有这么多属性和方法,这些都是可以通过“os.”访问的。因为os模块是使用纯Python实现的标准库,所以在Python安装目录中也可以找到os模块的源码。打开Python安装目录下 \Lib\os.py 文件,或者源码目录下 \Lib\os.py 文件,就可以查看os模块的源码了。整个代码看下来,os.py 主要是将底层接口进行了一层封装,不知道其他标准库是不是也是这样。

以下标题中的行数是与我所用的3.8.4版本os.py文件真实的行数对应的,而分析文字部分所说的行数,是对应截取的代码段的行数,这样比较方便看。

第1~22行 模块整体注释、nt与posix

首先是第1行到第22行,这是一段整个模块的注释。

r"""OS routines for NT or Posix depending on what system we're on.This exports:- all functions from posix or nt, e.g. unlink, stat, etc.- os.path is either posixpath or ntpath- os.name is either 'posix' or 'nt'- os.curdir is a string representing the current directory (always '.')- os.pardir is a string representing the parent directory (always '..')- os.sep is the (or a most common) pathname separator ('/' or '\\')- os.extsep is the extension separator (always '.')- os.altsep is the alternate pathname separator (None or '/')- os.pathsep is the component separator used in $PATH etc- os.linesep is the line separator in text files ('\r' or '\n' or '\r\n')- os.defpath is the default search path for executables- os.devnull is the file path of the null device ('/dev/null', etc.)Programs that import and use 'os' stand a better chance of being
portable between different platforms.  Of course, they must then
only use functions that are defined by all platforms (e.g., unlink
and opendir), and leave all pathname manipulation to os.path
(e.g., split and join).
"""
r"""OS模块的示例程序适用于NT体系还是Posix体系,取决于当前所在的系统。所有的接口和属性如下:- 所有posix或nt的支持的函数,例如 unlink, stat等。- os.path 等同于posixpath模块或者ntpath模块。- os.name 等于'posix'或'nt'。- os.curdir 是代表当前目录的字符串 (所有系统都是'.')。- os.pardir 是代表上级目录的字符串 (所有系统都是'..')。- os.sep 是(或者说是最常见的)路径名分隔符 (根据不同系统是'/'或'\\')。- os.extsep 是扩展分隔符 (所有系统都是'.')。- os.altsep 是备用路径名分隔符 (有的系统没有 有的系统是'/')。- os.pathsep 是环境变量等配置的组件分隔符 比如WIN是分号。- os.linesep 是文本文件中的行分隔符 (根据不同系统是'\r'或'\n'或'\r\n')。- os.defpath 是可执行文件的默认搜索路径。- os.devnull 是空设备的文件路径 ('/dev/null'等)。导入和使用os模块使你写的程序更容易在不同平台之间移植。
当然,它们必须使用对应平台所支持、定义的功能。(例如unlink和opendir),
关于路径名的操作留给os.path模块(也就是posixpath或ntpath)来处理,(例如split和join)。
"""

这段注释解释了OS模块的功能,就是为了支持跨平台的移植代码,根据不同的系统调用对应平台支持的接口。以前也写过跨Windows和Linux的Python程序,当时就是在网上查的,根据os.name的值是字符串'nt'还是'posix'来判断当前是哪种系统,然后走到对应的代码分支,并没有深究就是为什么。这次研究一下什么是NT和Posix:

NT即Windows NT,就是Windows系统的内核,其中的NT意为New Technology。微软一开始命名系统是以内核版本命名的,比如:Microsoft Windows NT 3.1 (1993)、Microsoft Windows NT 3.5 (1994),知道Windows 2000之后才改成现在的xp、7、Vista、10这种。它的内核版本还是在不断演进的,只是系统对外发布的命名风格发生了变化。所以OS模块中用‘nt’代表Windows系统。

POSIX提供了一套大体上基于Unix的可移植操作系统标准,意在期望获得源代码级别的软件可移植性。为了实现相同的功能,在不同的系统上可能有着不同的接口名字,写代码的时候需要根据不通系统在源代码级别上进行适配。为了解决这个问题,让不同系统都遵循POSIX标准,就是在原来的接口的基础上再封装一层,起一个通用的名字,这样就可以用一份源代码运行在不同的系统上,这个道理和OS模块的功能是一样的。Linux是与POSIX兼容的(现在Window也开始支持这个标准了),所以OS模块中用‘posix’代表Linux这种类UNIX系统。

为什么选择这两个单词我们不用深究,只是一种规定,我们用的时候只需'判断os.name == 'nt是Windows系统,os.name == 'posix'是Linux或Mac系统就行了,至于OS怎么根据‘nt’和‘posix’两个字符串实现区分不同系统的,这个在源码中有体现,后面马上就能看的到。

第24~46行 模块引入、_exists方法、_get_exports_list方法

#'
import abc
import sys
import stat as stfrom _collections_abc import _check_methods_names = sys.builtin_module_names# Note:  more names are added to __all__ later.
__all__ = ["altsep", "curdir", "pardir", "sep", "pathsep", "linesep","defpath", "name", "path", "devnull", "SEEK_SET", "SEEK_CUR","SEEK_END", "fsencode", "fsdecode", "get_exec_path", "fdopen","popen", "extsep"]def _exists(name):return name in globals()def _get_exports_list(module):try:return list(module.__all__)except AttributeError:return [n for n in dir(module) if n[0] != '_']

第2行引入 abc 模块,在os这里主要用到里面的ABC,Abstract Base Class(抽象基类),主要定义了基本类和最基本的抽象方法,可以为子类定义共有的API,不需要具体实现。相当于是Java中的接口或者是抽象类。这个抽象基类的作用一两句话说不清楚,鉴于abc.py也属于Python标准库,下一篇就具体研究一下这个abc模块。

第3行引入sys模块,这是一个C实现的内置模块,主要是实现Python解释器、操作系统相关的操作。

第4行引入stat模块,这个模块主要实现文件状态检查之类的操作,也属于一个标准库,在os这里只是用到了 st.S_ISDIR 这个方法,判断是否是目录。

第6行引入_collections_abc模块的_check_methods方法,这个模块是用于集合的抽象基类,也属于一个标准库,_check_methods用来判断一个对象是否含有某个属性。

第8行 sys.builtin_module_names 返回一个包含内建模块名字的元组,包含所有已经编译到Python解释器的模块名字。这里就可以解释os模块怎么根据‘nt’和‘posix’两个字符串实现区分不同系统的:Window系统的Python会安装nt模块,这个返回的元组中就会包含‘nt’,而其他系统的Python在安装时不安装nt模块,而是安装posix模块,这个返回的元组中就会包含‘posix’。

下面一行是一个注释,提示:更多的名称会在后面慢慢加入到__all__中。

第11行,__all__ 是针对模块公开接口的一种约定,以提供了”白名单“的形式暴露接口。如果定义了__all__,其他文件中使用from xxx import *导入该文件时,只会导入 __all__ 列出的成员,其他成员都被排除在外。若没定义,则导入模块内的所有公有属性/方法和类 。因为只是一种约定,就像用__前缀表示私有成员一样,它只对import *起作用,对from xxx import xxx不起作用。

下面定义了_exists方法,用于通过名字获得全局变量中的对象。其中global()方法是解释器内置方法,不需要导入就可以直接用,会以字典类型返回当前位置的全部全局变量。类似的还有locals()方法,后者以字典类型返回当前位置的局部变量。

再下面是_get_exports_list方法,这个了解了上面的__all__就很好理解,就是通过模块名获得对应模块对外暴露的接口,如果该模块没有定义__all__,即发生了AttributeError,就返回模块所有接口里面不是以_前缀开头的接口。可以看出这是在遵守约定。

第48~97行 根据系统不同导入不同的方法和属性

# Any new dependencies of the os module and/or changes in path separator
# requires updating importlib as well.
if 'posix' in _names:name = 'posix'linesep = '\n'from posix import *try:from posix import _exit__all__.append('_exit')except ImportError:passimport posixpath as pathtry:from posix import _have_functionsexcept ImportError:passimport posix__all__.extend(_get_exports_list(posix))del posixelif 'nt' in _names:name = 'nt'linesep = '\r\n'from nt import *try:from nt import _exit__all__.append('_exit')except ImportError:passimport ntpath as pathimport nt__all__.extend(_get_exports_list(nt))del nttry:from nt import _have_functionsexcept ImportError:passelse:raise ImportError('no os specific module found')sys.modules['os.path'] = path
from os.path import (curdir, pardir, sep, pathsep, defpath, extsep, altsep,devnull)del _names

注释是说,os模块的任何新依赖关系、路径分隔符的更改都需要更新导入库。因此下面的 if 和 elif 代码段就是在根据不同的系统更新导入的库。

第3行就是上面说的根据当前已经编译到Python解释器的模块名字判断当前系统是不是遵循POSIX标准的系统(Mac、Linux等),如果是就将新建属性 name 赋值 posix ,注意这里的属性是属于os模块的,这也是为什么我们在外面导入os模块后,就可以使用os.name来判断系统类型,和文件最开始的注释中的os.name就对上了,看了原理就会发现其实也没什么神奇的。同理,下一行的linesep对应文件最开始的注释中的os.linesep,表示文本文件的分隔符,在Mac、Linux等系统中默认是 '\n'。

第6行导入posix模块的全部内容。此模块提供了对基于 C 标准和 POSIX 标准(一种稍加修改的 Unix 接口)进行标准化的系统功能的访问。官方文档都用加粗字体说了:请勿直接导入此模块 而应导入os模块,它提供了此接口的可移植版本,而且没有性能损失。前面说过os模块就是对posix模块和nt模块的封装,在Windows上安装的Python是没有posix模块的,所以IDE里也没法跳转过去,这里暂不深究。

下面五行尝试从 posix 导入 _exit 方法并将其加到__all__中暴露出去,如果没有就pass。前面已经import *了,为什么还要再单独导入一次呢?我觉得这里是因为不确定 posix 里是否有此方法(可能POSIX标准的部分系统没有这个方法),所以不能直接使用__all__.append('_exit')。

第12行将 posixpath 导入并改名为 path,所以我们平时使用的 os.path 其实不是os模块本身。posixpath 是另外一个用Python实现的专门处理关于路径的问题的库,这个库Windows系统上的Python也会安装,后面的文章再具体分析。

下面是尝试导入_have_functions,这是一个列表,里面包含对应系统所支持的函数名,后面会用到。再下面就是把posix模块对外暴露的方法和属性和当前os模块合并,其中 _get_exports_list 方法是前面刚刚定义的。

再往下看,elif 代码块和前面一模一样,只是把 posix 换成了 nt 。后面还有一个 else 代码块,如果 'posix' 和 'nt'都不在内建属性列表中将会报错,说明 os 模块只支持 posix和nt 标准的系统(除了这两种估计也没啥别的标准了)。到这里,当前系统是什么类型已经很明确了,只需将对应系统的路径分隔符等导入进来。

第100~185行 ?[1]

if _exists("_have_functions"):_globals = globals()def _add(str, fn):if (fn in _globals) and (str in _have_functions):_set.add(_globals[fn])_set = set()_add("HAVE_FACCESSAT",  "access")_add("HAVE_FCHMODAT",   "chmod")_add("HAVE_FCHOWNAT",   "chown")_add("HAVE_FSTATAT",    "stat")_add("HAVE_FUTIMESAT",  "utime")_add("HAVE_LINKAT",     "link")_add("HAVE_MKDIRAT",    "mkdir")_add("HAVE_MKFIFOAT",   "mkfifo")_add("HAVE_MKNODAT",    "mknod")_add("HAVE_OPENAT",     "open")_add("HAVE_READLINKAT", "readlink")_add("HAVE_RENAMEAT",   "rename")_add("HAVE_SYMLINKAT",  "symlink")_add("HAVE_UNLINKAT",   "unlink")_add("HAVE_UNLINKAT",   "rmdir")_add("HAVE_UTIMENSAT",  "utime")supports_dir_fd = _set_set = set()_add("HAVE_FACCESSAT",  "access")supports_effective_ids = _set_set = set()_add("HAVE_FCHDIR",     "chdir")_add("HAVE_FCHMOD",     "chmod")_add("HAVE_FCHOWN",     "chown")_add("HAVE_FDOPENDIR",  "listdir")_add("HAVE_FDOPENDIR",  "scandir")_add("HAVE_FEXECVE",    "execve")_set.add(stat) # fstat always works_add("HAVE_FTRUNCATE",  "truncate")_add("HAVE_FUTIMENS",   "utime")_add("HAVE_FUTIMES",    "utime")_add("HAVE_FPATHCONF",  "pathconf")if _exists("statvfs") and _exists("fstatvfs"): # mac os x10.3_add("HAVE_FSTATVFS", "statvfs")supports_fd = _set_set = set()_add("HAVE_FACCESSAT",  "access")# Some platforms don't support lchmod().  Often the function exists# anyway, as a stub that always returns ENOSUP or perhaps EOPNOTSUPP.# (No, I don't know why that's a good design.)  ./configure will detect# this and reject it--so HAVE_LCHMOD still won't be defined on such# platforms.  This is Very Helpful.## However, sometimes platforms without a working lchmod() *do* have# fchmodat().  (Examples: Linux kernel 3.2 with glibc 2.15,# OpenIndiana 3.x.)  And fchmodat() has a flag that theoretically makes# it behave like lchmod().  So in theory it would be a suitable# replacement for lchmod().  But when lchmod() doesn't work, fchmodat()'s# flag doesn't work *either*.  Sadly ./configure isn't sophisticated# enough to detect this condition--it only determines whether or not# fchmodat() minimally works.## Therefore we simply ignore fchmodat() when deciding whether or not# os.chmod supports follow_symlinks.  Just checking lchmod() is# sufficient.  After all--if you have a working fchmodat(), your# lchmod() almost certainly works too.## _add("HAVE_FCHMODAT",   "chmod")_add("HAVE_FCHOWNAT",   "chown")_add("HAVE_FSTATAT",    "stat")_add("HAVE_LCHFLAGS",   "chflags")_add("HAVE_LCHMOD",     "chmod")if _exists("lchown"): # mac os x10.3_add("HAVE_LCHOWN", "chown")_add("HAVE_LINKAT",     "link")_add("HAVE_LUTIMES",    "utime")_add("HAVE_LSTAT",      "stat")_add("HAVE_FSTATAT",    "stat")_add("HAVE_UTIMENSAT",  "utime")_add("MS_WINDOWS",      "stat")supports_follow_symlinks = _setdel _setdel _have_functionsdel _globalsdel _add

这段代码的意思看懂了,但是没有明白它的作用。将一些 globals 里和 _have_functions 同时出现的方法名加到 一个set里,并重新赋值给supports_dir_fd、supports_effective_ids、supports_fd、supports_follow_symlinks四个集合。但是只有supports_dir_fd和supports_fd在后面的代码中用到了,另外两个集合整个代码里都没有用过,不知道是什么作用,先跳过,保留疑问[1]。

第188~193行 定义三个枚举变量

# Python uses fixed values for the SEEK_ constants; they are mapped
# to native constants if necessary in posixmodule.c
# Other possible SEEK values are directly imported from posixmodule.c
SEEK_SET = 0
SEEK_CUR = 1
SEEK_END = 2

注释的意思是: Python对SEEK_常量使用固定值,如果需要,它们在 posixmodule.c 中被映射到本机常量。其他可能的SEEK_常量值直接从posixmodule.c 导入。

这三个常量一般用作fseek函数的一个入参。fseek函数是C语言中用于二进制方式打开的文件时,移动文件读写指针位置。原型是int fseek(FILE *stream, long offset, int fromwhere);  第一个参数stream为文件指针第,第二个参数offset为偏移量,整数表示正向偏移,负数表示负向偏移, 第三个参数设定从文件的哪里开始偏移,可能取值为:SEEK_SET: 文件开头;SEEK_CUR: 当前位置;EEK_END: 文件结尾。这三个变量我觉得不用深究,鉴于模块开头的 __all__ 里也加入了这三个变量名,应该是在模块外面调用别的函数的时候作为入参使用的,这里定义一下可以起到枚举的作用。

第195~228行 makedirs——创建多级目录

# Super directory utilities.
# (Inspired by Eric Raymond; the doc strings are mostly his)def makedirs(name, mode=0o777, exist_ok=False):"""makedirs(name [, mode=0o777][, exist_ok=False])Super-mkdir; create a leaf directory and all intermediate ones.  Works likemkdir, except that any intermediate path segment (not just the rightmost)will be created if it does not exist. If the target directory alreadyexists, raise an OSError if exist_ok is False. Otherwise no exception israised.  This is recursive."""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 pathpasscdir = curdirif isinstance(tail, bytes):cdir = bytes(curdir, 'ASCII')if tail == cdir:           # xxx/newdir/. exists if xxx/newdir existsreturntry:mkdir(name, mode)except OSError:# Cannot rely on checking for EEXIST, since the operating system# could give priority to other errors like EACCES or EROFSif not exist_ok or not path.isdir(name):raise

前面大部分都是一些准备工作,到195行这里才刚刚开始正题。

首先是两行注释:关于目录的方法的超级版。(灵感来自Eric Raymond,the doc strings are mostly his(这句没搞明白啥意思,是文档的内容大部分是他的?))。埃里克·史蒂文·雷蒙德,著名的计算机程序员,开源软件运动的旗手。他是INTERCAL编程语言的主要创作者之一,曾经为EMACS编辑器作出贡献。雷蒙德还是著名的Fetchmail程序的作者。他还编写了一个最初用于Linux内核设置的设置程序——百度百科。

下面定义了 makedirs 方法,这是mkdir方法的超级版本,创建一个子目录和所有中间目录。它和mkdir的区别是:如果要在目录a 下新建一个 'a/b/c' 的目录,makedirs 可以一次新建 b 并且在 b 下新建c,而mkdir 需要先新建 b 再进入到 b 下新建 c,需要调用两次。如果目标目录已经存在,并且 exist_ok 参数是 False,则引发 OSError,exist_ok 参数是 True 则不引发异常。这个函数是通过递归实现前面的功能的。它有3个入参, name 是字符串类型的目标路径名,mode 表示创建的目录的权限,默认值是777,该参数在windows下会被忽略,exist_ok 前面注释里说了,就是在目标路径存在的情况下是否报错。这里可以看出os模块也不是仅仅对nt或posix进行简单封装,还加入了一些更加方便的功能。

看代码要先找准代码的主体部分,然后再去看那些辅助部分。这个方法的主体部分就是该段代码的第19行 makedirs(head, exist_ok=exist_ok) 和第29行 mkdir(name, mode) ,其他是一些条件判断或者校验。

第14行调用 path.split 即 posixpath 或者 ntpath模块的 split 方法。我看了ntpath里的 split 方法,考虑了传入字符串的许多种格式,包括一些写错的情况,当前我们只需要知道它的作用是对传入路径进行分割,以最后一个路径分隔符作为分隔,head和tail。比如传入 'C:/ttt/eee/sss/ttt',返回 head = 'C:/ttt/eee/sss',tail = 'ttt'。下面15行判断 tail 如果为空会再执行一次 split,主要是考虑到传入的参数最后带了路径分隔符的情况,比如传入'C:/ttt/eee/sss/ttt/' ,第一次执行split 就会返回 head = 'C:/ttt/eee/sss/ttt',tail = ''。

第17行是说如果 head 和 tail 都不为空,而且 head已经存在的情况下,调用makedirs进行递归。这里注意递归时mode参数被省略了,也就是说如果创建的是多级目录,除了第一层是用传入的权限,其他子目录都是用的默认参数权限即777,当然这是针对非Windows系统来说的。

第20~22行,如果发生路径已存在的异常,就pass,注释说这样为了避免多线程创建路径的情况。

第23行是取curdir,这个在开头注释中有说明,代表当前目录的字符串,就是 "." 。

第24~27行,是为了适配最后一个路径是 "."  的情况,还考虑到路径编码是bytes类型的情况。一开始没看出来这段代码的必要性,于是我将这段代码注释,然后执行 os.makedirs('test/test/.') ,结果出现异常如下图。

这是在第30行捕获到的OSError类型的异常,这说明调用底层接口 mkdir(name, mode) 时,对于路径  "." ,系统会自动解析为当前路径(引发异常时的“当前路径”就是 "test/test" ,已经递归创建了 ),当前路径已经存在,再创建自然会引发异常。但是我想应该不会有人这样传参吧,另外我测了一下路径最后是两个点 ".." 的情况,原代码会直接报异常,毕竟不可能把所有异常入参都覆盖到,这也是我平时写代码比较纠结的地方:到底要不得要把能想到的所有异常都主动捕获并处理掉?

后面的代码就好理解了,经过前面的一系列处理和递归,到第29行时,name 参数已经变成一层目录了,直接调用 nt 或 posix 模块的 mkdir() 就可以创建一层目录了。当然可能会出现一些异常,但这里主要是捕获“路径已存在”类型的异常,注释是说这里之所以捕获 OSError 而不是 EEXISTError ,是因为有的系统发现路径已存在时不一定报已存在而会报其他错误。

第230~250行 removedirs——删除多级目录

def removedirs(name):"""removedirs(name)Super-rmdir; remove a leaf directory and all empty intermediateones.  Works like rmdir except that, if the leaf directory issuccessfully removed, directories corresponding to rightmost pathsegments will be pruned away until either the whole path isconsumed or an error occurs.  Errors during this latter phase areignored -- they generally mean that a directory was not empty."""rmdir(name)head, tail = path.split(name)if not tail:head, tail = path.split(head)while head and tail:try:rmdir(head)except OSError:breakhead, tail = path.split(head)

这是一个删除多级目录的方法,而不是删除文件的。方法注释:删除一个子目录和所有空的中间目录。工作方式与rmdir类似,不同的地方是,如果最底层子目录被成功删除后,此时路径段的最右端目录也将被继续删除,直到整个路径被删除或者出现错误。后面这个阶段中的错误将被忽略——它们通常意味着目录不是空的。就是说,如果传参是"D:/ttt/eee/sss/" 会先删除sss目录,当然前提是 "sss"目录是空的。此时再看"eee"是否为空,如果是也把"eee"删掉,不为空就此退出。

第11行是调用 "nt" 或 "posix"模块的底层方法 rmdir,作用就是删除一个目录,如果非空会报错。

第12~14行和上面makedirs函数作用一样,是将路径最后一层分隔出来,考虑路径最后有两个斜杠的情况。

后面在循环中依次尝试将分隔出来的 head 删掉,知道出现异常跳出循环。很好理解。

第252~278行 renames——重命名目录或文件

def renames(old, new):"""renames(old, new)Super-rename; create directories as necessary and delete any leftempty.  Works like rename, except creation of any intermediatedirectories needed to make the new pathname good is attemptedfirst.  After the rename, directories corresponding to rightmostpath segments of the old name will be pruned until either thewhole path is consumed or a nonempty directory is found.Note: this function can fail with the new directory structure madeif you lack permissions needed to unlink the leaf directory orfile."""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"])

注释是说: 创建必要的目录,并删除所有空的。类似于重命名,不同的是本函数会首先尝试创建使新路径名有效所需的中间目录。在重命名之后,与旧名称最右边路径段对应的目录将被删除,直到把旧的路径都删完或找到一个非空目录。注意:如果您缺乏断开子目录或文件链接所需的权限,那么在创建新目录结构时,此函数可能会失败。

还是直接看代码好理解一点,第16~18行,先判断新目录的倒数第二层路径是否存在,如果不存在就创建。所以这个函数不仅支持"D:/ttt/eee/ --> D:/ttt/sss" 这种只改最后一层子目录的形式,也支持 "D:/ttt/eee/ --> D:/111/222" 这种同时重命名多级目录的形式,要不说是超级版本呢。只是要注意,不管哪种形式,原来的目录为空的话会被删除,所以这个函数不能用于新建目录,新建目录还是用makedirs吧。

下面还是调用 "nt"或"posix"里的方法,进行重命名,前面已经把新目录的上一层目录创建好了。再后面就是删除旧路径的过程,从最底层的目录开始,一层一层删过去,出现异常忽略。可以看出好多你自以为优雅的写法,只是标准库帮你把异常报错pass了而已。

最后把刚定义的三个函数加入到__all__里,前面说过__all__会不断进行扩充。

第280~421行 walk——目录树生成器

def walk(top, topdown=True, onerror=None, followlinks=False):"""Directory tree generator.For each directory in the directory tree rooted at top (including topitself, but excluding '.' and '..'), yields a 3-tupledirpath, dirnames, filenamesdirpath is a string, the path to the directory.  dirnames is a list ofthe names of the subdirectories in dirpath (excluding '.' and '..').filenames is a list of the names of the non-directory files in dirpath.Note that the names in the lists are just names, with no path components.To get a full path (which begins with top) to a file or directory indirpath, do os.path.join(dirpath, name).If optional arg 'topdown' is true or not specified, the triple for adirectory is generated before the triples for any of its subdirectories(directories are generated top down).  If topdown is false, the triplefor a directory is generated after the triples for all of itssubdirectories (directories are generated bottom up).When topdown is true, the caller can modify the dirnames list in-place(e.g., via del or slice assignment), and walk will only recurse into thesubdirectories whose names remain in dirnames; this can be used to prune thesearch, or to impose a specific order of visiting.  Modifying dirnames whentopdown is false has no effect on the behavior of os.walk(), since thedirectories in dirnames have already been generated by the time dirnamesitself is generated. No matter the value of topdown, the list ofsubdirectories is retrieved before the tuples for the directory and itssubdirectories are generated.By default errors from the os.scandir() call are ignored.  Ifoptional arg 'onerror' is specified, it should be a function; itwill be called with one argument, an OSError instance.  It canreport the error to continue with the walk, or raise the exceptionto abort the walk.  Note that the filename is available as thefilename attribute of the exception object.By default, os.walk does not follow symbolic links to subdirectories onsystems that support them.  In order to get this functionality, set theoptional argument 'followlinks' to true.Caution:  if you pass a relative pathname for top, don't change thecurrent working directory between resumptions of walk.  walk neverchanges the current directory, and assumes that the client doesn'teither.Example:import osfrom os.path import join, getsizefor root, dirs, files in os.walk('python/Lib/email'):print(root, "consumes", end="")print(sum(getsize(join(root, name)) for name in files), end="")print("bytes in", len(files), "non-directory files")if 'CVS' in dirs:dirs.remove('CVS')  # don't visit CVS directories"""top = fspath(top)dirs = []nondirs = []walk_dirs = []# We may not have read permission for top, in which case we can't# get a list of the files the directory contains.  os.walk# always suppressed the exception then, rather than blow up for a# minor reason when (say) a thousand readable directories are still# left to visit.  That logic is copied here.try:# Note that scandir is global in this module due# to earlier import-*.scandir_it = scandir(top)except OSError as error:if onerror is not None:onerror(error)returnwith scandir_it:while True:try:try:entry = next(scandir_it)except StopIteration:breakexcept OSError as error:if onerror is not None:onerror(error)returntry:is_dir = entry.is_dir()except OSError:# If is_dir() raises an OSError, consider that the entry is not# a directory, same behaviour than os.path.isdir().is_dir = Falseif 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 Falseif followlinks:walk_into = Trueelse:try:is_symlink = entry.is_symlink()except OSError:# If is_symlink() raises an OSError, consider that the# entry is not a symbolic link, same behaviour than# os.path.islink().is_symlink = Falsewalk_into = not is_symlinkif walk_into:walk_dirs.append(entry.path)# Yield before recursion if going top downif topdown:yield top, dirs, nondirs# Recurse into sub-directoriesislink, join = path.islink, path.joinfor dirname in dirs:new_path = join(top, dirname)# Issue #23605: os.path.islink() is used instead of caching# entry.is_symlink() result during the loop on os.scandir() because# the caller can replace the directory entry during the "yield"# above.if followlinks or not islink(new_path):yield from walk(new_path, topdown, onerror, followlinks)else:# Recurse into sub-directoriesfor new_path in walk_dirs:yield from walk(new_path, topdown, onerror, followlinks)# Yield after recursion if going bottom upyield top, dirs, nondirs__all__.append("walk")

我觉得 walk 函数是这个模块看到现在最符合Python特点的方法,强大,方便,优雅。

首先一段很长的注释,主要介绍了出参和入参。该方法返回一个迭代器,包含一个三元元组,dirpath, dirnames, filenames。

dirpath 是当前遍历的目录的名字,从入参top开始(如果topdown为False的话),字符串类型;

dirnames 是dirpath中的子目录的名字(路径名字)的列表,list类型;

filenames 是dirpath中的非目录的文件的名字(只有名字)的列表,list类型;

第一个入参 top 就是要遍历的文件夹,字符串类型。如果第二个可选入参 topdown 为 True 或未指定,目录是自顶向下生成的。如果 topdown 为 False ,则目录是自底向上生成的。举个例子:

# 当前目录结构:
# ttt
#  ├── sss
#  ├    └──333.txt
#  ├── 111.txt
#  └── 222.txtfor a, b, c in os.walk('ttt'):print(a, b, c)
# 输出:
# ttt ['sss'] ['111.txt', '222.txt']
# ttt\sss [] ['333.txt']for a, b, c in os.walk('ttt', topdown=True):print(a, b, c)
# 输出:
# ttt\sss [] ['333.txt'].
# ttt ['sss'] ['111.txt', '222.txt']

第三个入参 onerror 是一个回调函数,默认情况下,onerror 参数未指定,此时调用 scandir 方法时,出现的错误将被忽略 (scandir 方法就是 nt 或 posix模块里的底层遍历目录的方法,这个内置方法只能遍历一层目录,所以像前面几个函数一样,walk 方法其实可以看做 scandir 方法的超级版本)。如果可选参数 onerror 被指定,它必须是一个包含一个入参的函数,因为后面的循环中如果出现OSError异常,会将OSError类型实例作为参数传给它。

默认情况下,walk方法不遵循符号链接跳转到它们指向的子目录。为了获得此功能,将可选参数 followlinks 设置为True。这里应该是说类似Linux系统的软连接硬连接那种,该参数用来选择是否迭代它们所指向的目录。我在Windows测试,快捷方式文件是不生效的,它会把快捷方式当成一个普通文件。

注意:如果为top传递的是相对路径名,不要在 walk 函数执行期间之间更改当前工作目录。walk 从不更改当前目录,并假设客户机也不更改当前目录。

看看代码部分:第60行将 top 参数传到 fspath 转换了一下,这个方法是在本模块后面1060行定义的,主要作用是判断传入的参数是不是字符串类型的目录名,如果不是,直接报错。fspath 实现细节后面再具体看。在一些其他语言比如C语言中,要调用一个方法和属性必须在调用出现之前进行定义,在Python这里对这个顺序要求不是很在意。

第61~63行,定义了三个列表,dirs用来存放遍历过程中的目录名,nondirs用来存放遍历过程中的文件的名字,walk_dirs 用来存放要遍历的子目录,用于后面继续迭代它们的子目录。

第65~69行注释,在没有top目录的读权限的情况下,无法获得目录中包含的文件列表。大部分情况下walk总是忽略一些异常,这样避免(比方说)在还有1000个可读目录需要访问时,因为一个小原因而崩溃。这个逻辑复制到这里。

第71~73行,这里是walk函数的核心部分,调用 scandir (nt或posix里的),这个方法返回一个迭代器,包含入参目录下的所有DirEntry类型的子目录和文件,DirEntry是nt(或posix)模块定义的一个类,包含一个目录或文件的基本信息。这里将其返回的迭代器赋值给scandir_it。这里的注释是提醒scandir是在前面对 nt 模块或者 posix 模块 import * 时引入的。我怀疑os模块不是一个人完成的,因为上面几个函数在调用别的模块的方法时,就没有这种提示,还需要自己去找一些方法或属性的出处。

第74~77行,如果第一级目录再进行遍历的时候就出现了 OSError 类型的异常,就调用回调函数 onerror 方法处理(如果有定义的话),然后直接返回。

79行开始进入循环。第81~89行取出迭代器 scandir_it 的下一个元素,也就是 top 目录下的第一个子目录或者文件,如果迭代器被迭代完了,就pass。这里同遇到异常,会和74到77行的处理方法一样。

第91~101行,判断取出的第一个元素是不是目录,is_dir 方法是DirEntry类型实例的一个方法。(还记得吗,73行scandir_it = scandir(top)返回的迭代器,里面包含的实例是DirEntry类型的,在83行entry = next(scandir_it)取出)。如果该元素是目录就加到列表 dirs 里,否则加到列表 nondirs 里。

此时,如果 topdown 为True 或未指定,循环体就执行完了。接下来继续遍历,直到把第一层目录下的子目录和文件都分类保存在两个列表里(这里应该想到后面肯定会有递归)。如果topdown 参数为False,而且当前元素是个目录(这里来看链接也算目录),第103~119行,这里又要考虑followlinks参数,如果为True,就将当前元素的路径加入到列表 walk_dirs 里,如果为Flase或者未指定,当前元素是链接的话就不加入待迭代目录列表walk_dirs,不是链接(普通目录)就加入到walk_dirs。

第121~134行,如果是自顶向下生成的,这时候就可以 yield walk这个迭代器的第一个元素了。然后准备第二个元素(这么说可能不对) :将 top 和 dirs 里的路径名字组合,形成新的路径名,在for循环中调用walk就完成第二层目录的迭代,这样递归下去就可以遍历到所有的子目录,递归的跳出点就在两个return那里。

第135~140行,如果是自底向上生成的,就先不yield,先去处理待迭代的列表里的路径,层层递归,这样就会先 yield 最低层目录,再 yield 上层的目录。

最后,将walk函数加入到__all__列表里。总的来说,walk这个函数利用Python的迭代器,设计的很巧妙,都看完了让我写也写不出来。

未完待续……

今天写了一下午不知道为啥没保存上,晚上回来打开进度又回到上次写的那里,一下午白干了,刚刚才又凭着记忆重写回来,感觉有的地方跟第一遍用词不一样了。

os模块源码共1115行,到现在才写到421行,这篇文章已经太长了,我决定分成两篇文章,后续写在彻底弄懂Python标准库源码(二)—— os模块(续)里。

这篇文章是我看着源码凭自己理解写的,里面有一些自己的臆断,有看到错误的朋友,麻烦帮忙指出来更正,感谢!

彻底弄懂Python标准库源码(一)—— os模块相关推荐

  1. 彻底弄懂Python标准库源码(三)—— pprint模块

    目录 模块整体注释 依赖模块导入.对外暴露接口 saferepr函数--返回对象的字符串表示,并为无限递归数据结构提供保护 isreadable函数--返回对象的是否"可读" is ...

  2. 整理网上资料---C标准库值篇二 :标准库源码下载地址、标准库手册下载地址

    C标准库源码及手册.zip,包括: http://download.csdn.net/detail/yangzhao0001/9057823 C标准库函数集(头文件索引)------手册-AH.pdf ...

  3. Python Requests库源码分析

    1. Requests库简介 书籍是人类进步的阶梯,源码是程序员进步的阶梯.为了进步,我们就要不断地阅读源码,提升自己的技术水平.今天我们来剖析一下Python的Requests库. Requests ...

  4. Python标准库:一些受欢迎的模块

    目录 1.模块sys 1).介绍 1.模块sys中一些重要的函数和变量 2.变量sys.argv 3.函数sys.exit 4.映射sys.modules 5.变量sys.path 6.变量sys.p ...

  5. Python标准库04 文件管理 (部分os包,shutil包)

    摘要:Python标准库 os包: rmdir, mkdir, listdir, remove, rename, chmod, chown, stat, symlink shutil包: copy, ...

  6. 4.Python标准库_文件管理 (部分os包,shutil包)

    Vamei博客地址:http://www.cnblogs.com/vamei/archive/2012/09/14/2684775.html 作者:Vamei 出处:http://www.cnblog ...

  7. A Byte of Python 笔记(12)python 标准库:sys、os,更多内容

    第14章 python 标准库 Python标准库是随Python附带安装的,它包含大量极其有用的模块. sys 模块 sys 模块包含系统对应的功能.如 sys.argv 列表包含命令行参数. # ...

  8. Kotlin StandardKt 标准库源码走一波

    距离上篇Kotlin文章,应该有差不多半年时间.没别的原因,因为,懒,而且,想产出一篇稍微质量好的博客好难. 最近在研究Python,所以最近也可能会更新一些Python的学习笔记. Standard ...

  9. Python Sklearn库源码学习--kmeans

    前言: 分析体检数据希望不拘泥于Sklearn库中已有的聚类算法,想着改一下Kmeans算法.本着学习的目的,现在开始查看sklearn的源代码.希望能够写成一个通用的包. 有必要先交代一下我使用的p ...

最新文章

  1. java数组缓冲,java – 字节数组缓冲图像转换速度慢
  2. VCSA 6.5 HA配置 之三 :准备工作
  3. mysql slave是什么_创建slave的搜索结果-阿里云开发者社区
  4. python自增_关于 python 的自增运算
  5. CTO要我把这份MySQL规范贴在工位上!
  6. 深度学习领域专业词汇_深度学习时代的人文领域专业知识
  7. HTML中小meta的大作用
  8. C++中operator的两种用法
  9. android 创建文件夹_Android 动画小记
  10. java c 转换_Java怎么转换c语言?
  11. 【报告分享】2020年数字政府新基建发展白皮书.pdf(附下载链接)
  12. HDU 1561 The more, The Better (树形dp)
  13. HBuilderX的各版本软件自取
  14. 51单片机——多文件的建立
  15. 数据库 ER图 EER图(鸭蹼图) freedgo绘图工具
  16. 基于Prometheus+Grafana打造企业级Flink监控系统
  17. Openwrt修改flash大小
  18. PS基础操作之图像处理(三)
  19. 对Airtest报告的步骤标题做内容定制?实用速学
  20. 采用flv.js与dplayer播放器来播放http-flv格式视频

热门文章

  1. PMP与ACP有什么区别?
  2. 任正非:华为考虑向苹果等竞争对手销售5G芯片
  3. Thread wait和sleep的区别
  4. linux系统初级培训材料,Linux操作系统初级培训(5)
  5. 2020 年公众号最受欢迎文章!
  6. 在树莓派上使用微信聊天
  7. 属性表中的 <空> 空格 和 空白
  8. 闪存(SSD)技术,MLC和SLC差距到底有多大?
  9. Tensorflow2教程-文本卷积
  10. 第8周——定期存款利息计算器