[同步到 MaixPy3 文檔] 使用 Python 編程入門開源硬體項目

本文是給有一點 Python 基礎但還想進一步深入的同學,有經驗的開發者建議跳過。

前言

在寫這篇案例系列的時候 junhuanchen 期望能夠引導用戶如何成為專業的開發者,不是只會調用程式碼就好,所以在 MaixPy3 開源項目上期望為你帶來值得學習和容易上手的開源項目,所以開篇會引導用戶學習一些長期有利於編程工作上好的做法和觀念,就先從最簡單的認知項目開始吧。

第一次接觸需要編程的開源硬體項目,要做的第一件事就是先有一個好的開始,例如運行 Hello World 程式,意味著你必須能夠先將這個事物跑起來才能夠繼續後續的內容,它可能是硬體、軟體、工具等可編程的載體。

但這裡先不強調立刻開始運行程式,而是強調如何熟悉一個開源項目。

要先找到它提供的開發文檔(例如本文),先縱覽全文,站在專業的角度來看,你需要先關注它提供了哪些資源,可以在哪裡回饋你的問題,這樣就有利於你後續開發過程中出現問題後,該如何迅速得到解決,避免自己之後在學習和開發過程中耽誤時間。

有哪些資源是值得關注的?

  • 學會搜索!!!!!
  • 找到它的開源項目(如:github.com/sipeed),獲取它所提供的一系列源碼。
  • 找到它提供的用戶手冊、應用案例、數據手冊等等一系列開發所需要的文檔。
  • 找到它的開發、編譯、燒錄、量產等一系列配套工具鏈,為後續軟體開發活動中做準備。
  • 找到它的公開交流的環境,如 bbs、github、twitter、facebook、qq、wechat 等社交平台。

現在你可以放心的編程了,但你還需要遵守一些在開源軟體上的規則,認知到開源協議的存在,不要隨意地做出侵犯他人軟體的行為,哪怕沒有法律責任的問題。

在開源軟體的世界裡,鼓勵人們自由參與和貢獻程式碼,而不是鼓勵如何免費白嫖,自由不等於免費,免費不等於服務,將軟體源碼公開是為了讓用戶更好更具有針對性的提交和回饋項目中存在的問題,不是為了更好服務你,請不要以服務自己的產品為中心。

請尊重所有在開源環境里工作的朋友們,尊重他們(或是未來的你)的勞動成果。

最後在開源的世界裡,學會技術,學會成長,學會參與項目,學會分享成果!

Hello World

關於本機怎樣安裝運行 Python 的基礎知識,建議從其他網站教程得知。

說了這麼多,不如先來運行一段 Python3 程式碼吧。

print("hello world")

點擊下方的 run 按鈕即可運行,如果有條件就在本機運行測試。

在線 Python 編程 runoob-python google-colab 備用地址。

但這樣的程式碼是不夠的,稍微認真一點寫。

# encoding: utf-8

def unit_test():
    '''
    this is unit_test
    '''
    print("hello world")
    raise Exception('unit_test')

if __name__ == "__main__":
    try:
        unit_test()
    except Exception as e:
        import sys, traceback
        exc_type, exc_value, exc_obj = sys.exc_info()
        traceback.print_tb(exc_obj)
        print('have a error:', e)

運行結果:

PS C:\Users\dls\Documents\GitHub\MaixPy3> & C:/Users/dls/anaconda3/python.exe c:/Users/dls/Documents/GitHub/MaixPy3/test.py
hello world
  File "c:/Users/dls/Documents/GitHub/MaixPy3/test.py", line 12, in <module>
    unit_test()
  File "c:/Users/dls/Documents/GitHub/MaixPy3/test.py", line 8, in unit_test
    raise Exception('unit_test')
have a error: unit_test

程式碼瞬間就變得複雜了起來?其實不然,這麼寫必然有它的用意,那這麼寫都考慮到了哪些情況呢?

注意字元編碼和程式碼縮進格式

初學者經常會出現縮進不對齊的語法問題,程式碼的語法出現問題過於基礎就不詳談,檢查程式碼的小技巧就是 CTAL + A 全選程式碼,按 TAB 鍵右縮進,再配合 SHIFT + TAB 左縮進來發現哪段程式碼存在問題。

