【MaixPy3文檔】寫好 Python 代碼杂腰!

本文是給有一點(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')
image

而嵌入式設(shè)備上的 python 是通過(guò)串口(serial)傳出來(lái)喊崖。

image

當(dāng)寫完了第一行 Hello Worldprint 函數(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é)果衍菱。

image

為什么會(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_presshttp_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)偷懶的工具。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末汪拥,一起剝皮案震驚了整個(gè)濱河市达传,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌迫筑,老刑警劉巖宪赶,帶你破解...
    沈念sama閱讀 219,188評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異脯燃,居然都是意外死亡搂妻,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門辕棚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)欲主,“玉大人,你說(shuō)我怎么就攤上這事逝嚎”馄埃” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 165,562評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵补君,是天一觀的道長(zhǎng)引几。 經(jīng)常有香客問(wèn)我,道長(zhǎng)挽铁,這世上最難降的妖魔是什么伟桅? 我笑而不...
    開(kāi)封第一講書人閱讀 58,893評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮叽掘,結(jié)果婚禮上楣铁,老公的妹妹穿的比我還像新娘。我一直安慰自己更扁,他們只是感情好盖腕,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布赫冬。 她就那樣靜靜地躺著,像睡著了一般赊堪。 火紅的嫁衣襯著肌膚如雪面殖。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,708評(píng)論 1 305
  • 那天哭廉,我揣著相機(jī)與錄音,去河邊找鬼相叁。 笑死遵绰,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的增淹。 我是一名探鬼主播椿访,決...
    沈念sama閱讀 40,430評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼虑润!你這毒婦竟也來(lái)了成玫?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 39,342評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤拳喻,失蹤者是張志新(化名)和其女友劉穎哭当,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體冗澈,經(jīng)...
    沈念sama閱讀 45,801評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡钦勘,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了亚亲。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片彻采。...
    茶點(diǎn)故事閱讀 40,115評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖捌归,靈堂內(nèi)的尸體忽然破棺而出肛响,到底是詐尸還是另有隱情,我是刑警寧澤惜索,帶...
    沈念sama閱讀 35,804評(píng)論 5 346
  • 正文 年R本政府宣布特笋,位于F島的核電站,受9級(jí)特大地震影響门扇,放射性物質(zhì)發(fā)生泄漏雹有。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評(píng)論 3 331
  • 文/蒙蒙 一臼寄、第九天 我趴在偏房一處隱蔽的房頂上張望霸奕。 院中可真熱鬧,春花似錦吉拳、人聲如沸质帅。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,008評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)煤惩。三九已至嫉嘀,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間魄揉,已是汗流浹背剪侮。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,135評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留洛退,地道東北人瓣俯。 一個(gè)月前我還...
    沈念sama閱讀 48,365評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像兵怯,于是被迫代替她去往敵國(guó)和親彩匕。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評(píng)論 2 355