C 语言中可以使用 dlopen,dlsym 和 dlclose 让程序在运行过程中按需加载和卸载动态库。Python 也支持这种方式,使用模块动态加载技术,我们可以把程序的配置文件写成可运行的 python 程序,在程序运行过程中可以动态去更新配置。当然也可以将 python 脚本作为业务逻辑加载到正在运行的主程序中,而不用重启服务。
作者在个人项目 pyed 中使用了这种技术,本文对个人研究和使用这种技术的一个总结。如有问题,欢迎大家讨论。
python 提供了 exec 用于在程序中执行一段 python 代码,官方说明:
exec_stmt ::= "exec" or_expr ["in" expression ["," expression]]
该语句可以使用 exec() 函数进行替代。来看一个简单的例子:
>>> exec "print('Hello World')"
Hello World
>>>
这种使用方式,在程序中其实作用不大,我们使用动态加载,一般是希望将一个模块中的某个变量或函数按需引入到正在执行的程序中,而不仅仅是去执行一下,打印一句 “Hello World”,exec 中的 in 解决了这个问题。
in 的作用是将执行代码中的变量,函数或者类放入到一个字典中,这里再来看一个例子:
>>> exec "a=100" in tmp
>>> print tmp
{'__builtins__': ..., 'a': 100}
>>>
上面的语句等效于:
exec("a=100", tmp)
执行结果中,tmp 除了我们给定的一个 a 变量,赋值为 100 外,还有一个 __builtins__ 成员,内容很多,这里使用 … 替代了实际的内容。如果要访问 a 的值,只需要像操作字典一样就行了:
>>> print tmp["a"]
100
>>>
按照上面的思路,我们构造了一个模块
import traceback
class loader(object):
def __init__(self):
pass
def load(self, path):
try:
tmp = {}
exec open(path).read() in tmp
return tmp
except:
print("Load module [path %s] error: %s"
% (path, traceback.format_exc()))
return None
有一个配置文件 test.conf:
$ cat test.conf
addr="127.0.0.1"
port=2539
$
使用以下代码加载它:
load = loader()
m = load.load("test.conf")
addr = m["addr"]
port = m["port"]
print addr + ":" + str(port)
执行结果:
$ python loader.py
127.0.0.1:2539
$
如果要执行加载模块(test.py)中的函数:
def greeting(name):
print "Hello", name
使用以下代码加载它:
load = loader()
m = load.load("test.py")
func = m["greeting"]
func("World")
执行结果:
$ python loader.py
Hello World
$
按照上面的思路,如果加载的模块是一个类,其实调用方式也是大同小异的。
修改 test.py
class test(object):
def __init__(self):
pass
def greeting(self, name):
print "Hello", name
使用以下代码加载它:
load = loader()
m = load.load("test.py")
c = m["test"]
print c
print dir(c)
t = c()
t.greeting("World")
执行结果:
$ python loader.py
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'greeting']
Hello World
$
从上面可以看到 m[“test”] 是一个 class 类型,我们可以使用它创建类的实例,并调用实例方法
如果在加载的模块中导入了其它模块,调用方法也是不变的。我们引入一个 test1,继承上例中的 test:
from test import test
class test1(test):
def __init__(self):
test.__init__(self)
使用以下代码加载它:
load = loader()
m = load.load("subtest.py")
c = m["test1"]
print c
print dir(c)
t = c()
t.greeting("World")
执行结果:
$ python loader.py
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'greeting']
Hello World
$
上一节介绍了使用 exec … in … 的方式动态去加载模块。完成后可以直接使用返回的字典,访问模块中的变量,函数和类。但是从习惯上,我们更习惯使用模块去调用模块中的变量,函数和类,按此思路,我们对前面的模块加载器进行修改。
import traceback, types
class loader(object):
def __init__(self):
pass
def load(self, name, path):
try:
m = types.ModuleType(name)
exec open(path).read() in m.__dict__
return m
except:
print("Load module [path %s] error: %s"
% (path, traceback.format_exc()))
return None
这里使用 types.ModuleType 来构造一个模块 m,将 exec 生成的字典放入到 m.__dict__。这样就生成了一个简单的模块
待加载的模块:
def test():
s = 0
for i in range(1000000):
s += i
print s
执行逻辑:
load = loader()
m = load.load("test", "test.py")
print m
print m.__dict__
m.test()
执行结果:
$ python loader.py
{'__builtins__': ..., '__name__': 'test', 'test': , '__doc__': None}
499999500000
$
从执行结果,我们可以看到使用新的模块加载器,我们得到的是一个 module 类型的实例,其 __dict__ 中包含了 test 函数,我们可以直接使用 m.test() 调用该函数