首行的 # encoding: utf-8 是為了避免在程式碼中存在中文或其他語言的字元編碼導致的運行出錯的問題。

在 python3 的字元串類型中 str 與 bytes 是一對歡喜冤家,例如 print(b’123′) 列印出來的是 b’123′ ,而實際上就是 ‘123’ 的 bytes 字元串,前綴 b 只是為了和 str 區分,因為用途不同,在不同的介面對數據類型的需求不對,例如傳遞 str 字元串時候是不允許輸入 ‘\xFF’ (0xFF) 字元的(會在轉換過程中丟失),但 bytes 可以存儲和表達。

給程式碼加入單元測試和異常捕獲

想要寫出一套穩定可用的程式碼,需要圍繞介面可重入可測試的設計來編寫封裝,任何人寫的程式碼都可能存在缺陷,在不能確定是哪裡產生的問題之前,要能夠恢復現場也要能夠定位具體位置,以求問題能夠最快得到回饋。

所以在程式碼功能還沒寫之前,先把測試和異常的模板寫好,再開始寫功能,邊寫邊測,確保最終交付的軟體程式碼就算出問題也可以隨時被測試(定位)出來。


def unit_test():
    '''
    this is unit_test
    '''
    print("hello world")

if __name__ == "__main__":
    unit_test()

這樣的程式碼可以保證任何人在任何時候運行該程式碼的時候都可以復現當時寫下的場合所做的內容,然後 if __name__ == "__main__": 意味著該程式碼被其他模組包含的時候,不會在 import 該 Python 模組(可取名成 hello )模組時調用,而是根據自己的程式碼需要執行相應的單元測試進行測試。

import hello
hello.unit_test() # print("hello world")

接著加入異常機制(try: except Exception as e:)保護程式碼段,表示該段程式碼出錯的時候,能夠不停下程式碼繼續運行,像硬體資源訪問的程式碼常常會發生超時、找不到、無響應的錯誤狀態,這種情況下,一個跑起來的系統程式通常不需要停下來,出錯了也可以繼續運行下一件事,然後把當時的錯誤記錄下來,通過 print 或 logging 日誌模組記錄下來,拿著錯誤結果(日誌)回饋給開發者,這樣開發者就可以分析、定位和解決問題,這其中也包括你自己。

try:
    raise Exception('unit_test')
except Exception as e:
    import sys, traceback
    exc_type, exc_value, exc_obj = sys.exc_info()
    traceback.print_tb(exc_obj)
    print('have a error:', e)

單元測試是每個程式都儘可能保持的基本原則,雖然人會偷懶,但最起碼的程式碼格式還是要有的。

註:traceback 可以抓取最後一次運行出現的錯誤而不停止運行,但該模組不存在 MicroPython(MaixPy) 中,它有類似的替代方法。

封裝程式碼介面成通用模組的方法

世上本沒有路,走的人多了,也便成了路。

這裡說的路實際上就是一種封裝和參考,它意味著你寫的程式碼成為一種事實上的通用操作。

在 Python 上有很多封裝參考,主要是為了形成抽象的函數模組。

所以出現了一些經典的編程思想,如面向過程、面向對象、面向裝飾、面向函數等編程方法,哪一種更好就不比較和討論了。

這裡就簡單敘述一下這些編程方法的逐漸發展與變化的過程,可以如何做出選擇。

面向過程

用面向過程的思維寫程式碼,強調的是這份程式碼做的這件事需要分幾步完成,例如最開始寫程式碼都是這樣的。

one = 1
two = 2
three = one + two
print(three)

這是用人類直覺的過程來寫程式碼,後來意識到可以這樣寫成通用功能,這是最初的程式碼封裝成某個函數。

def sum(num1, num2):
    return num1 + num2
one, two = 1, 2
print(sum(one, two)) # 1 + 2 = 3

於是你多寫了個類似的乘法操作。

def mul(num1, num2):
    return num1 * num2
one, two = 1, 2
print(mul(one, two)) # 1 * 2 = 2

這時的程式碼是按照每一個程式碼操作流程來描述功能的。

面向對象

