Pythonの仮想環境の構築法

1.目的

Raspberry Pi 5の発売と共に、Raspberry Pi OSのバージョンが、“bullseye”から“bookworm”に上がりました。それに伴い、Pythonとpipのバージョンも上がりました。

pipのバージョン23以降は、PEP668の関係でpipを使ったnon-Devian-Packagedの外部ライブラリのインストールが出来なくなっています。
paho.mqttをpipでインストールすると以下の様なエラーが出ます。仮想環境venvを使用して、仮想環境の中にパッケージをインストールする必要があります。

以下で、OSのバージョンがbookwormのRaspberry Piにpythonの仮想環境を立ち上げて、ライブラリをインストールしてプログラムの動作を確認してみます。
また、Node-REDのexecノードからpythonプログラムを実行する際にも、仮想環境が必要になります。その方法に関しても後半で説明します。

ここで新たにインストールするライブラリは以下の2つです。
  matplotlib
  paho.mqtt

2.仮想環境の作り方

ここでは、~/source/python/TH-sensor/の下にプログラムを置いているので、仮想環境をこのディレクトリの下に作ります。

を実行します。
すると、.venvというディレクトリが作られ、その下にファイルが作られています。”.venv”のディレクトリ名は何でもOKです。

3.仮想環境への入り方/仮想環境の抜け方

3.1. 仮想環境に入る

.venv/binの下を見てみると、activateというファイルがあります。仮想環境を起動するには、このファイルを“source”します。

$ source .venv/bin/activate

と実行します。すると仮想環境になってプロンプトの前に(.venv)が付きます(この名前はオプションで変えられます)。

3.2. 仮想環境を抜ける

仮想環境を抜けるには、

$ deactivate

と入力します。すると仮想環境から抜けられます。

4.仮想環境でのパッケージのインストール

仮想環境に入って、パッケージをpipでインストールします。順番に、matplotlib、paho.mqttをインストールします。

5.仮想環境でのプログラムの実行

pythonのプログラムを実行します。以下に示すようにmatplotlibが動作しました。

では、source .venv/bin/activateを一つの端末で実行すれば、他の端末でも有効なのかを確認してみます。
下に示すように、左の端末でactivateを実行し、右の端末画面で、pythonを実行しましたが、matplotlibが無いとエラーになりました。
仮想環境は、activateを実行した画面でしか有効ではないです。

6.仮想環境の削除

仮想環境を削除するのは、作成時に指定したディレクトリを消すだけです。ここでは、作成時に以下のコマンドを使いました。

$ python –m venv .venv

そして、カレントディレクトリに.venvというディレクトリが出来ていますので、それを削除します。

$ rm –r .venv

7.Node-REDのexecノードでの仮想環境(単純コマンドの場合)

ここまでで、端末画面からの仮想環境の構築に関してはうまく行き、無時にプログラムも動作しました。次に、Node-REDのexecノードではどうかを調べます。
結論を先に述べると、シェルスクリプト準備して、その中で仮想環境を立上げ、そしてプログラムを実行する必要があります。
以下順番に見ていきます.
Node-REDのexecノードのコマンド欄に5章で動作確認した時と同様に、以下のコマンドを記載し、デプロイし、実行した時の画面を下に示します。

$ python /home/pi/source/python/TH-sensor/linear.py

matplotlibが無いというエラーが出ています。

これは、5章の後半の実験で確認したように、新しい端末画面では仮想環境が有効にならないからです。
そこで、実行する際に、シェルスクリプトを準備し、その中で①仮想環境を有効にし、②pythonのプログラムを実行し、③仮想環境を無効にします。
具体的には、以下の様なシェルスクリプトを準備します(仮に、th.shという名前にします)。

そして、実行権限を与えるために、以下のコマンドを実行します。

$ chmod 744 th.sh

以下に実行権限を与える前後のファイルの属性を示します。実行権限を与えた後では、読み書きだけの’rw’権限が、実行権限も加わって、’rwx’になっているのが分かります。また、ファイルの名称の色も緑に変わっています。

この状態でNode-REDで実行すると以下の様に、正常終了します。

8.Node-REDのexecノードでの仮想環境(無限ループを持つプログラムの場合)

次に、少し特殊なケースになりますが、pythonのプログラムが無限ループを持つ場合を考えてみます。具体的には、下の図のように、PythonのプログラムがNode-REDからサブスクライブしたデータを処理し、結果をパブリッシュで送り返す場合です。この場合、python側では、サブスクライブしているので、無限ループで待っています。pythonのサンプルプログラムは、Appendixに載せてありますので、参考にしてください。

