动态加载___import__动态加载技术
__import__ 可以實(shí)現(xiàn)模塊的動(dòng)態(tài)加載,Python中很多框架都使用了這種能力,如Flask的插件系統(tǒng)、APScheduler定時(shí)任務(wù)框架等。
這里簡(jiǎn)單看一下APScheduler定時(shí)任務(wù)框架是怎么使用 __import__ 重新載入任務(wù)對(duì)象的。
首先,補(bǔ)一下背景知識(shí),簡(jiǎn)單了解一下APScheduler是如何使用的。
一開始,定義一個(gè)類。
from?datetime?import?datetimeimport?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秒運(yùn)行一次,通過(guò)APScheduler的實(shí)現(xiàn)方式如下。
if?__name__?==?'__main__':????scheduler?=?BackgroundScheduler()?#?創(chuàng)建調(diào)度器
????scheduler.add_job(A().tick,?'interval',?seconds=3)?#?添加一個(gè)任務(wù),3秒后運(yùn)行
????scheduler.start()?#?啟動(dòng)
????print('Press?Ctrl+{0}?to?exit'.format('Break'?if?os.name?==?'nt'?else?'C'))
????try:
????????#?這是在這里模擬應(yīng)用程序活動(dòng)(使主線程保持活動(dòng)狀態(tài))。
????????while?True:
????????????time.sleep(2)
????except?(KeyboardInterrupt,?SystemExit):
????????#?關(guān)閉調(diào)度器
????????scheduler.shutdown()
上述代碼中,我們創(chuàng)建了BackgroundScheduler類型的調(diào)度器,然后通過(guò)add_job()方法將A().tick作為需要被執(zhí)行的任務(wù)加入,這個(gè)任務(wù)的實(shí)例默認(rèn)會(huì)存儲(chǔ)到內(nèi)存中,到指定的時(shí)間,再被取出運(yùn)行。
如果程序異常崩潰,內(nèi)存中存儲(chǔ)的任務(wù)也會(huì)丟失,為了避免這種情況,就需要將定時(shí)任務(wù)存儲(chǔ)在外部,比如存儲(chǔ)在MongoDB中。
當(dāng)我們指定APScheduler后存儲(chǔ)端使用MongoDB存儲(chǔ)時(shí),其具體效果如下。
{????"_id"?:?"brorherhood_timed_task",
????"next_run_time"?:?1604581734.31826,
????"job_state"?:?{?"$binary"?:?"gASV+gEAAAAAAAB9lCiMB3ZlcnNpb26USwGMAmlklIwWYnJvcmhlcmhvb2RfdGltZWRfdGFza5SMBGZ1bmOUjC1hcHAubG9naWNzLmJyb3RoZXJob29kOmJyb3JoZXJob29kX3RpbWVkX3Rhc2uUjAd0cmlnZ2VylIwdYXBzY2hlZHVsZXIudHJpZ2dlcnMuaW50ZXJ2YWyUjA9JbnRlcnZhbFRyaWdnZXKUk5QpgZR9lChoAUsCjAh0aW1lem9uZZSMBHB5dHqUjAJfcJSTlCiMDUFzaWEvU2hhbmdoYWmUTehxSwCMA0xNVJR0lFKUjApzdGFydF9kYXRllIwIZGF0ZXRpbWWUjAhkYXRldGltZZSTlEMKB+QLAxAINgTbOZRoDyhoEE2AcEsAjECNDU1SUdJRSlIaUUpSMCGVuZF9kYXRllE6MCGludGVydmFslGgVjAl0aW1lZGVsdGGUk5RLAE0IB0sAh5RSlIwGaml0dGVylE51YowIZXhlY3V0b3KUjAdkZWZhdWx0lIwEYXJnc5QpjAZrd2FyZ3OUfZSMBG5hbWWUaAOMEm1pc2ZpcmVfZ3JhY2VfdGltZZRLAYwIY29hbGVzY2WUiIwNbWF4X2luc3RhbmNlc5RLA4wNbmV4dF9ydW5fdGltZZRoF0MKB+QLKRUINgTbOZRoG4aUUpR1Lg==",?"$type"?:?"00"?}
}
簡(jiǎn)單而言,程序由動(dòng)態(tài)執(zhí)行狀態(tài)轉(zhuǎn)為靜態(tài)狀態(tài)。
如何讓其從靜態(tài)狀態(tài)再次轉(zhuǎn)為動(dòng)態(tài)狀態(tài)呢?這就需要使用我們的主角 __import__ 方法。
為了避免額外的復(fù)雜性,我自己編寫了一個(gè)簡(jiǎn)單的示例代碼來(lái)演示相同的效果。
首先,我們創(chuàng)建hello.py文件(python中單個(gè)py文件也稱為模塊),在其中創(chuàng)建一個(gè)簡(jiǎn)單的類,代碼如下。
class?HelloWorld(object):????def?run(self,?name):
????????print(f"Hello?{name}!")
然后,需要將其靜態(tài)化,對(duì)應(yīng)模塊中類的靜態(tài)化需要分2步走,第一步:獲得模塊路徑、類名等信息,第二步:是將類實(shí)例序列化的保存到本地,序列化的具體操作可以交由pickle庫(kù)實(shí)現(xiàn),該庫(kù)簡(jiǎn)單使用方式如下。
In?[1]:?import?pickleIn?[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'}
上述代碼中,通過(guò)pickle.dump方法將字典序列化到本地,又通過(guò)pickle.load方法將其反序列化的載入,用法非常簡(jiǎn)單。
那為何不直接序列化存儲(chǔ)類實(shí)例呢?還有獲取它的模塊信息和類信息干啥?
我們確實(shí)可以直接序列化類實(shí)例,但無(wú)法將其正常的反序列化,當(dāng)我們開始反序列化時(shí),會(huì)報(bào)出無(wú)法導(dǎo)入該類的錯(cuò)誤,即Python解釋器會(huì)嘗試導(dǎo)入當(dāng)前類實(shí)例對(duì)應(yīng)的類對(duì)象,但我們沒(méi)有import該類,所以會(huì)報(bào)錯(cuò)。
一個(gè)解決方法就是在反序列類實(shí)例的py文件頭部加上相關(guān)的import,但這種方法有比較大的局限性,為此我們可以利用模塊和類的信息通過(guò) __import__ 方法將其動(dòng)態(tài)導(dǎo)入。
為了可以動(dòng)態(tài)導(dǎo)入類,我們需要通過(guò)如下代碼實(shí)現(xiàn)類實(shí)例的序列化。
#所在文件:test_import1.pyimport?pickle
from?hello?import?HelloWorld
def?obj_to_ref(obj,?args):
????#?類實(shí)例方法對(duì)應(yīng)的類名和方法名
????#?HelloWorld().run?->?HelloWorld.run
????name?=?obj.__qualname__?
????#?調(diào)用類方法時(shí)需要傳入的self(實(shí)例本身)以及其他參數(shù)
????args?=?(obj.__self__,)?+?tuple(args)?
????return?{
????????'obj':?f"{obj.__module__}:{name}",?#?類實(shí)例所在的模塊:類實(shí)例方法對(duì)應(yīng)的名稱
????????'args':?args
????}
ref?=?obj_to_ref(obj=HelloWorld().run,?args=("ayuliao",))
print(ref)
#?序列化
with?open('hello.pickle',?'wb')?as?f:
????pickle.dump(ref,?f)
上述代碼中,獲得了類實(shí)例方法「HelloWorld().run」對(duì)應(yīng)的模塊名、類名、方法以及類實(shí)例對(duì)象self,然后再序列化存儲(chǔ)這些信息。
其對(duì)應(yīng)反序列化動(dòng)態(tài)導(dǎo)入的代碼如下。
#?所在文件:test_import2.pyimport?pickle
with?open('hello.pickle',?'rb')?as?f:
????ref?=?pickle.load(f)
????
obj_ref?=?ref['obj']
args?=?ref['args']
# modulename 模塊名:hello.py
# rest 類實(shí)例方法所在的路徑:HelloWorld.run
modulename,?rest?=?obj_ref.split(':',?1)
#?導(dǎo)入模塊模塊,相當(dāng)于import?hello
obj?=?__import__(modulename,?fromlist=[rest])
for?name?in?rest.split('.'):
????#?獲得方法本身
????obj?=?getattr(obj,?name)
#?調(diào)用方法
obj(*args)
這樣,我們就實(shí)現(xiàn)了python模塊動(dòng)態(tài)導(dǎo)入了。
一個(gè)有趣的細(xì)節(jié)是,如果我們多次導(dǎo)入hello.pickle文件,獲得的對(duì)象是相同的對(duì)象還是不同的對(duì)象?另一種問(wèn)法,如果HelloWorld類有 __init__ 方法話,該方法會(huì)被調(diào)用一次還是多次?
答案是,導(dǎo)入多次,獲得的是相同的對(duì)象,其 __init__ 方法并不會(huì)被調(diào)用,我們通過(guò)print(id(obj))可以獲得obj對(duì)應(yīng)的唯一id,可以發(fā)現(xiàn)是相同的。
這樣好像有點(diǎn)不對(duì),我都多次動(dòng)態(tài)導(dǎo)入了,每次導(dǎo)入不應(yīng)該都是獨(dú)立的嗎?那每個(gè)obj應(yīng)該也是獨(dú)立的呀?
其實(shí)每次導(dǎo)入都是獨(dú)立的,但我們導(dǎo)入后并沒(méi)有實(shí)例化HelloWorld類,而是使用hello.pickle序列化文件中的self對(duì)象作為類方法的第一個(gè)參數(shù),即我們使用的類實(shí)例其實(shí)是同一個(gè),所以getattr(obj, name)獲得的是同一個(gè)類實(shí)例的屬性方法。
因?yàn)槲覀儧](méi)有實(shí)例化HelloWorld類,所以該類的 __init__ 方法并不會(huì)被調(diào)用,該方法在序列化時(shí)已經(jīng)被調(diào)用了。
一日一技系列的特點(diǎn)是短,而這篇文章有點(diǎn)長(zhǎng),所以就單獨(dú)成文啦,下篇一日一技我們來(lái)討論一下如何妙用循環(huán)來(lái)實(shí)現(xiàn)樹的遍歷。
我是二兩,我們下篇文章見。
總結(jié)
以上是生活随笔為你收集整理的动态加载___import__动态加载技术的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 重磅回归之作!魅族20和20 PRO正式
- 下一篇: 文字描边_学会这种描边效果,你的PPT也