面向對象是相對於面向過程來講的,把相關的數據和方法組織為一個整體來看待,從更高的層次來進行系統建模,更貼近事物的自然運行模式,一切事物皆對象,通過面向對象的方式,將現實世界的事物抽象成對象,現實世界中的關係抽象成類、繼承,幫助人們實現對現實世界的抽象與數字建模。

在看了一些面向對象的描述後,你會意識到上節面向過程的函數操作可能很通用,應該不只適用於一種變數類型,所以可以通過面向對象(class)的方法來封裝它,於是可以試著這樣寫。

class object:
    def sum(self, a, b):
        return a + b
    def mul(self, a, b):
        return a * b
obj = object()
print(obj.sum(1, 2)) # 1 + 2 = 3
print(obj.mul(1, 2)) # 1 * 2 = 2

這樣會意識到似乎還不只是數字能用,感覺字元串也能用。

class object:
    def sum(self, a, b):
        return a + b
    def mul(self, a, b):
        return a * b
obj = object()
print(obj.sum('1', '2')) # 1 + 2 = 3
print(obj.mul('1', '2')) # 1 * 2 = 2

但這麼寫會出問題的,字元串相加的時候可以,但相乘的時候會報錯誤,因為是字元串這個類型的變數是不能相乘的。

12
Traceback (most recent call last):
  File "c:/Users/dls/Documents/GitHub/MaixPy3/test.py", line 8, in <module>
    print(obj.mul('1', '2')) # 1 * 2 = 2
  File "c:/Users/dls/Documents/GitHub/MaixPy3/test.py", line 5, in mul
    return a * b
TypeError: can't multiply sequence by non-int of type 'str'

顯然這樣寫程式碼就不合理了,但這時運用的面向對象的思想是可行的,只是實現的方式不夠好而已,所以這時候應該寫成下面的類結構。

class obj:
    def __init__(self, value):
        self.value = value
    def __add__(self, obj):
        return self.value + obj
    def __mul__(self, obj):
        return self.value * obj

print(obj(1) + 2) # 3
print(obj(1) * 2) # 2

其中 __add____mul__ 是可重載運算符函數,意味著這個類實例化的對象在做 + 和 * 運算操作的時候,會調用類(class)重載函數,接著可以提升可以運算的對象類型,進一步繼承對象拓展功能(class number(obj):)和訪問超類的函數(super().__add__(obj)),其中 if type(obj) is __class__: 用於判斷傳入的參數對象是否可以進一步處理。


class number(obj):
    def __add__(self, obj):
        if type(obj) is __class__:
            return self.value + obj.value
        return super().__add__(obj)
    def __mul__(self, obj):
        if type(obj) is __class__:
            return self.value * obj.value
        return super().__mul__(obj)

print(number(1) + 2)
print(number(1) * 2)
print(number(1) + number(2))
print(number(1) * number(2))

這時候會發現可以進一步改寫成字元串數值運算。


class str_number(obj):
    def __init__(self, value):
        self.value = int(value)
    def __add__(self, obj):
        if type(obj) is __class__:
            return str(self.value + int(obj.value))
        return str(super().__add__(int(obj)))
    def __mul__(self, obj):
        if type(obj) is __class__:
            return str(self.value * int(obj.value))
        return str(super().__mul__(int(obj)))

print(str_number(1) + '2')
print(str_number(1) * '2')
print(str_number(1) + str_number(2))
print(str_number(1) * str_number(2))

現在就可以解決了最初的同類操作適用不同的數據類型,把最初的一段操作通用到數值和字元串了,可以受此啟發,它不僅僅只是加法或乘法,還有可能是其他操作,關於面向對象的內容就說到這裡,感興趣的可以查閱相關資料深入學習,本節只講述可以怎樣使用面向對象的思維寫程式碼,而不是單純把 Class 當 Struct 來使用。

面向切面

現在到了選擇更多編程思維方式了,關於面向切面編程方法的場景是這樣提出的,有一些函數,它在產品調試的時候會需要,但在產品上線的時候是不需要的,那這樣的函數應該如何實現比較好?接下來不妨直接看程式碼,以日誌輸出的程式碼為例來說說面向切面,介紹一下如何使用裝飾器進行編程的方法。


