字符: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.py
和tarfile.py
源码,发现shutil.py
都引用了tarfile.py
,而tarfile.py
也都import copy
- https://hg.python.org/cpython/file/2.7/Lib/shutil.py
- https://github.com/python/cpython/blob/3.6/Lib/shutil.py
- https://hg.python.org/cpython/file/2.7/Lib/tarfile.py
- https://github.com/python/cpython/blob/3.6/Lib/tarfile.py
那就不可能是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
是一个不会和常见的库的前缀重名的字符串,就能保证项目里面不会再发生这样的名称冲突了。
参考资料
- https://stackoverflow.com/questions/123198/how-do-i-copy-a-file-in-python
- https://stackoverflow.com/questions/22131139/attributeerror-module-object-has-no-attribute-x
- https://docs.python.org/2/library/shutil.html
- https://stackoverflow.com/questions/8350853/how-to-import-module-when-module-name-has-a-dash-or-hyphen-in-it
- https://stackoverflow.com/questions/6031584/importing-from-builtin-library-when-module-with-same-name-exists
- https://bop.molun.net/07.basics.html
- https://bop.molun.net/11.modules.html
突然发现,我只是想实现一个文件拷贝,咋就绕了这么远呢?