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

作者: 杰克小麻雀
原文链接: https://blog.csdn.net/yushuaigee/article/details/106755148

目录

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

20200529172306852

可以看到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行,这是一段整个模块的注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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).
"""

这段注释解释了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方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#'
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行 根据系统不同导入不同的方法和属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 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]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
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行 定义三个枚举变量

1
2
3
4
5
6
# 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——创建多级目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 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/.’) ,结果出现异常如下图。

img

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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——重命名目录或文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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——目录树生成器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
def walk(top, topdown=True, onerror=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 ,则目录是自底向上生成的。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 当前目录结构:
# 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模块(续)里。

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

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2020-2021 杰克小麻雀
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信