__import__ 可以实现模块的动态加载,Python中很多框架都使用了这种能力,如Flask的插件系统、APScheduler定时任务框架等。
这里简单看一下APScheduler定时任务框架是怎么使用 __import__ 重新载入任务对象的。
首先,补一下背景知识,简单了解一下APScheduler是如何使用的。
一开始,定义一个类。
from datetime import datetime
import random
class A(object):
def __init__(self):
self.t = self.gen_random()
def tick(self):
print('Tick! The time is: %s' % datetime.now())
print(self.t)
def gen_random(self):
return random.randint(1, 100)
我们希望A类的tick方法每3秒运行一次,通过APScheduler的实现方式如下。
if __name__ == '__main__':
scheduler = BackgroundScheduler() # 创建调度器
scheduler.add_job(A().tick, 'interval', seconds=3) # 添加一个任务,3秒后运行
scheduler.start() # 启动
print('Press Ctrl+{0} to exit'.format('Break' if os.name == 'nt' else 'C'))
try:
# 这是在这里模拟应用程序活动(使主线程保持活动状态)。
while True:
time.sleep(2)
except (KeyboardInterrupt, SystemExit):
# 关闭调度器
scheduler.shutdown()
上述代码中,我们创建了BackgroundScheduler类型的调度器,然后通过add_job()方法将A().tick作为需要被执行的任务加入,这个任务的实例默认会存储到内存中,到指定的时间,再被取出运行。
如果程序异常崩溃,内存中存储的任务也会丢失,为了避免这种情况,就需要将定时任务存储在外部,比如存储在MongoDB中。
当我们指定APScheduler后存储端使用MongoDB存储时,其具体效果如下。
{
"_id" : "brorherhood_timed_task",
"next_run_time" : 1604581734.31826,
"job_state" : { "$binary" : "gASV+gEAAAAAAAB9lCiMB3ZlcnNpb26USwGMAmlklIwWYnJvcmhlcmhvb2RfdGltZWRfdGFza5SMBGZ1bmOUjC1hcHAubG9naWNzLmJyb3RoZXJob29kOmJyb3JoZXJob29kX3RpbWVkX3Rhc2uUjAd0cmlnZ2VylIwdYXBzY2hlZHVsZXIudHJpZ2dlcnMuaW50ZXJ2YWyUjA9JbnRlcnZhbFRyaWdnZXKUk5QpgZR9lChoAUsCjAh0aW1lem9uZZSMBHB5dHqUjAJfcJSTlCiMDUFzaWEvU2hhbmdoYWmUTehxSwCMA0xNVJR0lFKUjApzdGFydF9kYXRllIwIZGF0ZXRpbWWUjAhkYXRldGltZZSTlEMKB+QLAxAINgTbOZRoDyhoEE2AcEsAjECNDU1SUdJRSlIaUUpSMCGVuZF9kYXRllE6MCGludGVydmFslGgVjAl0aW1lZGVsdGGUk5RLAE0IB0sAh5RSlIwGaml0dGVylE51YowIZXhlY3V0b3KUjAdkZWZhdWx0lIwEYXJnc5QpjAZrd2FyZ3OUfZSMBG5hbWWUaAOMEm1pc2ZpcmVfZ3JhY2VfdGltZZRLAYwIY29hbGVzY2WUiIwNbWF4X2luc3RhbmNlc5RLA4wNbmV4dF9ydW5fdGltZZRoF0MKB+QLKRUINgTbOZRoG4aUUpR1Lg==", "$type" : "00" }
}
简单而言,程序由动态执行状态转为静态状态。
如何让其从静态状态再次转为动态状态呢?这就需要使用我们的主角 __import__ 方法。
为了避免额外的复杂性,我自己编写了一个简单的示例代码来演示相同的效果。
首先,我们创建hello.py文件(python中单个py文件也称为模块),在其中创建一个简单的类,代码如下。
class HelloWorld(object):
def run(self, name):
print(f"Hello {name}!")
然后,需要将其静态化,对应模块中类的静态化需要分2步走,第一步:获得模块路径、类名等信息,第二步:是将类实例序列化的保存到本地,序列化的具体操作可以交由pickle库实现,该库简单使用方式如下。
In [1]: import pickle
In [2]: d = {'name': '二两', 'age': 30, 'hobby': 'code'}
# 将变量序列化到文件中
In [3]: with open('example.pickle', 'wb') as f:
...: pickle.dump(d, f)
...:
# 将变量反序列化到文件中
In [4]: with open('example.pickle', 'rb') as f:
...: d1 = pickle.load(f)
...:
In [5]: d1
Out[5]: {'name': '二两', 'age': 30, 'hobby': 'code'}
上述代码中,通过pickle.dump方法将字典序列化到本地,又通过pickle.load方法将其反序列化的载入,用法非常简单。
那为何不直接序列化存储类实例呢?还有获取它的模块信息和类信息干啥?
我们确实可以直接序列化类实例,但无法将其正常的反序列化,当我们开始反序列化时,会报出无法导入该类的错误,即Python解释器会尝试导入当前类实例对应的类对象,但我们没有import该类,所以会报错。
一个解决方法就是在反序列类实例的py文件头部加上相关的import,但这种方法有比较大的局限性,为此我们可以利用模块和类的信息通过 __import__ 方法将其动态导入。
为了可以动态导入类,我们需要通过如下代码实现类实例的序列化。
#所在文件:test_import1.py
import pickle
from hello import HelloWorld
def obj_to_ref(obj, args):
# 类实例方法对应的类名和方法名
# HelloWorld().run -> HelloWorld.run
name = obj.__qualname__
# 调用类方法时需要传入的self(实例本身)以及其他参数
args = (obj.__self__,) + tuple(args)
return {
'obj': f"{obj.__module__}:{name}", # 类实例所在的模块:类实例方法对应的名称
'args': args
}
ref = obj_to_ref(obj=HelloWorld().run, args=("ayuliao",))
print(ref)
# 序列化
with open('hello.pickle', 'wb') as f:
pickle.dump(ref, f)
上述代码中,获得了类实例方法「HelloWorld().run」对应的模块名、类名、方法以及类实例对象self,然后再序列化存储这些信息。
其对应反序列化动态导入的代码如下。
# 所在文件:test_import2.py
import pickle
with open('hello.pickle', 'rb') as f:
ref = pickle.load(f)
obj_ref = ref['obj']
args = ref['args']
# modulename 模块名:hello.py
# rest 类实例方法所在的路径:HelloWorld.run
modulename, rest = obj_ref.split(':', 1)
# 导入模块模块,相当于import hello
obj = __import__(modulename, fromlist=[rest])
for name in rest.split('.'):
# 获得方法本身
obj = getattr(obj, name)
# 调用方法
obj(*args)
这样,我们就实现了python模块动态导入了。
一个有趣的细节是,如果我们多次导入hello.pickle文件,获得的对象是相同的对象还是不同的对象?另一种问法,如果HelloWorld类有 __init__ 方法话,该方法会被调用一次还是多次?
答案是,导入多次,获得的是相同的对象,其 __init__ 方法并不会被调用,我们通过print(id(obj))可以获得obj对应的唯一id,可以发现是相同的。
这样好像有点不对,我都多次动态导入了,每次导入不应该都是独立的吗?那每个obj应该也是独立的呀?
其实每次导入都是独立的,但我们导入后并没有实例化HelloWorld类,而是使用hello.pickle序列化文件中的self对象作为类方法的第一个参数,即我们使用的类实例其实是同一个,所以getattr(obj, name)获得的是同一个类实例的属性方法。
因为我们没有实例化HelloWorld类,所以该类的 __init__ 方法并不会被调用,该方法在序列化时已经被调用了。
一日一技系列的特点是短,而这篇文章有点长,所以就单独成文啦,下篇一日一技我们来讨论一下如何妙用循环来实现树的遍历。
我是二两,我们下篇文章见。