@TOCpython当中的os模块源码
本文转载自:https://blog.csdn.net/yushuaigee/article/details/106755148
目录
第24~46行 模块引入、_exists方法、_get_exports_list方法
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 st
-
-
from _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:
-
pass
-
import posixpath
as path
-
-
try:
-
from posix
import _have_functions
-
except ImportError:
-
pass
-
-
import posix
-
__all__.extend(_get_exports_list(posix))
-
del posix
-
-
elif
'nt'
in _names:
-
name =
'nt'
-
linesep =
'\r\n'
-
from nt
import *
-
try:
-
from nt
import _exit
-
__all__.append(
'_exit')
-
except ImportError:
-
pass
-
import ntpath
as path
-
-
import nt
-
__all__.extend(_get_exports_list(nt))
-
del nt
-
-
try:
-
from nt
import _have_functions
-
except ImportError:
-
pass
-
-
else:
-
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 = _set
-
-
del _set
-
del _have_functions
-
del _globals
-
del _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 like
-
mkdir, except that any intermediate path segment (not just the rightmost)
-
will be created if it does not exist. If the target directory already
-
exists, raise an OSError if exist_ok is False. Otherwise no exception is
-
raised. 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 path
-
pass
-
cdir = curdir
-
if isinstance(tail, bytes):
-
cdir = bytes(curdir,
'ASCII')
-
if tail == cdir:
# xxx/newdir/. exists if xxx/newdir exists
-
return
-
try:
-
mkdir(name, mode)
-
except OSError:
-
# Cannot rely on checking for EEXIST, since the operating system
-
# could give priority to other errors like EACCES or EROFS
-
if
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 intermediate
-
ones. Works like rmdir except that, if the leaf directory is
-
successfully removed, directories corresponding to rightmost path
-
segments will be pruned away until either the whole path is
-
consumed or an error occurs. Errors during this latter phase are
-
ignored -- 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:
-
break
-
head, 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 left
-
empty. Works like rename, except creation of any intermediate
-
directories needed to make the new pathname good is attempted
-
first. After the rename, directories corresponding to rightmost
-
path segments of the old name will be pruned until either the
-
whole path is consumed or a nonempty directory is found.
-
-
Note: this function can fail with the new directory structure made
-
if you lack permissions needed to unlink the leaf directory or
-
file.
-
-
"""
-
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, οnerrοr=None, followlinks=False):
-
"""Directory tree generator.
-
-
For each directory in the directory tree rooted at top (including top
-
itself, but excluding '.' and '..'), yields a 3-tuple
-
-
dirpath, dirnames, filenames
-
-
dirpath is a string, the path to the directory. dirnames is a list of
-
the 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 in
-
dirpath, do os.path.join(dirpath, name).
-
-
If optional arg 'topdown' is true or not specified, the triple for a
-
directory is generated before the triples for any of its subdirectories
-
(directories are generated top down). If topdown is false, the triple
-
for a directory is generated after the triples for all of its
-
subdirectories (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 the
-
subdirectories whose names remain in dirnames; this can be used to prune the
-
search, or to impose a specific order of visiting. Modifying dirnames when
-
topdown is false has no effect on the behavior of os.walk(), since the
-
directories in dirnames have already been generated by the time dirnames
-
itself is generated. No matter the value of topdown, the list of
-
subdirectories is retrieved before the tuples for the directory and its
-
subdirectories are generated.
-
-
By default errors from the os.scandir() call are ignored. If
-
optional arg 'onerror' is specified, it should be a function; it
-
will be called with one argument, an OSError instance. It can
-
report the error to continue with the walk, or raise the exception
-
to abort the walk. Note that the filename is available as the
-
filename attribute of the exception object.
-
-
By default, os.walk does not follow symbolic links to subdirectories on
-
systems that support them. In order to get this functionality, set the
-
optional argument 'followlinks' to true.
-
-
Caution: if you pass a relative pathname for top, don't change the
-
current working directory between resumptions of walk. walk never
-
changes the current directory, and assumes that the client doesn't
-
either.
-
-
Example:
-
-
import os
-
from os.path import join, getsize
-
for 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)
-
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:
-
# If is_dir() raises an OSError, consider that the entry is not
-
# a directory, same behaviour than os.path.isdir().
-
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:
-
# If is_symlink() raises an OSError, consider that the
-
# entry is not a symbolic link, same behaviour than
-
# os.path.islink().
-
is_symlink =
False
-
walk_into =
not is_symlink
-
-
if walk_into:
-
walk_dirs.append(entry.path)
-
-
# Yield before recursion if going top down
-
if topdown:
-
yield top, dirs, nondirs
-
-
# Recurse into sub-directories
-
islink, join = path.islink, path.join
-
for 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-directories
-
for new_path
in walk_dirs:
-
yield
from walk(new_path, topdown, onerror, followlinks)
-
# Yield after recursion if going bottom up
-
yield 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.txt
-
-
for 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模块(续)里。
这篇文章是我看着源码凭自己理解写的,里面有一些自己的臆断,有看到错误的朋友,麻烦帮忙指出来更正,感谢!