全網最適合入門的物件導向程式設計教程:18 類和物件的 Python 實現-多重繼承與 PyQtGraph 串列埠資料繪製曲線圖

FreakStudio發表於2024-07-17

全網最適合入門的物件導向程式設計教程:18 類和物件的 Python 實現-多重繼承與 PyQtGraph 串列埠資料繪製曲線圖

摘要:

本文主要介紹了 Python 中建立自定義類時如何使用多重繼承、菱形繼承的概念和易錯點,同時講解了如何使用 PyQtGraph 庫對串列埠接收的資料進行繪圖。

原文連結:

FreakStudio的部落格

往期推薦:

學嵌入式的你,還不會物件導向??!

全網最適合入門的物件導向程式設計教程:00 物件導向設計方法導論

全網最適合入門的物件導向程式設計教程:01 物件導向程式設計的基本概念

全網最適合入門的物件導向程式設計教程:02 類和物件的 Python 實現-使用 Python 建立類

全網最適合入門的物件導向程式設計教程:03 類和物件的 Python 實現-為自定義類新增屬性

全網最適合入門的物件導向程式設計教程:04 類和物件的Python實現-為自定義類新增方法

全網最適合入門的物件導向程式設計教程:05 類和物件的Python實現-PyCharm程式碼標籤

全網最適合入門的物件導向程式設計教程:06 類和物件的Python實現-自定義類的資料封裝

全網最適合入門的物件導向程式設計教程:07 類和物件的Python實現-型別註解

全網最適合入門的物件導向程式設計教程:08 類和物件的Python實現-@property裝飾器

全網最適合入門的物件導向程式設計教程:09 類和物件的Python實現-類之間的關係

全網最適合入門的物件導向程式設計教程:10 類和物件的Python實現-類的繼承和里氏替換原則

全網最適合入門的物件導向程式設計教程:11 類和物件的Python實現-子類呼叫父類方法

全網最適合入門的物件導向程式設計教程:12 類和物件的Python實現-Python使用logging模組輸出程式執行日誌

全網最適合入門的物件導向程式設計教程:13 類和物件的Python實現-視覺化閱讀程式碼神器Sourcetrail的安裝使用

全網最適合入門的物件導向程式設計教程:全網最適合入門的物件導向程式設計教程:14 類和物件的Python實現-類的靜態方法和類方法

全網最適合入門的物件導向程式設計教程:15 類和物件的 Python 實現-__slots__魔法方法

全網最適合入門的物件導向程式設計教程:16 類和物件的Python實現-多型、方法重寫與開閉原則

全網最適合入門的物件導向程式設計教程:17 類和物件的Python實現-鴨子型別與“file-like object“

更多精彩內容可看:

給你的 Python 加加速:一文速通 Python 平行計算

一文搞懂 CM3 微控制器除錯原理

肝了半個月,嵌入式技術棧大彙總出爐

電子計算機類比賽的“武林秘籍”

一個MicroPython的開源專案集錦:awesome-micropython,包含各個方面的Micropython工具庫

文件和程式碼獲取:

可訪問如下連結進行對文件下載:

https://github.com/leezisheng/Doc

image

本文件主要介紹如何使用 Python 進行物件導向程式設計,需要讀者對 Python 語法和微控制器開發具有基本瞭解。相比其他講解 Python 物件導向程式設計的部落格或書籍而言,本文件更加詳細、側重於嵌入式上位機應用,以上位機和下位機的常見串列埠資料收發、資料處理、動態圖繪製等為應用例項,同時使用 Sourcetrail 程式碼軟體對程式碼進行視覺化閱讀便於讀者理解。

相關示例程式碼獲取連結如下:https://github.com/leezisheng/Python-OOP-Demo

正文

在 python 中一個類能繼承自不止一個父類,這叫做 python 的多重繼承,多重繼承的語法與單繼承類似:

class SubclassName(BaseClass1, BaseClass2, BaseClass3, ...):
    pass

當然,子類所繼承的所有父類同樣也能有自己的父類,這樣就可以得到一個繼承關係機構圖如下圖所示:

image

多重繼承最常見的用途就是用於建立包含兩組完全不同行為的物件。例如,設計一個物件用於連線掃描器並將掃描的檔案透過傳真傳送出去,這一物件可能繼承自兩個完全獨立的 scanner 和 faxer 物件。

對於 MasterClass 來說,我們希望它可以具有繪圖功能,能夠將串列埠接收到的感測器資料動態繪製曲線,這裡我們藉助 PyQtGraph 庫來完成,PyQtGraph 是純 Python 圖形 GUI 庫,它充分利用 PyQt 和 PtSide 的高質量的圖形表現水平和 NumPy 的快速科學計算與處理能力,在數學、科學和工程領域都有廣泛的應用。

PyQtGraph 相比於 matplotlib 更加適合於資料採集和分析應用。

我們使用如下兩條語句完成 PyQtGraph 庫和其依賴庫 PyQt5 的安裝:

**pip install pyqtgraph **

**pip install PyQt5 **

pip install numpy

這裡,我們首先定義一個繪圖類及其方法,示例程式碼如下:

class PlotClass:
    _# 繪圖類初始化_
    def __init__(self,wintitle:str="Basic plotting examples",plottitle:str="Updating plot",width:int=1000,height:int=600):
        _# Qt應用例項物件_
        self.app        = None
        _# 視窗物件_
        self.win        = None
        _# 設定視窗標題_
        self.title      = wintitle
        _# 設定視窗尺寸_
        self.width      = width
        self.height     = height
        _# 感測器資料_
        self.value      = 0
        _# 計數變數_
        self.__count    = 0
        _# 感測器資料快取列表_
        self.valuelist  = []
        _# 繪圖曲線_
        self.curve      = None
        _# 圖層物件_
        self.plotob     = None
        _# 圖層標題_
        self.plottitle  = plottitle
        _# Qt應用和視窗初始化_
        self.appinit()

    _# 應用程式初始化_
    def appinit(self):
        _# 建立一個Qt應用,並返回該應用的例項物件_
        self.app = pg.mkQApp("Plotting Example")
        _# 生成多皮膚圖形_
        _# show:(bool) 如果為 True,則在建立小部件後立即顯示小部件。_
        _# title:(str 或 None)如果指定,則為此小部件設定視窗標題。_
        self.win = pg.GraphicsLayoutWidget(show=True, title=self.title)
        _# 設定視窗尺寸_
        self.win.resize(self.width, self.height)
        _# 進行視窗全域性設定,setConfigOptions一次性配置多項引數_
        _# antialias啟用抗鋸齒,useNumba對影像進行加速_
        pg.setConfigOptions(antialias=True, useNumba=True)

        _# 新增圖層_
        self.plotob = self.win.addPlot(title=self.plottitle)
        _# 新增曲線_
        self.curve = self.plotob.plot(pen='y')

    _# 接收資料_
    def GetValue(self,value):
        self.value = value
        _# 加入資料快取列表_
        self.valuelist.append(value)

    _# 更新曲線資料_
    def DataUpdate(self):
        _# 模擬繪製正弦曲線_
        _# 計數變數更新_
        self.__count = self.__count + 0.1
        self.value = np.sin(self.__count)
        self.GetValue(self.value)
        _# 將資料轉化為圖形_
        self.curve.setData(self.valuelist)

    _# 設定定時更新_
    def SetUpdate(self,time:int = 100):
        _# 建立定時器物件_
        timer = QtCore.QTimer()
        _# 定時器結束,觸發DataUpdate方法_
        timer.timeout.connect(self.DataUpdate)
        _# 啟動定時器_
        timer.start(time)
        _# 進入主事件迴圈並等待_
        pg.exec()

if __name__ == '__main__':
    _# 建立PlotClass物件,自動完成初始化_
    p = PlotClass()
    _# 設定定時更新任務_
    p.SetUpdate()

這裡,我們定義瞭如下屬性和方法:

屬性/方法 作用
wintitle 視窗標題
plottitle 圖層標題
width 視窗寬度
height 視窗高度
app Qt 應用例項物件
win 視窗物件
value 感測器資料
__count 計數變數
valuelist 感測器資料快取列表
curve 繪圖曲線
plotob 圖層物件
appinit(self) 用於 qt 應用程式初始化,新增視窗、曲線和圖層
GetValue(self,value) 用於接收感測器資料,加入快取列表
DataUpdate(self) 用於定時進行曲線更新,這裡模擬繪製正弦曲線
SetUpdate(self,time:int = 100) 設定定時更新任務

首先,我們在__init__和 appinit()中完成初始化操作,包括對類內屬性、Qt 應用例項、視窗圖層及曲線的初始化:

image

接著我們在 SetUpdate 方法中建立定時器物件並設定定時任務,當設定的延時時間到達時,呼叫 DataUpdate 方法,在其中對資料曲線並進行更新,示例中,我們利用__count 屬性每次遞增,使得其繪製正弦曲線。

同時設定進入主事件迴圈並等待嗎,以使得 Qt 介面保持顯示:

image

這裡,我們在主函式中建立物件並啟動執行定時重新整理曲線,如下為結果:

image

這裡,我們想要使得 MasterClass 類同時具備串列埠收發和繪圖功能,因此要用到多重繼承,MasterClass 類同時繼承於 SerialClass 和 PlotClass。透過多重繼承,一個子類就可以同時獲得多個父類的所有功能。

示例程式碼如下:

