内容简介:话说这个事儿距今已经差不多一年时间了(2017年底发生的事情), 当时就说要记录一下来着, 但是一直没有写, 拖到现在又想起这个事情来了, 所以翻出当时的聊天记录重新整理记录一下.事情的背景呢大概就是某(前)同事, 用 Python 写一个脚本工具的时候, 发现总是报错, 报错的那个版本的代码已不可考, 而且当时他报告的出错现象其实并不准确, 因为其实照那个方式不是百分之百再现. 当然这些都可以不用太关心, 反正最终经过整理, 发现可以稳定重现 bug 的代码大致如下:其实发现这3行代码会稳定出错的过程也
话说这个事儿距今已经差不多一年时间了(2017年底发生的事情), 当时就说要记录一下来着, 但是一直没有写, 拖到现在又想起这个事情来了, 所以翻出当时的聊天记录重新整理记录一下.
背景
事情的背景呢大概就是某(前)同事, 用 Python 写一个脚本 工具 的时候, 发现总是报错, 报错的那个版本的代码已不可考, 而且当时他报告的出错现象其实并不准确, 因为其实照那个方式不是百分之百再现. 当然这些都可以不用太关心, 反正最终经过整理, 发现可以稳定重现 bug 的代码大致如下:
# 以下版本已经是忽略掉所以无用代码仅仅展示稳定出现异常的部分 import distutils.dir_util import shutil path = "/home/lane/path" path_new = "/home/lane/path_new" # 以下3行是出错的稳定再现办法 # 使用 distutils.dir_util.copy_tree 拷贝源目录到新地址, 可以成功 distutils.dir_util.copy_tree(path, path_new) # 使用 shutil.rmtree 删除掉新的目录 shutil.rmtree(path_new) # 再次使用 distutils.dir_util.copy_tree 尝试拷贝源目录到新地址 # 此时百分之百稳定报错 # 此时报错信息类似: # IOError: [Errno 2] No such file or directory: 'home/lane/path_new/sub_folder/test.txt' distutils.dir_util.copy_tree(path, path_new)
分析
其实发现这3行代码会稳定出错的过程也是很有趣的, 但是由于代码有点多和很久了, 没法考证这部分历史了. 所以只能忽略掉了.
但是仅仅看这三行代码其实似乎也没什么出错的道理, 我既然第一次拷贝可以成功, 那么为什么第二次拷贝却会百分之百稳定报错呢? 中间我只进行了一次删除文件夹, 没有道理删掉文件夹就会报错啊, 删除文件夹又不会带来文件权限的变更.
此时仔细观察报错信息会发现一些端倪, 错误是 IOError
, 报错的地址是一个第二级目录 /path_new/sub_folder
, 其实这种情况我也遭遇过, 就是在 /home/lane/path_new
存在但是 /home/lane/path_new/sub_folder
尚未存在的情况下, 如果你尝试往 sub_folder
中拷贝 test.txt
的话, 系统就会报出类似的错误, 原因就是你尚未创建目录 sub_folder
.
那么为什么为啥第一次拷贝的时候不会报这种错误呢? 此时不得不怀疑第一次和第二次拷贝的时候, 模块 distutils.dir_util
使用的是不同的策略.
没有什么道理, 完全就是直觉, 我就直接猜测第一次 copy_tree
拷贝的时候遍历顺序始终是从顶层开始, 再保证上层的各个文件夹创建成功后, 才继续各个上层文件夹下子文件夹的操作. 同时整个 distutils.dir_util
模块会在内部保存某种信息, 使得未来调用该模块的时候可以使用.
而第二次拷贝的时候, 整个 distutils.dir_util
因为已经保存了第一次拷贝的目录就结构, 它可能就会直接尝试从底层直接拷贝文件了, 因为它认为各个层级的目录结构已然存在. 然而由于我们中间执行过一次 shutil.rmtree(path_new)
导致这些目录结构其实已经不存在了, 所以出错.
确认
有了思路其实就可以直接查看源代码确认了, 只要能在模块 distutils.dir_util
中找到某种保存目录结构信息的操作就可以证明我的猜测了.
那么我们直接找到 distutils/dir_util.py
打开后查看代码.
其实源文件一上来就有这么个全局变量:
# cache for by mkpath() -- in addition to cheapening redundant calls, # eliminates redundant "creating /foo/bar/baz" messages in dry-run mode _path_created = {}
这个其实就是我要找的保存目录结构信息的变量. 当然我当时是没觉得有啥, 我还是记录下当时的查找过程.
先直接找到 copy_tree
函数:
def copy_tree(src, dst, preserve_mode=1, preserve_times=1, preserve_symlinks=0, update=0, verbose=1, dry_run=0): """Copy an entire directory tree 'src' to a new location 'dst'. Both 'src' and 'dst' must be directory names. If 'src' is not a directory, raise DistutilsFileError. If 'dst' does not exist, it is created with 'mkpath()'. The end result of the copy is that every file in 'src' is copied to 'dst', and directories under 'src' are recursively copied to 'dst'. Return the list of files that were copied or might have been copied, using their output name. The return value is unaffected by 'update' or 'dry_run': it is simply the list of all files under 'src', with the names changed to be under 'dst'. 'preserve_mode' and 'preserve_times' are the same as for 'copy_file'; note that they only apply to regular files, not to directories. If 'preserve_symlinks' is true, symlinks will be copied as symlinks (on platforms that support them!); otherwise (the default), the destination of the symlink will be copied. 'update' and 'verbose' are the same as for 'copy_file'. """ from distutils.file_util import copy_file if not dry_run and not os.path.isdir(src): raise DistutilsFileError, \ "cannot copy tree '%s': not a directory" % src try: names = os.listdir(src) except os.error, (errno, errstr): if dry_run: names = [] else: raise DistutilsFileError, \ "error listing files in '%s': %s" % (src, errstr) if not dry_run: mkpath(dst, verbose=verbose) outputs = [] for n in names: src_name = os.path.join(src, n) dst_name = os.path.join(dst, n) if n.startswith('.nfs'): # skip NFS rename files continue if preserve_symlinks and os.path.islink(src_name): link_dest = os.readlink(src_name) if verbose >= 1: log.info("linking %s -> %s", dst_name, link_dest) if not dry_run: os.symlink(link_dest, dst_name) outputs.append(dst_name) elif os.path.isdir(src_name): outputs.extend( copy_tree(src_name, dst_name, preserve_mode, preserve_times, preserve_symlinks, update, verbose=verbose, dry_run=dry_run)) else: copy_file(src_name, dst_name, preserve_mode, preserve_times, update, verbose=verbose, dry_run=dry_run) outputs.append(dst_name) return outputs
看了一圈, 没法先有啥保存的操作. 但是函数中, 除了递归调用自己以外, 还调用了一个 mkpath
:
if not dry_run: mkpath(dst, verbose=verbose)
那么我们又观察一下 mkpath
好了:
def mkpath(name, mode=0777, verbose=1, dry_run=0): """Create a directory and any missing ancestor directories. If the directory already exists (or if 'name' is the empty string, which means the current directory, which of course exists), then do nothing. Raise DistutilsFileError if unable to create some directory along the way (eg. some sub-path exists, but is a file rather than a directory). If 'verbose' is true, print a one-line summary of each mkdir to stdout. Return the list of directories actually created. """ global _path_created # Detect a common bug -- name is None if not isinstance(name, basestring): raise DistutilsInternalError, \ "mkpath: 'name' must be a string (got %r)" % (name,) # XXX what's the better way to handle verbosity? print as we create # each directory in the path (the current behaviour), or only announce # the creation of the whole path? (quite easy to do the latter since # we're not using a recursive algorithm) name = os.path.normpath(name) created_dirs = [] if os.path.isdir(name) or name == '': return created_dirs if _path_created.get(os.path.abspath(name)): return created_dirs (head, tail) = os.path.split(name) tails = [tail] # stack of lone dirs to create while head and tail and not os.path.isdir(head): (head, tail) = os.path.split(head) tails.insert(0, tail) # push next higher dir onto stack # now 'head' contains the deepest directory that already exists # (that is, the child of 'head' in 'name' is the highest directory # that does *not* exist) for d in tails: #print "head = %s, d = %s: " % (head, d), head = os.path.join(head, d) abs_head = os.path.abspath(head) if _path_created.get(abs_head): continue if verbose >= 1: log.info("creating %s", head) if not dry_run: try: os.mkdir(head, mode) except OSError, exc: if not (exc.errno == errno.EEXIST and os.path.isdir(head)): raise DistutilsFileError( "could not create '%s': %s" % (head, exc.args[-1])) created_dirs.append(head) _path_created[abs_head] = 1 return created_dirs
在这里就能发现多次使用了一个叫做 _path_created
的全局变量, 它会保存已经创建过的目录的信息. 如果已经创建过了, 那么就不会重复创建了:
if _path_created.get(os.path.abspath(name)): return created_dirs
所以这就很好解释了, 第一次运行的时候, 这个全局变量为空, 每一个文件夹都会被依次创建, 然后相应的记录会存在 _path_created
全局变量里. 然而第二次运行的时候, 每次尝试创建的时候都会发现这个全局变量里已经有值了, 而不会去真的创建, 而是直接跳过, 直到真的开始拷贝文件的时候才会发现文件夹不存在, 因为已经被 shutil.rmtree(path_new)
这一句话删掉了. 但是模块 distutils.dir_util
并不知道啊.
继续查找变量 _path_created
还可以发现, remove_tree
函数里使用了这个全局变量:
abspath = os.path.abspath(cmd[1]) if abspath in _path_created: del _path_created[abspath]
由此可见, 如果你使用同一模块下的 remove_tree
函数去做文件夹的删除的话, 该模块是可以感知到的. 因为它能相应的删除全局变量中的记录.
改进 & 总结
知道了原因, 可以尝试改进来使之不出错, 根据上面的分析, 只要把调用 shutil.rmtree
改成调用 distutils.dir_util.remove_tree
即可. 原因是使用同一模块自己的 remove_tree
函数可以清除掉其中全局变量存储的数据, 即让模块感知到相应的文件夹已经不存在了. 这样才能在第二次的时候正常的去创建文件夹.
修改后的代码如下:
import distutils.dir_util path = "/home/lane/path" path_new = "/home/lane/path_new" # 使用 distutils.dir_util.copy_tree 拷贝源目录到新地址, 可以成功 distutils.dir_util.copy_tree(path, path_new) # 使用 shutil.rmtree 删除掉新的目录 distutils.dir_util.remove_tree(path_new) # 再次使用 distutils.dir_util.copy_tree 尝试拷贝源目录到新地址 # 此时不再会报错, 第二次仍然可以成功 distutils.dir_util.copy_tree(path, path_new)
总结下来, 大概就是不要混用不同模块的创建删除函数, 否则可能会导致一些意想不到的情况. 比如本文总结的情况. 当然这个模块的这种做法是不是特别好我也不清楚, 缓存下来的文件夹创建记录真的能节约很多时间吗? 这个我没法验证了.
以上所述就是小编给大家介绍的《记一次 Python distutils.dir_util 模块 debug》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Node.js模块系统 (创建模块与加载模块)
- 黑客基础,Metasploit模块简介,渗透攻击模块、攻击载荷模块
- 022.Python模块序列化模块(json,pickle)和math模块
- 024.Python模块OS模块
- 023.Python的随机模块和时间模块
- Laravel 模块化开发模块 – Caffienate
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
我看电商3:零售的变革
黄若 / 电子工业出版社 / 2018-4 / 49
在《我看电商3:零售的变革》之前,黄若先生的“我看电商”系列图书《我看电商》《再看电商》《我看电商2》,均为行业畅销书。黄若先生的图书有两大特如一是干货满满,二是观点鲜明。 “新零售”是眼下的热门词。在2017年里,数以万计的企业以“新零售”作为标识进入市场。但是社会上对“新零售“存在着各种模糊的定义和不尽相同的解读。 《我看电商3:零售的变革》中明确提出:新零售不应过分关注于渠道形式......一起来看看 《我看电商3:零售的变革》 这本书的介绍吧!