def log(param):
    # simple
    if callable(param):
        def wrapper(*args, **kw):
            print('%s function()' % (param.__name__,))
            param(*args, **kw)
        return wrapper
    # complex
    def decorator(func):
        import functools
        @functools.wraps(func)
        def wrapper(*args, **kw):
            print('%s %s():' % (param, func.__name__))
            return func(*args, **kw)
        return wrapper
    return decorator

def now():
    print("2019")

@log
def now1():
    print("2020")

@log("Is this year?")
def now2():
    print("2021")

now()
now1()
now2()

運行結果:

PS C:\Users\dls\Documents\GitHub\MaixPy3> & C:/Users/dls/anaconda3/python.exe c:/Users/dls/Documents/GitHub/MaixPy3/test.py
2019
now1 function()
2020
Is this year? now2():
2021
PS C:\Users\dls\Documents\GitHub\MaixPy3>

對於產品上線時不需要的函數,注釋掉就可以了,更進一步還可以重新設計某些函數滿足於某些條件後再運行。

  • 在執行某段操作前,先列印當前的系統狀態記錄下來,確保出錯時可以追溯到出錯的地方。
  • 在發送網路數據前,要先檢查網路通路是否存在,網卡是否還在工作。
  • 在運行操作前,先檢查記憶體夠不夠,是否需要釋放記憶體再繼續操作。

可以看到,當想要不改變某些現成庫程式碼的條件下拓展系統的功能,就不免需要面向切面的設計方法。

注意!面向切面提出的是編程思想,實現的方法不一定是裝飾函數,可以是回調函數,也可以是重載函數。

面向函數

關於面向函數的場景是由於有些問題是被數學公式提出的,所以對於一些數學問題,並不一定要按過程化的思維來寫,如實現階乘函數(factorial),它的功能就是返回一個數的階乘,即1*2*3*...*該數。

def fact(n):
    if n == 3:
        return 3*2*1
    if n == 2:
        return 2*1
    if n == 1:
        return 1
print(fact(3))
print(fact(2))
print(fact(1))

不難看出用最初的面向過程來寫是寫不下去的,不可能去定義所有的可能性,所以要找出規律,可以通過遞歸的方式實現。

def fact(n):
    return 1 if n == 1 else n * fact(n - 1)
print(fact(1))
print(fact(5))
print(fact(100))

這樣功能就完整了,簡單來說函數式編程是讓編程思維追求程式中存在的公式。

試試快速迭代的敏捷開發?

現代開源軟體在經歷了產測、內測、公測等環節後,直至更新到用戶的手裡,從前到後的過程通常在一周內就可以完成,所以在設計程式介面的時候,可以接受當下介面設計的不完美,等到未來有一個更好的替代功能介面的時候,就可以將其迭代替換下來,這意味著可以不用設計好整體的軟體系統再開始工作,而是邊做邊改進,這套理論適用於初期需要頻繁更新業務邏輯的開源軟體。

這裡簡單引用一段小故事來說明這個現象。

快速迭代,不是說一定要產品做好了,才能上線,半成品也能上線。

在沒有上線之前,你怎麼知道哪好那不好。所以半成品也是可以出門的,一定不要吝惜在家,醜媳婦才需要儘早見公婆。儘早的讓用戶去評判你的想法,你的設計是否可以贏得用戶的喜愛。快速發出,緊盯用戶回饋。百度完成了第一版的搜索引擎,也是讓用戶去做的選擇。用百度 CEO 李彥宏(Robin)的話來說「你怎麼知道如何把這個產品設計成最好的呢?只有讓用戶儘快去用它。既然大家對這版產品有信心,在基本的產品功能上我們有競爭優勢,就應該抓住時機儘快將產品推向市場,真正完善它的人將是用戶。他們會告訴你喜歡哪裡不喜歡哪裡,知道了他們的想法,我們就迅速改,改了一百次之後,肯定就是一個非常好的產品了。」

準備一個好的開始

看到這裡的你,可能會困惑,可能會看不懂,會覺得很複雜,這是認知上的偏差,實際上本文所講述的都是編程思想上的基礎,如果想專業起來,不認真是不行的。

不妨自己動手試試看吧。