[同步到 MaixPy3 文件] 使用 Python 程式設計入門開源硬體專案

Juwan發表於2021-03-05

本文是給有一點 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 來使用。

像最初寫的程式碼,如果不通過物件繼承分解函式,最終將會形成一個巨大的 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)的話來說“你怎麼知道如何把這個產品設計成最好的呢?只有讓使用者儘快去用它。既然大家對這版產品有信心,在基本的產品功能上我們有競爭優勢,就應該抓住時機儘快將產品推向市場,真正完善它的人將是使用者。他們會告訴你喜歡哪裡不喜歡哪裡,知道了他們的想法,我們就迅速改,改了一百次之後,肯定就是一個非常好的產品了。”

準備一個好的開始

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

不妨自己動手試試看吧。

相關文章