本文是給有一點(diǎn) Python 基礎(chǔ)但還想進(jìn)一步深入的同學(xué)复亏,有經(jīng)驗(yàn)的開(kāi)發(fā)者建議跳過(guò)绢彤。
前言
上文講述了如何認(rèn)識(shí)開(kāi)源項(xiàng)目和一些編程方法的介紹,這節(jié)主要來(lái)說(shuō)說(shuō) Python 代碼怎么寫的一些演化過(guò)程和可以如何寫的參考蜓耻,在現(xiàn)在的 Sipeed 開(kāi)源社區(qū)/社群里茫舶,有太多的新手不知道如何寫好 Python 代碼,尤其是嵌入式中的 Python 代碼也是有不少的技巧和觀念需要注意的刹淌,至少讓這篇文章從循環(huán)開(kāi)始說(shuō)起饶氏。
可以把本文當(dāng)作一篇經(jīng)驗(yàn)之談,主要是探討代碼穩(wěn)定性與性能有勾,以及一些計(jì)算機(jī)知識(shí)的拓展疹启。
循環(huán)執(zhí)行代碼
當(dāng)寫下第一行代碼的時(shí)候,在電腦上的 Python 解釋器運(yùn)行效果是這樣的蔼卡。
print('Hello World')
而嵌入式設(shè)備上的 python 是通過(guò)串口(serial)傳出來(lái)喊崖。
當(dāng)寫完了第一行 Hello World
的 print
函數(shù),總不能一直復(fù)制雇逞、粘貼代碼吧荤懂。
print('Hello World')
print('Hello World')
print('Hello World')
print('Hello World')
print('Hello World')
也不是只運(yùn)行驗(yàn)證功能就好了吧,所以加上了循環(huán)(while
)執(zhí)行代碼塘砸。
while True:
print('Hello World')
如果想要穩(wěn)定一些节仿,最好還要為它加入異常機(jī)制,保證它不會(huì)因?yàn)?Python 代碼的運(yùn)行出錯(cuò)而停下來(lái)掉蔬。
while True:
try:
print('Hello World')
except Exception as e:
pass
循環(huán)代碼中為什么需要異常機(jī)制
是不是以為 print 這樣的代碼就不會(huì)出錯(cuò)廊宪?其實(shí)不然,其實(shí)程序越接近底層硬件越容易出錯(cuò)女轿。
從功能上說(shuō)上文兩者之間并沒(méi)有什么區(qū)別箭启,都是輸出,但你會(huì)發(fā)現(xiàn)串口輸出可能會(huì)出現(xiàn)下面幾類情況蛉迹。
- 串口芯片損壞或線路斷路傅寡、串口到芯片的通路損壞導(dǎo)致的串口沒(méi)有數(shù)據(jù)輸出。
- 串口線路數(shù)據(jù)不穩(wěn)定婿禽、串口協(xié)議(波特率赏僧、停止位)等配置錯(cuò)誤導(dǎo)致的數(shù)據(jù)亂碼。
這就意味著你會(huì)遇到很多來(lái)自硬件上的問(wèn)題扭倾,所以要注意到這些意外淀零。
那在軟件代碼上會(huì)發(fā)生什么有關(guān)于硬件上的意外呢?
通常有無(wú)響應(yīng)膛壹、無(wú)應(yīng)答驾中、未連接等不成功的錯(cuò)誤唉堪,它們是來(lái)自 IO 的錯(cuò)誤。
- 當(dāng)網(wǎng)絡(luò)連接失敗后需要超時(shí)重連肩民,傳輸數(shù)據(jù)通道閑置時(shí)需要定時(shí)檢查心跳數(shù)據(jù)包唠亚。
- 當(dāng)配置文件寫入后通常會(huì)讀出來(lái)確認(rèn)真的寫入了,也是為了防止出錯(cuò)持痰,可能是存儲(chǔ)介質(zhì)出錯(cuò)灶搜,也可能是邏輯出錯(cuò)。
- 當(dāng)用戶向輸入框填了錯(cuò)誤數(shù)據(jù)工窍,不用寫怎么判斷和處理割卖,不合法的數(shù)據(jù)拋出異常就行。
因?yàn)檫@些現(xiàn)象太多不確定的可能性患雏,才會(huì)需要對(duì)代碼進(jìn)行異常捕獲機(jī)制鹏溯,來(lái)決定是否放過(guò)這次意外,可能會(huì)在下一次的循環(huán)就恢復(fù)了淹仑,這樣就能夠基本保證了 Python 代碼循環(huán)的穩(wěn)定性了丙挽。
來(lái)自外部/硬件上異常機(jī)制
這樣就足夠了嗎?
事實(shí)上有些錯(cuò)誤不源于 Python 代碼匀借,可能來(lái)自于底層 C 代碼颜阐,或其他程序,上文說(shuō)的異常機(jī)制只能捕獲 Python 異常怀吻,不能捕獲來(lái)自其他語(yǔ)言的異常瞬浓。
所以實(shí)際情況比想象的要更嚴(yán)峻一些,當(dāng)你無(wú)法解決不穩(wěn)定的系統(tǒng)帶來(lái)其他異常的時(shí)候蓬坡,通常在服務(wù)器程序上設(shè)計(jì)會(huì)在外部附加一個(gè)守護(hù)程序(如調(diào)試程序)來(lái)定時(shí)檢查自己的程序,例如可以檢查下面的一些情況磅叛。
- 檢查當(dāng)前的系統(tǒng)是否能聯(lián)網(wǎng)
- 檢查數(shù)據(jù)庫(kù)的通路是否正常
- 檢查指定的程序是否在運(yùn)行
總得來(lái)說(shuō)屑咳,你要為你的程序做一個(gè)監(jiān)控程序,可以是守護(hù)程序弊琴,也可以是看門狗兆龙。
具體怎么實(shí)現(xiàn),可以了解一些守護(hù)進(jìn)程的實(shí)現(xiàn)敲董。
看門狗(watchdog)是什么紫皇?
如上述的守護(hù)程序是靠一個(gè)軟件去監(jiān)控另一個(gè)軟件的狀態(tài),而看門狗的工作行為描述如下:
假設(shè)有一條需要定時(shí)吃飯(更新)的狗腋寨、如果不定時(shí)喂它(feed)就會(huì)餓著肚子叫聪铺。那么問(wèn)題來(lái)了,什么時(shí)候狗會(huì)叫呢萄窜?因?yàn)槿耍ㄐ酒┧懒肆逄蓿瑳](méi)人喂它了撒桨。(這也許是一個(gè)冷笑話)
看門狗是要求芯片程序負(fù)責(zé)定時(shí)喂狗,如果沒(méi)有喂狗就狗就餓死了键兜,作為報(bào)復(fù)狗會(huì)把芯片重啟凤类。讓它可以繼續(xù)喂狗。
任何硬件產(chǎn)品都有可能出現(xiàn)意外和錯(cuò)誤普气,看門狗相當(dāng)于芯片上的最后一層保障機(jī)制谜疤,通常它可能會(huì)發(fā)生在函數(shù)棧的指針參數(shù)執(zhí)行出錯(cuò),導(dǎo)致后續(xù)的喂狗操作再也執(zhí)行不到了现诀,具體怎么實(shí)現(xiàn)夷磕,可以查閱不同芯片提供的程序接口或寄存器。
優(yōu)化赶盔!優(yōu)化F笮俊!優(yōu)化S谖础K涸堋黑忱!
當(dāng)你的程序已經(jīng)跑起來(lái)以后绰疤,你會(huì)發(fā)現(xiàn)程序并沒(méi)有達(dá)到令人滿意的效果矾策,在性能树灶、內(nèi)存上都沒(méi)有經(jīng)過(guò)任何考慮鲜棠,只是實(shí)現(xiàn)了最起碼的功能而已猫态,那么完成了功能以后望薄,可以如何繼續(xù)呢槽驶?
當(dāng)然握侧,在優(yōu)化程序之前得先建立計(jì)算代碼執(zhí)行時(shí)間的觀念蚯瞧,建立起最簡(jiǎn)單的性能指標(biāo),如在代碼加上時(shí)間計(jì)算品擎。
def func():
i = 20**20000
import time
last = time.time()
func()
tmp = time.time() - last
print(tmp)
在 CPU I5-7300HQ 的計(jì)算機(jī)上見(jiàn)到每一次的循環(huán)的時(shí)間間隔約為 0.000997781753540039 不足 1ms 即可完成埋合。
PS C:\Users\dls\Documents\GitHub\MaixPy3> & C:/Users/dls/anaconda3/python.exe c:/Users/dls/Documents/GitHub/MaixPy3/test.py
0.000997781753540039
注意不要寫到 print(time.time() - last)
,因?yàn)橹囟ㄏ蚝蟮?print 是相當(dāng)耗時(shí)的萄传,尤其是當(dāng)內(nèi)容輸出到串口終端或網(wǎng)頁(yè)前端的時(shí)候甚颂,如下使用 M2dock 設(shè)備來(lái)演示一下串口輸出。
重定向指改變內(nèi)容要輸出的地方
root@sipeed:/# python3
Python 3.8.5 (default, Jan 17 2021, 06:07:56)
[GCC 6.4.1] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> def func():
... i = 20**20000
...
>>> import time
>>> last = time.time()
>>> func()
>>> tmp = time.time() - last
>>> print(tmp)
0.09001994132995605
>>>
>>>
>>> def func():
... i = 20**20000
...
>>> import time
>>> last = time.time()
>>> func()
>>> print(time.time() - last)
1.480057954788208
>>>
可以看到相差可能有 1 秒秀菱,而事實(shí)上只需要 90ms 就可以完成 func 函數(shù)的運(yùn)算振诬,這就產(chǎn)生了誤差導(dǎo)致不準(zhǔn)確,若是使用 jupyter 輸出就會(huì)看到 0.026456356048583984 需要 26ms 可以較為準(zhǔn)確的推算出它的真實(shí)運(yùn)算結(jié)果衍菱。
為什么會(huì)造成這種差異的原因是因?yàn)榇谝来屋斎朊钶敵鼋Y(jié)果需要時(shí)間赶么,所以依次輸入語(yǔ)句執(zhí)行自然會(huì)存在誤差,而 jupyter 是通過(guò)網(wǎng)絡(luò) socket 連接傳輸顯示到屏幕上梦碗,所以耗時(shí)誤差只會(huì)發(fā)生在運(yùn)算重定向輸出結(jié)果的時(shí)候禽绪,最終結(jié)果會(huì)較為貼近真實(shí)運(yùn)算結(jié)果蓖救,通過(guò)保存下述代碼文件來(lái)運(yùn)行即可得知真實(shí)情況下約為 26 ~ 28ms 完成。
root@sipeed:/# cat test.py
def func():
i = 20**20000
import time
last = time.time()
func()
tmp = time.time() - last
print(tmp)
root@sipeed:/# python test.py
0.028677940368652344
root@sipeed:/#
所以從現(xiàn)在建立起最基礎(chǔ)的計(jì)算耗時(shí)印屁,并且認(rèn)知到在計(jì)算機(jī)的世界里循捺,毫秒其實(shí)已經(jīng)很慢了,然后可以類比一種感受雄人,人眼感到流暢的畫面至少是 24 fps 从橘,而平時(shí)的視頻在 15 fps 的流動(dòng)是不會(huì)讓你感受到卡頓的,如果低于這個(gè)閾值础钠,則會(huì)出現(xiàn)卡頓造成心理上的不愉快恰力,這個(gè) 15 fps 意味著每秒 15 張存在變化的畫面,如果用程序來(lái)類比就是 1000 ms / 15 = 66 ms 旗吁,也就是每個(gè)流程操作最好是在 66ms 內(nèi)完成踩萎,這樣用戶才不會(huì)覺(jué)得卡頓,同理很钓,當(dāng) 1000ms / 24 = 41ms 就可以確保用戶體驗(yàn)這個(gè)軟件的時(shí)候會(huì)覺(jué)得流暢香府。
有了基本的性能指標(biāo),就有了優(yōu)化的對(duì)比參考码倦,如果是一些測(cè)試框架會(huì)幫助你自動(dòng)完成每個(gè)函數(shù)的耗時(shí)統(tǒng)計(jì)企孩,但在沒(méi)有現(xiàn)成框架工具的時(shí)候就要稍微辛苦一下自己了。
講一些經(jīng)典案例
在日常中存在最多操作就是循環(huán)和判斷袁稽,顯然好的優(yōu)化就是減少不必要的指令操作勿璃,可以通過(guò)改變代碼的執(zhí)行結(jié)構(gòu)來(lái)進(jìn)行優(yōu)化,下面就來(lái)具體分析吧推汽。
如某個(gè)向網(wǎng)絡(luò)上發(fā)送數(shù)據(jù)的操作补疑,最初可能會(huì)按人類直覺(jué)寫出以下的代碼,這是一種不用思考也可以很容易寫出來(lái)的同步阻塞式的結(jié)構(gòu)歹撒,每一條語(yǔ)句都是滿足了某些條件再繼續(xù)執(zhí)行癣丧。
def xxxx_func():
import random
return random.randint(0, 1)
while True:
is_idle = True
if is_idle is True:
print('try start')
is_ready = xxxx_func()
if is_ready is True:
print('try ready')
is_connected = xxxx_func()
if is_connected is True:
print('try connect')
is_send = xxxx_func()
if is_send is True:
print('try send')
is_reply = xxxx_func()
if is_reply is True:
print('wait reply')
is_exit = xxxx_func()
if is_exit is True:
print('operate successfully')
而優(yōu)化只需要加狀態(tài)變量改寫成狀態(tài)機(jī)結(jié)構(gòu)(fsm)就可以了,所有代碼都可以平行化執(zhí)行栈妆,并根據(jù)執(zhí)行頻率的重要程度(權(quán)重)調(diào)整各項(xiàng)判斷的順序,尤其是移除一些不必要的判斷厢钧。
def xxxx_func():
return 1
# state value
is_idle, is_ready, is_connected, is_send, is_reply, is_exit = 0, 1, 2, 3, 4, 5
state = is_idle
while state != is_exit:
if state is is_reply:
print('wait reply')
state = is_exit if xxxx_func() else is_send
continue
if state is is_send:
print('try send')
state = is_reply if xxxx_func() else is_connected
continue
if state is is_connected:
print('try connect')
state = is_send if xxxx_func() else is_ready
continue
if state is is_ready:
print('try ready')
state = is_connected if xxxx_func() else is_idle
continue
if state is is_idle:
print('try start')
state = is_ready
continue
這樣改造執(zhí)行結(jié)構(gòu)后鳞尔,每個(gè)代碼之間的上下文關(guān)系并不強(qiáng)烈,是否執(zhí)行某個(gè)語(yǔ)句取決于系統(tǒng)對(duì)于某個(gè)狀態(tài)是否滿足早直,如果狀態(tài)失敗也不會(huì)倒退回最初的判斷寥假,也就不需要每次都對(duì)各個(gè)狀態(tài)做檢查,檢查只會(huì)發(fā)生在出錯(cuò)的時(shí)候狀態(tài)跌落(state - 1)霞扬。
缺點(diǎn)就是需要消耗一些記錄狀態(tài)的變量(●'?'●)糕韧,不過(guò)代碼的拓展性和維護(hù)性就上來(lái)了枫振。
可以根據(jù)實(shí)際情況增加狀態(tài)的判斷或是減少狀態(tài)的轉(zhuǎn)移(調(diào)整狀態(tài)轉(zhuǎn)移范圍),如直接設(shè)置 state = is_ready萤彩,假設(shè)某些操作是已知的就可以跳過(guò)粪滤,可以添加 continue 跳過(guò)一些不可能發(fā)生的狀態(tài)。
還有嗎雀扶?
進(jìn)一步優(yōu)化還可以干掉 if 直接將狀態(tài)與函數(shù)聯(lián)合索引執(zhí)行杖小,簡(jiǎn)化代碼如下。
is_a, is_b, is_c = 0, 1, 2
state = is_a
def try_b():
global state
state = is_c
def try_a():
global state
state = is_b
func = [try_a, try_b]
while state != is_c:
func[state]()
# print(state)
基于上述結(jié)構(gòu)給出一個(gè)示例代碼參考愚墓。
class xxxx_fsm:
is_start, is_ready, is_connected, is_send, is_reply, is_exit = 0, 1, 2, 3, 4, 5
def xxxx_func(self):
return 1
def __init__(self):
self.func = [self.try_start, self.try_ready, self.try_connect, self.try_send, self.wait_reply]
self.state = __class__.is_start # state value
def wait_reply(self):
self.state = __class__.is_exit if self.xxxx_func() else __class__.is_send
def try_send(self):
self.state = __class__.is_reply if self.xxxx_func() else __class__.is_connected
def try_connect(self):
self.state = __class__.is_send if self.xxxx_func() else __class__.is_ready
def try_ready(self):
self.state = __class__.is_connected if self.xxxx_func() else __class__.is_start
def try_start(self):
self.state = __class__.is_ready
def event(self):
self.func[self.state]()
def check(self):
return self.state != __class__.is_exit
tmp = xxxx_fsm()
while tmp.check():
tmp.event()
# print(tmp.state)
其實(shí)上述的有限狀態(tài)機(jī)并非萬(wàn)能的代碼結(jié)構(gòu)予权,只是剛好很適合拆分已知的復(fù)雜業(yè)務(wù)邏輯的同步阻塞代碼,那么還有什么結(jié)構(gòu)可以選擇嗎浪册?有的扫腺,此前說(shuō)的都是同步阻塞的代碼,所以還有所謂的異步執(zhí)行的代碼村象。
說(shuō)說(shuō)異步的執(zhí)行方式
在這之前的代碼都是按每個(gè)循環(huán)的步驟有序執(zhí)行完成功能(同步執(zhí)行)笆环,但現(xiàn)實(shí)生活中的操作一定是按順序發(fā)生的嗎?其實(shí)不然煞肾,其實(shí)很多操作可能會(huì)在任意時(shí)刻發(fā)生咧织。
想象一個(gè)程序,它會(huì)響應(yīng)來(lái)自網(wǎng)絡(luò)的數(shù)據(jù)籍救,也會(huì)響應(yīng)來(lái)自人類的按鍵輸入操作习绢,這兩個(gè)操作如果按上述的結(jié)構(gòu)來(lái)寫,可能會(huì)是下面這樣蝙昙。
import time, random
def check_http():
time.sleep(random.randint(0, 3))
return random.randint(0, 1)
def http_recv():
while True:
if check_http():
print('http_recv')
break
def check_key():
time.sleep(random.randint(0, 2))
return random.randint(0, 1)
def key_press():
while True:
if check_key():
print('key_press')
break
while True:
http_recv()
key_press()
可以看到 http_recv 和 key_press 兩個(gè)事件的檢查會(huì)各自占據(jù)一段不知何時(shí)會(huì)觸發(fā)或結(jié)束的檢測(cè)的時(shí)間闪萄,程序只能循環(huán)等待這些事件會(huì)不會(huì)發(fā)生(或稱輪詢)。
這是個(gè)看起來(lái)可以工作但浪費(fèi)了很多時(shí)間的程序奇颠,現(xiàn)實(shí)里接收到許多用戶的網(wǎng)絡(luò)連接败去,而服務(wù)程序不可能只服務(wù)某個(gè)用戶的連接。
所以改寫異步的第一步就是簡(jiǎn)化代碼中不必要的循環(huán)烈拒,將每個(gè)需要循環(huán)等待的部分拆分成非阻塞的函數(shù)圆裕。
非阻塞意味著某個(gè)操作會(huì)在有限的時(shí)間內(nèi)結(jié)束,期望某個(gè)函數(shù)能夠在較短的時(shí)間(10ms)內(nèi)退出荆几,退出不代表功能結(jié)束吓妆,只是需要把這個(gè)時(shí)間讓出去給其他函數(shù)調(diào)用。
import time, random
http_state, key_state = 0, 0
def http_recv():
global http_state
if http_state:
print('http_recv')
def key_press():
global key_state
if key_state:
print('key_press')
def check_state():
global key_state, http_state
time.sleep(random.randint(0, 1))
key_state, http_state = random.randint(0, 2), random.randint(0, 2)
while True:
check_state()
http_recv()
key_press()
從邏輯上移除了等待吨铸,再通過(guò)統(tǒng)一的(check_state)檢查每個(gè)操作的狀態(tài)再?zèng)Q定是否喚醒該操作行拢,變成只有滿足某個(gè)狀態(tài)才執(zhí)行該操作,將此前的多個(gè)循環(huán)拆分出來(lái)诞吱。
但你會(huì)發(fā)現(xiàn)這樣寫還是有問(wèn)題舟奠,這樣豈不是意味著所有代碼都要按這個(gè)接口來(lái)寫了嗎竭缝?那么多的代碼,不可能全都可以拆分吧沼瘫。
所以是時(shí)候加入異步 IO (asyncio)的 async 和 await 語(yǔ)法了抬纸!先來(lái)點(diǎn)簡(jiǎn)單的。
import asyncio
async def test_task(name, tm):
await asyncio.sleep(tm)
print('%s over...' % name)
async def main(name):
import time
last = time.time()
await asyncio.gather(
test_task(name + 'A', 0.1),
test_task(name + 'B', 0.2),
test_task(name + 'C', 0.3),
)
print(name, time.time() - last)
loop = asyncio.get_event_loop()
tasks = [ main('l: '), main('r: ') ]
loop.run_until_complete(asyncio.wait(tasks))
運(yùn)行結(jié)果如下:
PS python.exe test.py
r: A over...
l: A over...
r: B over...
l: B over...
r: C over...
l: C over...
r: 0.3076450824737549
l: 0.3076450824737549
可以看到代碼總共耗時(shí)為 0.3s 完成晕鹊,但運(yùn)行了兩次不同所屬的 main 函數(shù)以及各自調(diào)用三次不同延時(shí)的 test_task 任務(wù)松却,而 await asyncio.sleep(tm) 延時(shí)期間實(shí)際上是被 asyncio 拿去運(yùn)行其他的 async 函數(shù)了,基于此結(jié)構(gòu)可以這樣改寫溅话。
import asyncio, random
async def key_press():
await asyncio.sleep(0.1)
key_state = random.randint(0, 1)
if key_state:
return 'have key_press'
async def http_recv():
await asyncio.sleep(0.2)
http_state = random.randint(0, 1)
if http_state:
return 'have http_recv'
async def run():
import time
while True:
task_list = [http_recv(), key_press()]
done, pending = await asyncio.wait(task_list, timeout=random.randint(0, 1) / 2)
print(time.time(), [done_task.result() for done_task in done])
await asyncio.sleep(0.2) # remove to run too fast.
loop = asyncio.get_event_loop()
loop.run_until_complete(run())
執(zhí)行效果如下晓锻。
1615141673.93252 [None, None]
1615141674.134 [None, 'have http_recv']
1615141674.3350334 [None, None]
1615141674.7361133 ['have key_press', 'have http_recv']
1615141674.9365196 [None, None]
1615141675.1399093 ['have http_recv', None]
可以看到在運(yùn)行 run 函數(shù)延時(shí) await asyncio.sleep(0.2)
后就會(huì)循環(huán)加載異步事件函數(shù)執(zhí)行,配置 asyncio.wait 函數(shù)的參數(shù) timeout
會(huì)導(dǎo)致 random.randint(0, 1) / 2
秒后就會(huì)自行超時(shí)退出飞几,退出的時(shí)候會(huì)收集當(dāng)前的 key_press
和 http_recv
函數(shù)的運(yùn)行結(jié)果砚哆,如果期間異步函數(shù)成功返回值(return 'have http_recv'
),最終結(jié)果就會(huì)輸出 1615138982.9762554 ['have http_recv']
表示有事件觸發(fā)并執(zhí)行了屑墨,否則為 None 躁锁,這將在下一次循環(huán)重新提交異步函數(shù)列表 [http_recv(), key_press()]
執(zhí)行。
注意 Python 3.7 以前的版本使用 loop = asyncio.get_event_loop() & loop.run_forever() & loop.run_until_complete() 卵史,而后采用 asyncio.run() 了战转。每個(gè)編程語(yǔ)言都有自己的異步框架和語(yǔ)法特色,請(qǐng)根據(jù)實(shí)際情況選用以躯。
考慮一下封裝模塊給其他人使用吧槐秧?
隨著代碼越寫越多,項(xiàng)目越來(lái)越大忧设,大到可能不是你一個(gè)人寫的時(shí)候刁标,你就要開(kāi)始注意工程項(xiàng)目的管理了,這與個(gè)人寫代碼時(shí)的優(yōu)化略微不同址晕,主要強(qiáng)調(diào)的是不同代碼之間的接口分離膀懈,盡量不干涉到他人的實(shí)現(xiàn)和提交,所以在寫代碼的時(shí)候谨垃,不妨為自己準(zhǔn)備一個(gè)獨(dú)立模塊启搂,以方便與其他人寫的分離或是導(dǎo)入其他(import)模塊。
若是在某個(gè)目錄(mod
)下存在一個(gè) __init__.py
的話刘陶,它就會(huì)變成 Python 模塊狐血,且名為 mod
,其中 __init__.py
的內(nèi)容可能如下:
def code():
print('this is code')
而且在該目錄下還存在一個(gè)額外的代碼文件(如 tmp.py
)內(nèi)容如下:
info = 'nihao'
對(duì)于開(kāi)發(fā)者或用戶來(lái)說(shuō)易核,在 import mod
的時(shí)候會(huì)調(diào)用 mod
目錄下的 __init__.py
,而 from mod import tmp
會(huì)調(diào)用 mod
目錄下的 tmp.py
代碼浪默。
>>> import mod
>>> mod
<module 'mod' from 'C:\\mod\\__init__.py'>
>>> mod.code()
this is code
>>> from mod import tmp
>>> tmp
<module 'mod.tmp' from 'C:\\mod\\tmp.py'>
>>> tmp.info
'nihao'
>>>
這樣你寫的代碼就可以作為一個(gè)模塊被其他人所使用了牡直,注意 import 只會(huì)加載并執(zhí)行一次缀匕,想要再次加載請(qǐng)使用 reload 函數(shù)。
如何進(jìn)行內(nèi)存上的分析碰逸?
這里就推薦 memory_profiler 開(kāi)源工具乡小,快去體驗(yàn)吧。
使用方法:python -m memory_profiler example.py
from memory_profiler import profile
@profile
def my_func():
a = [1] * (10 ** 6)
b = [2] * (2 * 10 ** 7)
del b
return a
運(yùn)行結(jié)果:
Line # Mem usage Increment Occurences Line Contents
============================================================
3 38.816 MiB 38.816 MiB 1 @profile
4 def my_func():
5 46.492 MiB 7.676 MiB 1 a = [1] * (10 ** 6)
6 199.117 MiB 152.625 MiB 1 b = [2] * (2 * 10 ** 7)
7 46.629 MiB -152.488 MiB 1 del b
8 46.629 MiB 0.000 MiB 1 return a
總結(jié)
其實(shí)所謂的優(yōu)化就是在程序上不斷追求無(wú)延遲饵史、零等待满钟、魯棒性、藝術(shù)品胳喷、最佳實(shí)踐等指標(biāo)湃番。
當(dāng)完成了自己的某個(gè)作品,多少都會(huì)希望自己的作品是最好的吭露,又或是越做越好的吠撮。熬夜辛苦寫下的程序,用盡自己的腦力和各種邏輯思維來(lái)不斷打磨它讲竿,盡可能的把它變成一件藝術(shù)品泥兰,然后為之自豪和興奮,恨不得向它人炫耀自己的成果题禀。
但愿你不會(huì)在往后的一堆垃圾代碼中失去了最初喜歡上編程的心情鞋诗。
附錄:多線程?多進(jìn)程迈嘹?該不該使用削彬?
事實(shí)上多線程和多進(jìn)程都是建立在操作系統(tǒng)之上的概念,由于操作系統(tǒng)中存在不同優(yōu)先級(jí)的中斷函數(shù)江锨,其中優(yōu)先級(jí)較高的函數(shù)棧會(huì)打斷優(yōu)先級(jí)低的函數(shù)棧執(zhí)行吃警,并且優(yōu)先級(jí)高的操作結(jié)束就會(huì)輪到優(yōu)先級(jí)低的操作,優(yōu)先級(jí)高的操作通常都會(huì)被設(shè)計(jì)成盡快結(jié)束退出(哪怕是失斪挠)酌心,不然用戶程序就會(huì)像老爺爺一樣緩慢運(yùn)行了。
多線程是由擁有內(nèi)存空間進(jìn)程(某個(gè)程序)創(chuàng)造出來(lái)的挑豌,多線程函數(shù)“看上去”是彼此并行的安券,并且共用所屬進(jìn)程的內(nèi)存數(shù)據(jù),而不同進(jìn)程之間申請(qǐng)的內(nèi)存空間并不互通氓英,所以當(dāng)你想要實(shí)現(xiàn)守護(hù)進(jìn)程的程序侯勉,是需要對(duì)其他進(jìn)程進(jìn)行通信的(如卸載程序時(shí)會(huì)檢查并發(fā)送信號(hào)停止要卸載的程序),并非是在代碼中修改一個(gè)變量那么簡(jiǎn)單铝阐。
事實(shí)上我并不鼓勵(lì)用戶在 Python 上使用多線程址貌,因?yàn)槿纸忉屍麈i(GIL)的存在,CPython 解釋器中執(zhí)行的每一個(gè) Python 線程,都會(huì)先鎖住自己练对,以阻止別的線程執(zhí)行遍蟋。而 CPython 解釋器會(huì)去輪詢檢查線程 GIL 的鎖住情況,每隔一段時(shí)間螟凭,Python 解釋器就會(huì)強(qiáng)制當(dāng)前線程去釋放 GIL虚青,這樣別的線程才能有執(zhí)行的機(jī)會(huì)÷菽校總得來(lái)說(shuō) CPython 的實(shí)現(xiàn)決定了使用多線程并不會(huì)帶來(lái)太大的性能提升棒厘,反而會(huì)帶來(lái)更多線程安全的問(wèn)題,尤其是需要線程資源同步了下隧。
警告:請(qǐng)不要在每個(gè)線程中都寫上不會(huì)退出的死循環(huán)奢人,多線程的并不是拿來(lái)偷懶的工具。