假设模块文件名是data_used_to_test.py,放在tests文件夹下

文件夹结构如下:

project
    |-tests
        |-data_used_to_test.py

文件内包含一个test_class类:

class test_class():
    def test_func(arg):
        return "hello {}".format(arg)

代码全部基于 Python3.6.4

  1. 使用imp
  1. 用imp.find_module查找模块
In [1]:file, pathname, description = imp.find_module('data_used_to_test', path=['tests/'])

In [2]: file
Out[2]: <_io.TextIOWrapper name='tests/data_used_to_test.py' mode='r' encoding='utf-8'>

In [3]: pathname
Out[3]: 'tests/data_used_to_test.py'

In [4]: description
Out[4]: ('.py', 'r', 1)
  1. 用imp.load_module将找到的模块加载到sys.modules中
In [5]: mod = imp.load_module('test_load', file, pathname, description)
In [6]: mod
Out[6]: <module 'test_load' from 'tests/data_used_to_test.py'>

这时候sys.modules里会多一条'test_load'的记录,值就是mod的值。

  1. 这时候就可以直接通过mod访问包内的对象了
In [7]: mod.test_class().test_func('x')
Out[7]: 'hello x'

这个方法的优点是

  1. 简单,容易实现。
  2. 不用对pyc文件做特殊处理

缺点是可定制性太低。不适合框架使用

  1. 无法动态修改模块源代码, 开放的api必须要很稳定,不会经常变动。
  2. 可以访问的对象一开始就要制定成固定名称。无法动态注册访问。
  1. 上面的问题2可以用getattr解决,让我们更进一步。
In [8]: tmp = getattr(mod, 'test_class')

In [9]: tmp
Out[9]: test_load.test_class
In [10]: tmp().test_func('l')
Out[10]: 'hello l'

这样就可以通过事先在外部模块中调用准备好的注册函数,

把外部模块中的类或函数注册到一个全局的单例变量中,

实现动态的模块加载和对象访问。

但仍然无法解决问题 1.

所以就需要另一种模块加载方法。

  1. 显式的定义一个加载函数
import imp
import sys
    
def load_module(module_name, module_content):
    if fullname in sys.modules:
        return sys.modules[fullname]
        
    mod = sys.modules.setdefault(url, imp.new_module(module_name))
    mod.__file__ = module_name
    mod.__package__ = ''
        
    # if *.py
    code = compile(module_content, url, 'exec')
    # if *.pyc 有问题,我运行一直报错
    # code = marshal.loads(module_content[8:])
        
    exec(code, mod.__dict__)
    # 2的写法是 exec code in mod.__dict__
    # 其实就是让code在环境里运行一下,所以这里可能会有注入漏洞
    return mod

这个函数接受模块的名字和源码内容,并使用 compile() 将其编译到一个代码对象中, 然后在一个新创建的模块对象的字典中来执行它。

下面是这个函数的使用方式:

In[1]: module_content = open('tests/data_used_to_test.py').read()
    
In[2]: mod = load_module('test_import', module_content)

后面就和 1.4 一样了。

这样可以同时解决 1.3 中提出的两个问题。

因为你是先将源码作为普通文件读进来的,也就可以做各种修改后再注册到sys.modules中。

Pocsuite使用的就是这种方法。虽然我觉得他们当时可能也没太弄明白。

当然这还是有不足的。

  1. 上面的两种方法都只支持简单的模块。并没有嵌入到通常的import语句中。并不支持更高级的结构比如包。
  2. 不够酷
  1. 自定义导入器
    PythonCookbook上给了自定义导入器的两种方式
  1. 创建一个元路径导入器
  2. 编写一个钩子直接嵌入到 sys.path 变量中去

还没看明白,PEP302的文档太长了,大概知道是继承importlib.abc来做的。