ここでは、main関数の部分を抜粋します。mqtt_client.loop_forever()の部分で無限ループに入って、サブスクライブデータを待っています。キーボードからCtrl-cを入力することでプログラムを終了できます。その際に、’mqtt_client.disconnect()’を実行し、mqttの後処理を行い、 “MQTT disconnected: {broker_address}“を表示します。

def main():
    # MQTT ブローカーに接続
    mqtt_client.connect(broker_address, port)
    print(f"MQTT connected: {broker_address}")
    mqtt_client.on_connect = on_connect
    mqtt_client.on_message = on_message

    try:
        mqtt_client.loop_forever()
    except KeyboardInterrupt:
        # MQTT クライアントを切断
        mqtt_client.disconnect()
        print(f"MQTT disconnected: {broker_address}")

8-1.端末からpythonプログラムを実行する場合

下の図では、端末から、’python test_venv.py’を実行し、終了時にCtrl-c(^C)を入力することで、プログラムがKeboarInterruptの処理に飛んで、後処理後、‘MQTT disconnected: localhost’を表示しています。
もう一つ止める方法は、実行しているプログラムのID番号を調べて、killコマンドで止める方法です。

次の2つの端末の上の画面で、’python test_venv.py’を実行しています。下の画面で、‘ps ax’コマンドでプログラムのID番号を調べています。
‘python test_venv.py’のプログラムIDが’103898’であることが分かります。killコマンドでプログラムを止めるのですが、Ctrl-cに相当するSIGINTを発行するのが、オプションの2番です。そこで、‘kill -2 103898’を入力することで、プログラムが、KeboarInterruptの処理に飛んで、‘MQTT disconnected: localhost’を表示しています。

単に、‘kill 117539’だけでは、次に示すように、Teminatedで終了するだけで、MQTTの後処理が実行されませんので、注意が必要です。

8-2.Node-REDからプログラムを実行する場合

Node-REDからプログラムを実行する場合を考えます。
仮想環境ではなかった時には、execノードで起動して、終了時にinjectノードでmsg.killに、“SIGINT”を入れてexecノードに送れば、Ctrl-cで止めたのと同様に止めることが出来ました。しかし、仮想環境になって、シェルスクリプトで実行することになりますので、msg.killをexecノードに送っても、シェルスクリプトが終了するだけで、pythonプログラムは終了しません。ゾンビとして残ります。
更に、起動すると、もう一つpythonプルグラムが実行されて、2つpublishされてきます。

対策の一つの例としては、killする際にもシェルスクリプトを走らせ、意図的にプログラムを終了させます。ただし、pythonプログラムを‘kill -2 (PID)’ コマンドで止める必要があり、PIDが毎回変わるので少し面倒です。

シェルスクリプトの例を、以下に示します。名前をkill.shとします。2行目で、ps axを実行し、PIDを取得し、’kill -2 (PID)’コマンドにして、‘kill.lst’というファイルに保存します。3行目で、その’kill.lst’を実行しています。

#!/usr/bin/bash
ps ax | grep "test_venv.py" | grep python | awk '{print "kill -2",$1}' > kill.lst
source kill.lst

このシェルスクリプトを走らせることで、pythonのプログラムがゾンビにならずに消えています。

ここでは、力技でプログラムをkill方法を示しましたが、pythonプログラムで、subscribeしたデータがある値だったらプログラムを終了するというようにする方法もあります。

Appendix. pythonプログラムのコード


'''
venv_test.py
 venv環境のtest program
'''
import paho.mqtt.client as mqtt
import json
import datetime

# MQTT ブローカーの設定
broker_address = "localhost"
port = 1883
topic = "count-value"
topic2 = "number-time"

# MQTT クライアントを作成
mqtt_client = mqtt.Client()

sdata = {    # 辞書型データ
    'number': 0, 
    'time' : '1024'
}

def publish(sdata):
    # Publish on MQTT
    mqtt_client.publish(topic2, sdata)

def on_connect(client, userdata, flags, rc):
    print("Connected with result code "+str(rc))
    client.subscribe(topic)

def on_message(client, userdata, msg):
    dict_data = json.loads(msg.payload)
    N = dict_data["count"] + 1000

    # 現在の時刻を取得
    time = datetime.datetime.now()

    sdata['number'] = N
    sdata['time'] = time.strftime('%Y-%m-%d %H:%M:%S')
    print(sdata)
    data_json = json.dumps(sdata)
    publish(data_json)
    
def main():
    # MQTT ブローカーに接続
    mqtt_client.connect(broker_address, port)
    print(f"MQTT connected: {broker_address}")
    mqtt_client.on_connect = on_connect
    mqtt_client.on_message = on_message

    try:
        mqtt_client.loop_forever()
    except KeyboardInterrupt:
        # MQTT クライアントを切断
        mqtt_client.disconnect()
        print(f"MQTT disconnected: {broker_address}")

if __name__ == "__main__":
    main()