使用自定义加载函数时模块未定义
考虑一下下面这个程序,它的作用是加载一个目录里的所有模块:
import pkgutil
import os.path
import sys
def load_all(directory):
for loader, name, ispkg in pkgutil.walk_packages([directory]):
loader.find_module(name).load_module(name)
if __name__ == '__main__':
here = os.path.dirname(os.path.realpath(__file__))
path = os.path.join(here, 'lib')
sys.path.append(path)
load_all(path)
现在我们来看一下下面这些文件。
lib/magic/imho.py:
"""This is an empty module.
"""
lib/magic/wtf.py:
import magic.imho
print "magic contents:", dir(magic)
lib/magic/__init__.py:
"Empty package init file"
当运行上面的程序时,它输出了:
magic contents: ['__builtins__', '__doc__', '__file__', '__name__', '__package__', '__path__']
换句话说,尽管我们有 import magic.imho
,但是在 magic
这个包里并没有 imho
这个属性,这就导致后面如果想用 magic.imho
的话会出错。
如果直接在 Python 中运行差不多相同的代码,输出结果会有所不同,这也是我在运行加载程序时所期待的结果。
$ PYTHONPATH=lib ipython
Python 2.7.6 (default, Mar 22 2014, 22:59:38)
Type "copyright", "credits" or "license" for more information.
IPython 1.2.1 -- An enhanced Interactive Python.
? -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help -> Python's own help system.
object? -> Details about 'object', use 'object??' for extra details.
In [1]: import magic.wtf
magic contents: ['__builtins__', '__doc__', '__file__', '__name__', '__package__', '__path__', 'imho']
为什么会有这样的差异呢?
更新: 这里的关键是,尽管 magic.imho
被明确导入了,但它在 magic.wtf
中并不存在。因此,我们需要对自定义的加载过程做些什么,才能让这个符号在被导入到 magic.wtf
后可见。
使用 setattr
的解决方案: BartoszKP 提供了下面的解决方案,使用 exec
来给包赋值。因为我通常不喜欢使用 exec
(原因有很多,注入攻击是其中之一),所以我使用了 setattr
和 rpartition
,具体如下:
def load_all(directory):
for loader, name, ispkg in pkgutil.walk_packages([directory]):
module = loader.find_module(name).load_module(name)
pkg_name, _, mod_name = name.rpartition('.')
if pkg_name:
setattr(sys.modules[pkg], mod_name, module)
1 个回答
根据这个回答的建议,为了更准确地模拟import
的行为,你应该这样修改你的load_all
方法:
def load_all(directory):
for loader, name, ispkg in pkgutil.walk_packages([directory]):
module = loader.find_module(name).load_module(name)
exec('%s = module' % name)
这样做的话,wtf.py的名字就会正确地绑定在magic
里面。所以下次再导入magic
时,它就不会重新加载了(因为这个函数已经把它放进了sys.modules
),而且它的所有子模块也会被正确地分配。
这似乎解决了import
机制中的一些怪异情况,我在下面尝试分析了一下。
实际上,差异主要来自于模块加载的顺序。在第一种情况下,模块的加载顺序和第二种情况是不同的。当你在每个模块中添加print
语句时,执行主脚本时会看到以下内容:
加载 magic
加载 imho
加载 wtf
magic 内容: ['__builtins__', '__doc__', '__file__', '__name__', '__package__', '__path__']
对于第二种情况:
加载 magic
加载 wtf
加载 imho
magic 内容: ['__builtins__', '__doc__', '__file__', '__name__', '__package__', '__path__', 'imho']
所以,在第一种情况下,当wtf.py执行时,magic
和imho
已经被加载,而import
语句不会重新加载已经加载的模块:
首先,如果模块已经存在于 sys.modules 中(如果加载器在导入机制之外被调用,这种情况是可能的),那么它会使用那个模块进行初始化,而不是一个新的模块。
如果你这样修改你的主脚本:
if __name__ == '__main__':
here = os.path.dirname(os.path.realpath(__file__))
path = os.path.join(here, 'lib')
sys.path.append(path)
import magic.wtf
它的工作方式和从解释器中运行是完全一样的。
如果你像下面这样修改wtf.py,你的原始脚本也会按预期工作(为了演示的目的):
import sys
try:
del sys.modules['magic.imho']
except:
pass
import magic.imho
print "magic contents:", dir(magic)
或者这样:
import sys
import magic.imho
magic.imho = sys.modules['magic.imho']
print "magic contents:", dir(magic)
输出:
加载 magic
加载 imho
加载 wtf
加载 imho
magic 内容: ['__builtins__', '__doc__', '__file__', '__name__', '__package__', '__path__', 'imho']
注意:一个模块在sys.modules
中存在是因为它被加载过,这和被导入是不同的。在第一种情况下,它是加载过的,但并没有被导入。所以它存在于sys.modules
中,但不在当前的命名空间中。因此我猜测它是被你的加载器加载的,但没有将imho
绑定到它(所以它只是一个裸的magic
模块)。之后,当Python看到import magic.imho
时,它会检查magic
和magic.imho
是否已经被加载(可以通过print sys.modules
验证),然后取这个版本的magic
并将其绑定到本地变量magic
。
相关内容:
Python导入系统中的陷阱(你不应该将本地包添加到
sys.path
)