使用自定义加载函数时模块未定义

1 投票
1 回答
1664 浏览
提问于 2025-04-18 18:09

考虑一下下面这个程序,它的作用是加载一个目录里的所有模块:

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(原因有很多,注入攻击是其中之一),所以我使用了 setattrrpartition,具体如下:

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 个回答

0

根据这个回答的建议,为了更准确地模拟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执行时,magicimho已经被加载,而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时,它会检查magicmagic.imho是否已经被加载(可以通过print sys.modules验证),然后取这个版本的magic并将其绑定到本地变量magic


相关内容:

撰写回答