class MasterClass(SerialClass,PlotClass):
    _# 類變數:_
    _#   BUSY_STATE  -忙碌狀態-0_
    _#   IDLE_STATE  -空閒狀態-1_
    BUSY_STATE, IDLE_STATE = (0, 1)

    _# 類變數:_
    _#   START_CMD       - 開啟命令      -0_
    _#   STOP_CMD        - 關閉命令      -1_
    _#   SENDID_CMD      - 傳送ID命令    -2_
    _#   SENDVALUE_CMD   - 傳送資料命令   -3_
    START_CMD, STOP_CMD, SENDID_CMD, SENDVALUE_CMD = (0, 1, 2, 3)

    _# 類的初始化_
    def __init__(self,state:int = IDLE_STATE,port:str = "COM17",wintitle:str="Basic plotting examples",plottitle:str="Updating plot",width:int=1000,height:int=600):
        _# 分別呼叫不同父類的__init__方法_
        SerialClass.__init__(self,port)
        PlotClass.__init__(self,wintitle,plottitle,width,height)
        self.valuequeue   = queue.Queue(10)
        self.__masterstatue = state
        _# 初始化完成的標誌量_
        self.INIT_FLAG = False

    @classmethod
    def MasterInfo(cls):
        print("Info : "+str(cls))

    _# 開啟主機_
    def StartMaster(self):
        super().OpenSerial()
        logging.info("START MASTER :"+self.dev.port)

    _# 停止主機_
    def StopMaster(self):
        super().CloseSerial()
        logging.info("CLOSE MASTER :" + self.dev.port)

    _# 接收感測器ID號_
    def RecvSensorID(self):
        sensorid = super().ReadSerial()
        logging.info("MASTER RECIEVE ID : " + str(sensorid))
        return sensorid

    _# 接收感測器資料_
    def RecvSensorValue(self):
        data = super().ReadSerial()
        logging.info("MASTER RECIEVE DATA : " + str(data))
        self.valuequeue.put(data)
        return data

    _# 主機傳送命令_
    def SendSensorCMD(self,cmd):
        super().WriteSerial(str(cmd))
        logging.info("MASTER SEND CMD : " + str(cmd))

    _# 主機返回工作狀態-_
    def RetMasterStatue(self):
        return self.__masterstatue

    _# 重寫父類的DataUpdate方法_
    def DataUpdate(self):
        self.SendSensorCMD(self.SENDVALUE_CMD)
        self.value = self.RecvSensorValue()
        self.WriteSerial("Recv:"+str(self.value))
        self.GetValue(self.value)
        self.curve.setData(self.valuelist)

if __name__ == "__main__":
    _# 初始化物件_
    m = MasterClass(state = MasterClass.IDLE_STATE,
                    port = "COM17",
                    wintitle = "Basic plotting examples",
                    plottitle = "Updating plot",
                    width = 1000,
                    height = 600)
    m.StartMaster()
    m.SendSensorCMD(MasterClass.SENDID_CMD)
    m.RecvSensorID()
    _# 設定定時更新任務_
    m.SetUpdate()

我們可以看到兩個父類和子類關係及不同類的屬性和方法如下:

image

首先,我們使用如下語句表明 MasterClass 繼承於 SerialClass 和 PlotClass:

class MasterClass(SerialClass,PlotClass):

接著,我們在 MasterClass 的初始化方法中分別呼叫不同父類的__init__方法:

SerialClass.__init__(self,port)
        PlotClass.__init__(self,wintitle,plottitle,width,height)

同時我們在 MasterClass 中重寫父類的 DataUpdate 方法,首先傳送查詢資料指令,接著等待接收資料,完成資料接收後傳送接收到的資料並存入資料快取列表,在設定定時任務後,定時更新曲線。

image

如下為執行效果,我們可以看到接收到資料後正常完成曲線的更新:

image

在測試過程中,我們可以看到 Qt 視窗會有無法響應的情況出現,這是由於介面主執行緒是單執行緒,如果在 UI 主執行緒中執行耗時操作,例如點選按鈕,響應函式去資料庫查詢資料,資料量比較大時,查詢需要幾秒鐘甚至幾十秒的時間,如果 UI 主執行緒一直等待響應函式返回,阻塞在響應函式內部,就無法響應介面的其他訊息或者事件,介面就會卡死,無響應。這種情況可以利用 Python 的多執行緒或多程序得以避免,這個情況將在後面講到。

可以看到,在建立包含兩組完全不同行為的物件的情況下,兩個類介面不同,子類完全可以正常執行,但是如果兩個類的介面有重疊,同時繼承就可能造成混亂。最好的方法就是避免這種情況,重新分析系統,看看是否能夠去掉多重繼承關係並用其他的關聯或組合設計替代。

同時切記,儘量不要在子類的初始化方法中手動呼叫父類物件的初始化方法,會導致導致菱形繼承無法被正確處理,儘量使用 Python 內建的 super() 函式,並且為 Python 類規定了標準的方法解析順序 MRO 。使用 super() 函式初始化父類可以確保菱形繼承體系中的共同超類只初始化一次。MRO 則可以確定超類之間的初始化順序。

關於多重繼承中呼叫同名方法時的具體情況和呼叫順序可以檢視如下連結:

https://pythonhowto.readthedocs.io/zh-cn/latest/object.html#id29

image

相關文章