python错误:module ‘shutil’ has no attribute ‘copy’

字符:5706
平移阅读时长:7分钟

今天在写一个小脚本来把自己写的一个html批量复制并在保持后缀不变的情况下随机命名。

先打算实现文件的拷贝。

Google了一下,发现了一个shutil的库(这个库的名字纠结了发音老半天,后面才理解是sh和util的结合,也就是shell-util)

这个库提供了与shell命令操作文件等同的所有方法:copy, copytree, rmtree, move

代码实现

有了这个库,代码很容易就实现了:

import shutil

src = 'sa-jumper.html'

randomStr = 'ad239sdfasdl2asdf9adsfi23dfladf'
strLen = len(randomStr)

shutil.copy(src, 'tmp/aa.html')

关于随机的部分,就只是提前做了准备。先用的shutil.copy来测试文件拷贝功能。

然后我先用的python3.6执行了这个文件,结果报错:

Traceback (most recent call last):
  File "copy.py", line 2, in <module>
    import shutil
  File "/usr/local/Cellar/python3/3.6.0_1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/shutil.py", line 13, in <module>
    import tarfile
  File "/usr/local/Cellar/python3/3.6.0_1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/tarfile.py", line 49, in <module>
    import copy
  File "/Library/WebServer/Documents/xxx/copy.py", line 13, in <module>
    shutil.copy(src, 'tmp/aa.html')
AttributeError: module 'shutil' has no attribute 'copy'

后面改用python2.7执行,顺利通过。

debug

打开Python的解释器提示符,执行上述代码,也是成功的。

这下我就没辙了,感觉个人的Python经验只能到此为止,启用Google大法。

在StackOverflow的一篇文章上,发现有人遇到类似的问题,链接如下:

https://stackoverflow.com/questions/22131139/attributeerror-module-object-has-no-attribute-x

在上述链接里面,最终的错误是因为提问的人,python脚本的名称是org.py跟系统的一个文件重名,导致了一个错误的引用。

所以很自然的,我就联想到了我的文件,名字是copy.py,而看上面的错误信息里面,是用一个import copy,估计也是同样错误的把我的脚本当作官方库给错误引用了。

修复

把文件名改成了copy-file.py,再用python3.6执行,一切顺利。

分析

从这个错误里面想到几个问题:

  • 为啥跟官方库里面的文件同名会导致引用错误
  • 为啥python2.7没报错
  • 如何避免这样的问题

关于引用错误

重新翻了一下之前看的《Python简明教程》

关于模块的索引

如果它不是一个已编译好的模块,即用 Python 编写的模块,那么 Python 解释器将从它的 sys.path 变量所提供的目录中进行搜索。如果找到了对应模块,则该模块中的语句将在开始运行,并能够为你所使用。在这里需要注意的是,初始化工作只需在我们第一次导入模块时完成。

关于索引的顺序

sys.path 内包含了导入模块的字典名称列表。你能观察到 sys.path 的第一段字符串是空的——这一空字符串代表当前目录也是 sys.path 的一部分,它与 PYTHONPATH 环境变量等同。这意味着你可以直接导入位于当前目录的模块。否则,你必须将你的模块放置在 sys.path 内所列出的目录中。

从这里可以看到,执行的脚本所在的目录也是在python搜索模块的范围内的,而且是第一位,也就是会优先搜索的,这也就是为啥同名会导致引用错误的原因了。

为啥2.7没错

按照上面StackOverflow那边文章的分析过程,我分别看了python3.6和python2.7版本里面shutil.pytarfile.py源码,发现shutil.py都引用了tarfile.py,而tarfile.py也都import copy

那就不可能是2.7里面没有引用copy了。

然后我又仔细的看了一下各自的实现,终于发现了问题,在2.7里面,不是在初始化的时候就import了tarfile,而是在一个_make_tarball 的函数里面。

我在这个函数的import tarfile前面加了一个print('import tarfile'),再去执行刚刚的copy.py(这块为了测试方便,把copy-file.py复制了一份重命名为copy.py)。

发现没有print任何内容。

