返回文章列表

Python 命令列引數處理與遠端核心管理

本文探討 Python 命令列引數處理的兩種方式:手動檢查 sys.argv 和使用 argparse 模組,並比較 Click 模組如何簡化命令列介面開發。此外,文章還介紹瞭如何利用 remote_ikernel 工具設定 Jupyter Notebook

Python 系統管理

Python 提供多種方式處理命令列引數,從 sys.argv 到 argparse,各有其優缺點。sys.argv 簡易直觀,適合處理少量引數,但缺乏彈性。argparse 功能豐富,能處理複雜引數,但設定較繁瑣。Click 模組則簡化了命令列介面開發,使用裝飾器定義命令和引數,更直觀易用,並支援樣式輸出,提升使用者經驗。對於需要在遠端機器上執行程式碼的場景,Jupyter Notebook 的遠端核心功能提供了便捷的解決方案。透過 remote_ikernel 工具,可以輕鬆設定遠端核心,讓本地 Jupyter 例項連線到遠端機器,實作跨機器程式碼執行和測試,大幅降低手動操作的負擔,提升開發效率。

命令列引數處理:從手動檢查到使用argparse

在開發命令列工具時,處理使用者輸入的引數是一項基本且重要的任務。Python提供了多種方法來實作這一功能,從簡單的手動檢查sys.argv到使用標準函式庫中的argparse模組。

手動檢查sys.argv

在沒有任何外部依賴的情況下,Python指令碼可以透過檢查sys.argv列表來取得命令列引數。這個列表的第一個元素是指令碼的名稱,其餘元素是傳遞給指令碼的引數。

簡單範例:sensors_argv.py

#!/usr/bin/env python
# coding: utf-8
import socket
import sys
import psutil

HELP_TEXT = """用法:python {program_name:s}
顯示感測器的值
選項和引數:
--help:顯示此訊息"""

def python_version():
    return sys.version_info

def ip_addresses():
    hostname = socket.gethostname()
    addresses = socket.getaddrinfo(socket.gethostname(), None)
    address_info = []
    for address in addresses:
        address_info.append((address[0].name, address[4][0]))
    return address_info

def cpu_load():
    return psutil.cpu_percent(interval=0.1)

def ram_available():
    return psutil.virtual_memory().available

def ac_connected():
    return psutil.sensors_battery().power_plugged

def show_sensors():
    print("Python版本:{0.major}.{0.minor}".format(python_version()))
    for address in ip_addresses():
        print("IP位址:{0[1]} ({0[0]})".format(address))
    print("CPU負載:{:.1f}".format(cpu_load()))
    print("可用RAM:{} MiB".format(ram_available() / 1024**2))
    print("交流電源連線:{}".format(ac_connected()))

def command_line(argv):
    program_name, *arguments = argv
    if not arguments:
        show_sensors()
    elif arguments and arguments[0] == '--help':
        print(HELP_TEXT.format(program_name=program_name))
        return
    else:
        raise ValueError("未知引數 {}".format(arguments))

if __name__ == '__main__':
    command_line(sys.argv)

內容解密:

  1. sys.argv列表包含了命令列引數,其中第一個元素是指令碼名稱。
  2. command_line函式解析sys.argv,根據引數決定是否顯示感測器資訊或幫助訊息。
  3. 使用者未提供引數時,預設顯示感測器資訊;提供--help時,顯示幫助訊息。

使用argparse處理命令列引數

雖然手動檢查sys.argv可以滿足簡單需求,但對於複雜的命令列工具,argparse模組提供了更強大的功能。

argparse範例:sensors_argparse.py

#!/usr/bin/env python
# coding: utf-8
import argparse
import socket
import sys
import psutil

def python_version():
    return sys.version_info

def ip_addresses():
    hostname = socket.gethostname()
    addresses = socket.getaddrinfo(socket.gethostname(), None)
    address_info = []
    for address in addresses:
        address_info.append((address[0].name, address[4][0]))
    return address_info

def cpu_load():
    return psutil.cpu_percent(interval=0.1)

def ram_available():
    return psutil.virtual_memory().available

def ac_connected():
    return psutil.sensors_battery().power_plugged

def show_sensors():
    print("Python版本:{0.major}.{0.minor}".format(python_version()))
    for address in ip_addresses():
        print("IP位址:{0[1]} ({0[0]})".format(address))
    print("CPU負載:{:.1f}".format(cpu_load()))
    print("可用RAM:{} MiB".format(ram_available() / 1024**2))
    print("交流電源連線:{}".format(ac_connected()))

def main():
    parser = argparse.ArgumentParser(description='顯示感測器資訊')
    # 可在此新增引數定義,例如:parser.add_argument('--detail', action='store_true', help='顯示詳細資訊')
    args = parser.parse_args()
    show_sensors()

if __name__ == '__main__':
    main()

內容解密:

  1. argparse.ArgumentParser用於建立解析器,定義了命令列工具的基本描述。
  2. 可透過add_argument方法新增自定義引數,目前範例尚未新增任何自定義引數。
  3. parse_args方法解析命令列引數,並根據解析結果執行相應操作。
  4. 使用argparse可以自動生成幫助訊息,並處理使用者輸入的各種引數。

使用 Click 模組最佳化命令列介面

在開發命令列工具時,易用性和直覺性是至關重要的。Python 的 argparse 模組雖然功能強大,但使用起來略顯複雜。Click 是一個第三方模組,它簡化了命令列介面的建立過程,並鼓勵開發者遵循常見的使用模式,從而提高工具的易用性。

Click 的基本用法

Click 使用裝飾器(decorators)來定義命令列介面的結構和引數,這種方式比 argparse 的組態方式更為簡潔和直觀。首先,我們需要安裝 Click 模組:

pipenv install click

安裝完成後,我們可以將原有的 argparse 程式碼轉換為使用 Click。主要的變更包括:

  1. 使用 @click.command() 裝飾器定義命令。
  2. print 陳述式替換為 click.echo,以利用 Click 提供的輸出處理功能。

程式碼範例:使用 Click 的命令列工具

#!/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 = []
    for address in addresses:
        address_info.append((address[0].name, address[4][0]))
    return address_info

def cpu_load():
    return psutil.cpu_percent(interval=0.1)

def ram_available():
    return psutil.virtual_memory().available

def ac_connected():
    return psutil.sensors_battery().power_plugged

@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 負載: {:.1f}".format(cpu_load()))
    click.echo("可用 RAM: {} MiB".format(ram_available() / 1024**2))
    click.echo("交流電連線: {}".format(ac_connected()))

if __name__ == '__main__':
    show_sensors()

內容解密:

  1. @click.command():定義了一個 Click 命令,引數 help 指定了命令的幫助訊息。
  2. click.echo:用於輸出訊息,比 print 更具彈性,能夠處理不同編碼和終端機的格式化輸出。
  3. 函式定義:程式碼保留了原有的功能函式,如 cpu_loadram_available 等,用於取得系統資訊。

進階用法:樣式輸出

Click 提供了豐富的功能來增強命令列介面的使用者經驗,例如使用 click.secho 來輸出帶有樣式的文字。

程式碼範例:帶有樣式的輸出

@click.command(help="顯示感測器的值")
def show_sensors():
    click.secho("Python 版本: ", bold=True, nl=False)
    click.echo("{0.major}.{0.minor}".format(python_version()))
    for address in ip_addresses():
        click.secho("IP 位址: ", bold=True, nl=False)
        click.echo("{0[1]} ({0[0]})".format(address))
    click.secho("CPU 負載: ", bold=True, nl=False)
    click.echo("{:.1f}".format(cpu_load()))
    click.secho("可用 RAM: ", bold=True, nl=False)
    click.echo("{} MiB".format(ram_available() / 1024**2))
    click.secho("交流電連線: ", bold=True, nl=False)
    click.echo("{}".format(ac_connected()))

內容解密:

  1. click.secho:用於輸出帶有樣式的文字,例如粗體、顏色等。
  2. bold=True:設定輸出的文字為粗體。
  3. nl=False:避免輸出換行符號,讓後續的輸出能夠接續在同一行。

遠端核心管理:提升 Jupyter Notebook 的靈活性

在開發和測試過程中,我們經常需要在不同的機器上執行程式碼,尤其是在物聯網(IoT)裝置或遠端伺服器上執行 Jupyter Notebook。然而,直接在這些裝置上執行 Jupyter 環境並不總是方便或安全。幸運的是,Jupyter 的可插拔核心架構允許我們將本地執行的 Jupyter Notebook 連線到遠端電腦,從而實作跨多台機器的透明測試,同時將手動工作量降至最低。

瞭解 Jupyter 的核心規格

當 Jupyter 顯示其可用的執行目標列表時,它實際上是在列出已知的核心規格。每當選擇一個核心規格時,就會建立該核心的一個例項並將其連結到 Notebook。這種機制使得我們可以手動連線到遠端機器並啟動一個獨立的核心供本地 Jupyter 例項使用。不過,這種方法通常效率較低。透過使用 remote_ikernel 工具,我們可以更方便地新增使用遠端主機的核心規格。

安裝和設定 remote_ikernel

首先,我們需要在本地機器上安裝 remote_ikernel 工具:

pip install --user remote_ikernel

接著,在遠端主機(如 Raspberry Pi)上設定環境和核心輔助程式:

rpi> python -m pip install --user pipenv
rpi> mkdir development-testing
rpi> cd development-testing
rpi> pipenv install ipykernel

注意:在低效能主機上的安裝問題

對於像 Raspberry Pi 這樣的低效能主機,安裝 ipykernel 可能會比較慢。可以考慮使用套件管理器的版本:

rpi> sudo apt install python3-ipykernel
rpi> pipenv --three --site-packages

或者,可以在 Pipfile 中新增 piwheels 源來加速安裝:

[[source]]
url = "https://www.piwheels.org/simple"
name = "piwheels"
verify_ssl = true

然後,使用 pipenv install 安裝 ipykernel

建立遠端核心規格

在遠端主機上安裝 ipykernel 後,建立一個新的核心規格:

rpi> pipenv run ipython kernel install --user --name=development-testing

檢視產生的核心規格檔案:

{
  "argv": [
    "/home/pi/.local/share/virtualenvs/development-testing-nbi70cWI/bin/python",
    "-m",
    "ipykernel_launcher",
    "-f",
    "{connection_file}"
  ],
  "display_name": "development-testing",
  "language": "python"
}

這個檔案描述瞭如何在 Raspberry Pi 上啟動核心。

在本地機器上建立遠端核心規格

使用 remote_ikernel 在本地機器上建立一個指向遠端環境的核心規格:

remote_ikernel manage --add --kernel_cmd="/home/pi/.local/share/virtualenvs/development-testing-nbi70cWI/bin/python -m ipykernel_launcher -f {connection_file}" --name="development-testing" --interface=ssh --host=pi@raspberrypi --workdir="/home/pi/development-testing" --language=python

內容解密:

此命令的引數包括:

  • --kernel_cmd:指定啟動核心的命令,來自遠端核心規格檔案中的 argv 部分。
  • --name:指定核心規格的名稱。
  • --interface=ssh:指定連線到遠端主機的介面為 SSH。
  • --host:指定遠端主機的使用者和主機名稱。
  • --workdir:指定遠端主機上的工作目錄。
  • --language=python:指定核心使用的程式語言。