__import__动态加载技术

__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__ 方法并不会被调用,该方法在序列化时已经被调用了。

一日一技系列的特点是短,而这篇文章有点长,所以就单独成文啦,下篇一日一技我们来讨论一下如何妙用循环来实现树的遍历。

我是二两,我们下篇文章见。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

懒编程-二两

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值