而在3.6里面,是在文件头部,一开始就import了tarfile。这也就解释了为啥2.7没问题,3.6有问题了。同时也说明:python的import是按需加载的。写在函数里面的import,只有在函数被调用的时候才会真实的去import

为了反证这个推断是否有效,我找到了2.7里面shutil的一个会触发import tarfile的函数make_archive

使用了官方提供的example:

import shutil
import os
archive_name = os.path.expanduser(os.path.join('~', 'myarchive'))
root_dir = os.path.expanduser(os.path.join('~', '.ssh'))
shutil.make_archive(archive_name, 'gztar', root_dir)

执行python copy.py(我的环境里面,默认是2.7)

果然报错了:

import tarfile
import tarfile
Traceback (most recent call last):
  File "copy.py", line 20, in <module>
    shutil.make_archive(archive_name, 'gztar', root_dir)
  File "/usr/local/Cellar/python/2.7.13/Frameworks/Python.framework/Versions/2.7/lib/python2.7/shutil.py", line 561, in make_archive
    filename = func(base_name, base_dir, **kwargs)
  File "/usr/local/Cellar/python/2.7.13/Frameworks/Python.framework/Versions/2.7/lib/python2.7/shutil.py", line 376, in _make_tarball
    import tarfile  # late import so Python build itself doesn't break
  File "/usr/local/Cellar/python/2.7.13/Frameworks/Python.framework/Versions/2.7/lib/python2.7/tarfile.py", line 52, in <module>
    import copy
  File "/Library/WebServer/Documents/xxx/copy.py", line 20, in <module>
    shutil.make_archive(archive_name, 'gztar', root_dir)
  File "/usr/local/Cellar/python/2.7.13/Frameworks/Python.framework/Versions/2.7/lib/python2.7/shutil.py", line 561, in make_archive
    filename = func(base_name, base_dir, **kwargs)
  File "/usr/local/Cellar/python/2.7.13/Frameworks/Python.framework/Versions/2.7/lib/python2.7/shutil.py", line 394, in _make_tarball
    tar = tarfile.open(archive_name, 'w|%s' % tar_compression[compress])
AttributeError: 'module' object has no attribute 'open'

然后我尝试把文件名修改为copy-file.py,再执行,顺利通过。

看来2.7和3.6都是一样的索引模块的方式,只是因为shutil模块里面引用tarfile的方式不一样,才造成一开始的执行结果不一样。

如何避免这样的问题

我还是一个python新手,不知道python在命名的时候有没有提供一些合理的规范,比如不能使用的单词。

然后我又一次看了《简明Python教程》,里面关于标识符命名这块:

量是标识符的一个例子。标识符(Identifiers) 是为 某些东西 提供的给定名称。在你命名标识符时,你需要遵守以下规则:
• 第一个字符必须是字母表中的字母(大写 ASCII 字符或小写 ASCII 字符或 Unicode 字符)或下划线()。
• 标识符的其它部分可以由字符(大写 ASCII 字符或小写 ASCII 字符或 Unicode 字符)、下划线(
)、数字(0~9)组成。
• 标识符名称区分大小写。例如,myname 和 myName 并不等同。要注意到前者是小写字母 n 而后者是大写字母 N。
• 有效 的标识符名称可以是 i 或 name_2_3 ,无效 的标识符名称可能是 2things,this is spaced out,my-name 和 >a1b2_c3。

我想,如果我以非这个规范的方式定义文件名是不是就可以了。

然而用类似import copy-file的方式引用自己的模块,会报错,SyntaxError

也有看到有相关的解决方式:

  • python2.7 execfile('foo-bar.py')
  • python3.x exec(open(fn).read())

不过这些方法感觉都不是很自然,还是放弃这样的尝试了。

只能使用另一种方式了,就是统一文件名的前缀。如果你的项目是foo,那么可以给所有项目的文件定义以这个为前缀的名称: foo__copy.py, foo_shutil.py等等。

这样只要foo是一个不会和常见的库的前缀重名的字符串,就能保证项目里面不会再发生这样的名称冲突了。

参考资料

突然发现,我只是想实现一个文件拷贝,咋就绕了这么远呢?

发表评论