在嵌入式系統或特殊硬體環境下進行開發時,搭建遠端開發環境至關重要。本文介紹如何結合 Jupyter 和 SSH,在本地端操控遠端 Raspberry Pi 進行開發和測試,涵蓋遠端 Jupyter Kernel 的設定、函式庫的安裝與同步,以及程式碼的本地與遠端執行。此方法讓開發者能有效利用本地端開發環境的便利性,同時運用遠端硬體資源,例如連線感測器進行資料收集和原型開發。文章也探討如何將 Jupyter Notebook 的程式碼轉換成可在本地和遠端執行的 Python 指令碼,並比較了 Unittest 和 Pytest 兩種測試框架,說明 Pytest 如何簡化測試流程,提升開發效率。
遠端開發環境的建立與應用
在進行需要特定硬體或環境的開發工作時,例如使用 Raspberry Pi 進行感測器資料收集,遠端開發環境的建立變得至關重要。透過 Jupyter 與 SSH 的結合,可以實作在本地端使用遠端環境進行開發與測試。
建立遠端 Jupyter Kernel
要實作遠端開發,首先需要在目標機器(如 Raspberry Pi)上安裝並組態 Jupyter Kernel。以下是設定的步驟:
安裝 ipykernel:在遠端機器上,使用
pipenv安裝ipykernel。註冊 Kernel:使用遠端機器的 Python 環境註冊新的 kernel,並記錄其名稱。
複製 Kernel Spec:將註冊的 kernel spec 複製到本地端,用於設定遠端連線。
修改 Kernel 連線設定:在本地端修改 kernel spec 中的連線設定,使用
rik ssh install指令安裝並設定遠端 kernel。rik ssh install --kernel-name <kernel-name> --name <display-name> --interface <ssh-interface> --host <ssh-host> --workdir <remote-workdir> --language <language>--name引數定義了在 Jupyter 中顯示的名稱。--interface和--host定義瞭如何連線到遠端機器。--workdir設定預設的工作目錄。--language指定程式語言,用於區分不同語言的 kernel。
測試與驗證遠端連線
設定完成後,可以透過 jupyter kernelspec list 指令檢視已設定的 kernel,並使用 jupyter console --kernel=<kernel-name> 進行連線測試。
> jupyter kernelspec list
Available kernels:
advancedpython C:\Users\micro\AppData\Roaming\jupyter\kernels\advancedpython
rik_ssh_pi_raspberrypi_developmenttesting C:\Users\micro\AppData\Roaming\jupyter\kernels\rik_ssh_pi_raspberrypi_developmenttesting
> jupyter console --kernel=rik_ssh_pi_raspberrypi_developmenttesting
內容解密:
jupyter kernelspec list用於列出所有可用的 kernel。jupyter console --kernel=<kernel-name>開啟與指定 kernel 的 IPython shell 連線,用於測試。
在遠端環境中安裝特定函式庫
對於需要在遠端環境中使用的特定函式庫,如 Raspberry Pi 的 Adafruit DHT 函式庫,可以透過 pipenv 安裝,並使用條件式依賴語法限制其安裝環境。
> pipenv install "Adafruit-CircuitPython-DHT ; 'arm' in platform_machine"
內容解密:
pipenv install指令用於安裝依賴。"Adafruit-CircuitPython-DHT ; 'arm' in platform_machine"指定了只有在 ARM 平台上才安裝此依賴。
同步依賴至遠端環境
將更新後的 Pipfile 和 Pipfile.lock 複製到遠端機器,並執行 pipenv install --deploy 以確保遠端環境的一致性。
rpi> cd /home/pi/development-testing
rpi> pipenv install --deploy
內容解密:
--deploy引數確保只在完全比對的版本下進行佈署。- 若遇到平台相容性問題,可使用
pipenv lock --keep-outdated重新鎖定依賴版本。
實際應用與開發
完成上述設定後,便可以在 Jupyter 中選擇遠端 kernel 進行開發,利用 Raspberry Pi 的硬體資源,如感測器,進行資料收集與原型開發。
此流程有效結合了本地開發環境的便利性與遠端硬體資源的特殊能力,為需要特定環境的開發工作提供了彈性且強大的支援。
遠端開發與本地執行的整合實踐
在現代軟體開發中,如何將程式碼在不同環境下執行是一個常見的挑戰。本章節將探討如何利用 Jupyter Notebook 結合遠端 Raspberry Pi 進行開發,並將程式碼轉換為可在本地及遠端環境中執行的 Python 指令碼。
遠端開發的挑戰與解決方案
當我們在 Jupyter Notebook 中開發時,可能會遇到某些程式碼片段只能在特定環境下執行。例如,連線 DHT22 感測器的程式碼需要執行在具有特定硬體支援的 Raspberry Pi 上。
程式碼範例:處理環境相依性
def get_relative_humidity():
try:
# 連線到 GPIO pin 4 上的 DHT22 感測器
from adafruit_dht import DHT22
from board import D4
return DHT22(D4).humidity
except (ImportError, NotImplementedError):
# 當沒有 DHT library 或是在未知平台上執行時傳回 None
return None
內容解密:
- 例外處理機制:透過捕捉
ImportError和NotImplementedError,確保函式在不支援的環境下不會當機,而是傳回None。 - 硬體相依性處理:只有在支援的硬體平台上才會嘗試匯入相關 library 並讀取感測器資料。
- 跨平台相容性:使得該函式能夠在任意機器上被呼叫,無論是否有感測器支援。
完整的指令碼實作
將各個功能模組化後,我們可以建立一個完整的指令碼,如下所示:
#!/usr/bin/env python
# coding: utf-8
import socket
import sys
import click
import psutil
def python_version():
return sys.version_info
def ip_addresses():
hostname = socket.gethostname()
addresses = socket.getaddrinfo(socket.gethostname(), None)
address_info = [(address[0].name, address[4][0]) for address in addresses]
return address_info
def cpu_load():
return psutil.cpu_percent(interval=0.1) / 100.0
def ram_available():
return psutil.virtual_memory().available
def ac_connected():
return psutil.sensors_battery().power_plugged
def get_relative_humidity():
try:
from adafruit_dht import DHT22
from board import D4
return DHT22(D4).humidity
except (ImportError, NotImplementedError):
return None
@click.command(help="顯示感測器的數值")
def show_sensors():
click.echo("Python 版本: {0.major}.{0.minor}".format(python_version()))
for address in ip_addresses():
click.echo("IP 位址: {0[1]} ({0[0]})".format(address))
click.echo("CPU 負載: {:.1%}".format(cpu_load()))
click.echo("可用 RAM: {:.0f} MiB".format(ram_available() / 1024**2))
click.echo("AC 電源連線: {!r}".format(ac_connected()))
click.echo("濕度: {!r}".format(get_relative_humidity()))
if __name__ == '__main__':
show_sensors()
內容解密:
- 模組化設計:每個功能(如取得 Python 版本、IP 位址等)都被封裝成獨立函式,提高了程式碼的可讀性和可維護性。
- 命令列介面:利用
click函式庫建立使用者友善的命令列介面,使得指令碼易於使用。 - 跨平台支援:透過適當的錯誤處理,確保指令碼可以在不同環境下執行。
測試、檢查與程式碼檢查工具
Python 以其「鴨子型別」(duck typing)著稱,也就是說,開發者無需明確進行型別檢查就能撰寫程式碼。只要物件提供正確的方法且這些方法具有正確的含義,它們就能正常運作。
Python 透過晚繫結(late binding)和動態派發(dynamic dispatch)實作這一特性。動態派發意味著函式的解析發生在呼叫時,而不是在程式撰寫時。這種特性使得函式能夠在不知道物件具體型別的情況下信任其底層實作。
簡單範例與問題
考慮一個簡單的溫度感測器函式,如下所示:
def get_temperature():
# 連線到 GPIO pin 4 的 DHT22 感測器
try:
from adafruit_dht import DHT22
from board import D4
except (ImportError, NotImplementedError):
# 若無 DHT 函式庫則會引發 ImportError
# 在未知平台上執行時會引發 NotImplementedError
return None
return DHT22(D4).temperature
我們可能希望允許使用者以不同的格式檢視溫度資料。因此,我們需要撰寫轉換函式,如下所示:
def celsius_to_fahrenheit(celsius):
return (celsius * 9/5) + 32
def celsius_to_kelvin(celsius):
return celsius + 273.15
內容解密:
celsius_to_fahrenheit函式將攝氏溫度轉換為華氏溫度。公式(celsius * 9/5) + 32是華氏溫標的標準轉換公式。celsius_to_kelvin函式將攝氏溫度轉換為克氏溫度。公式celsius + 273.15是克氏溫標的標準轉換公式。- 這兩個函式都接受數值型別的引數,包括
int、float、decimal.Decimal和fractions.Fraction。
測試的重要性
未經測試的程式碼是有問題的程式碼。Python 提供內建的測試支援,透過標準函式庫中的 unittest 模組實作。該模組提供了一個 TestCase 類別,用於包裝個別測試以及設定和拆卸程式碼。此外,它還提供了輔助函式,用於斷言值之間的關係。
雖然可以使用 unittest 模組單獨撰寫測試,但強烈建議使用附加模組 pytest。
型別檢查的益處
型別檢查可以幫助識別函式運作的型別,從而提高程式碼的可讀性和可維護性。雖然 Python 的動態型別系統使得型別檢查不是強制性的,但使用型別提示(type hints)可以提供額外的資訊,有助於開發者理解程式碼。
範例分析
考慮以下範例:
def celsius_to_fahrenheit(celsius: float) -> float:
return (celsius * 9/5) + 32
內容解密:
- 該函式使用型別提示指明
celsius引數和傳回值都是float型別。 - 型別提示提供了額外的資訊,有助於開發者理解函式的預期輸入和輸出。
Pytest 與 Unittest 的比較:簡化測試流程
Pytest 極大地簡化了測試系統的設定過程。比較以下使用 Unittest 風格(清單 2-5)和 Pytest 風格(清單 2-6)撰寫的測試,它們用於測試之前原型設計的溫度轉換函式(清單 2-4)。
被測試的 temperature.py
def celsius_to_fahrenheit(celsius):
return celsius * 9 / 5 + 32
def celsius_to_kelvin(celsius):
return 273.15 + celsius
內容解密:
celsius_to_fahrenheit函式將攝氏溫度轉換為華氏溫度,計算公式為 ( \text{華氏溫度} = \text{攝氏溫度} \times \frac{9}{5} + 32 )。celsius_to_kelvin函式將攝氏溫度轉換為開爾文溫度,計算公式為 ( \text{開爾文溫度} = 273.15 + \text{攝氏溫度} )。
Unittest 風格的測試
import unittest
from temperature import celsius_to_fahrenheit
class TestTemperatureConversion(unittest.TestCase):
def test_celsius_to_fahrenheit(self):
self.assertEqual(celsius_to_fahrenheit(21), 69.8)
def test_celsius_to_fahrenheit_equivlance_point(self):
self.assertEqual(celsius_to_fahrenheit(-40), -40)
def test_celsius_to_fahrenheit_float(self):
self.assertEqual(celsius_to_fahrenheit(21.2), 70.16)
def test_celsius_to_fahrenheit_string(self):
with self.assertRaises(TypeError):
f = celsius_to_fahrenheit("21")
if __name__ == '__main__':
unittest.main()
內容解密:
- 使用
unittest.TestCase作為測試類別的基礎類別。 - 每個測試方法名稱以
test_開頭,代表一個獨立的測試案例。 - 使用
self.assertEqual和self.assertRaises等斷言方法來驗證預期結果。
Pytest 風格的測試
import pytest
from temperature import celsius_to_fahrenheit
def test_celsius_to_fahrenheit():
assert celsius_to_fahrenheit(21) == 69.8
def test_celsius_to_fahrenheit_equivlance_point():
assert celsius_to_fahrenheit(-40) == -40
def test_celsius_to_fahrenheit_float():
assert celsius_to_fahrenheit(21.2) == 70.16
def test_celsius_to_fahrenheit_string():
with pytest.raises(TypeError):
f = celsius_to_fahrenheit("21")
內容解密:
- Pytest 風格的測試直接使用 Python 的
assert陳述式進行斷言。 - 使用
pytest.raises來檢查是否拋出了預期的異常。 - 無需繼承特定的基礎類別,測試函式名稱以
test_開頭即可被識別為測試案例。
Pytest 與 Unittest 的主要差異
| 比較專案 | Unittest | Pytest |
|---|---|---|
| 值相等 | self.assertEqual(x, y) | assert x == y |
| 值不相等 | self.assertNotEqual(x, y) | assert x != y |
| 值為 None | self.assertIsNone(x) | assert x is None |
| 清單包含 | self.assertIn(x, y) | assert x in y |
| 浮點數比較(誤差小於 0.000001) | self.assertAlmostEqual(x, y) | assert x == pytest.approx(y) |
| 異常檢查 | with self.assertRaises(TypeError): | with pytest.raises(TypeError): |
內容解密:
- Pytest 的斷言風格更自然,直接使用 Python 的
assert陳述式。 - Unittest 需要使用特定的斷言方法,如
assertEqual、assertRaises等。 - Pytest 提供
pytest.approx用於浮點數的近似比較。