【Python】Pyinstaller と PySimpleGUI でアプリを簡単に共有

2022/08/31Python,アプリ

Python でなんらかのツールを作った際、皆さんはどうやって共有しますか。

この記事では、「カッコいいツールではないけど、相手に付加的な準備をさせず、とりあえず目先の自動化・効率化に十分使えそうな Python ツールの共有」を実現する「実行ファイルにして渡す」方法について書きたいと思います。(ちなみに実行ファイルなんで作り手の悪意を考えるとセキュリティはゴミです)

以下、私なりに実行ファイルにして配布することのメリット・デメリットをまとめてみました。

実行ファイルにして配布するメリット

  • 配布先(ユーザー)に環境整備などの工数がかからない(ファイル共有して、ダブルクリックで実行)
  • (プログラムで明示しない限り)インターネットに繋げないで実行できるので、作成元が信頼できればセキュリティ上安心
  • 実行ファイル化が簡単(サーバー立てたり、外部に何か申請したりする必要なし)

実行ファイルにして配布するデメリット

  • メンテナンスが最悪に面倒、再度実行ファイル化をやり直して再配布する必要がある
  • OS など、ユーザーに合わせた開発環境を用意する必要がある
  • 共有する実行ファイルは基本的に重たい(100MB は軽く超えると思います)

そして、これを実現するのためのパッケージがこちら:

Pyinstaller…Python で作成したプログラムを exe ファイル(実行ファイル)にしてくれるパッケージ

PySimpleGUI…Python で簡単に GUI を作ることができるパッケージ。

実行前の注意

そもそもこれを満たさないと動かないよっていう要件や、理由はわかってないけど手こずりました的なことを書いておきます。

  • 異なる OS に対する変換には対応していないので、配布先の OS と同じ OS で実行ファイルを作成する必要があります
  • OS が同じでも、バージョンが異なると実行できないことがあります
  • Python3.7 でやるのが安定(2021/10/22時点)
  • Mac だとうまくいかない部分が多い、、pyenv でバージョンを3.7.12にしてやってますが、どうやら PySimpleGUI をうまく認識してくれてないみたいで(うまくいく方、ぜひ教えてください)

要するに、「Windows 環境で Python3.7 をインストールして、同じバージョンの Windows を使っている相手に対する共有」が少なくとも現状ではうまくいくケースの1つなのかなと思います。ただ、一応公式では Python3.5~3.9 で動くと書いてありますので、お試しください。

ちなみに私は Mac ユーザーですが、どうも Mac と pyinstaller との相性が悪いようなので、AWS で Windows インスタンスを立てて、そこで実行ファイルを作成しています。。。

GUI を作り、実行ファイル化してみる

GUI の実行ファイル化までの大まかな道のりは以下の通りです:

  1. 仮想環境に入る
  2. ロジック部分の Python プログラムを作成
  3. PySimpleGUI の枠組みに2で作成したプログラムを組み込む
  4. pyinstaller で実行ファイル化

ちなみに、今回は「アヤメの大きさの情報を与えた時、それが Setosa である確率を計算するツール」を GUI で設計します。それでは順番にやってみます。

仮想環境に入る

まず、実行ファイル作成にあたり、パッケージの依存関係やらなんやらでごちゃごちゃしないように仮想環境に入りましょう。

Mac の場合

> python3 -m venv pyinstaller_env
> source pyinstaller_env/bin/activate

Windows(PowerShell)の場合

まず、PowerShell でスクリプトの実行許可を与えるために、以下のコードを最初の一度だけ実行します。

> Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -Force

仮想化は、

> python -m venv pyinstaller_env
> pyinstaller_env/Scripts/activate.ps1

で完了です。

ちなみに、ターミナル(Windows なら PowerShell)で左端に (pyinstaller_env) とついていたら無事仮想環境に入れてます。

なお、必要なパッケージは随時仮想環境に入った状態で pip でインストールしてください

Python プログラムを作成:アヤメの種類判別

iris データセットを使って、アヤメの種類の2値分類モデルを推定し、新たな入力に対して分類結果(確率)を出力するプログラムを書きます。

今回はモデルの精度などはどうでもよいので、Setosa か否 かの2値分類とし、手元のデータ全てを使ってロジスティック回帰モデルを推定します。

from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
import pandas as pd

# iris データの読み込み
data = load_iris()
X = pd.DataFrame(data.data, columns = data.feature_names)
y = pd.DataFrame(data.target, columns = ['Species'])

# 2値分類にするため、Virginica を Versicolor と同じ1にして Setosa (Species が0) or not にする
y = y.replace({2: 1})

# フィッティング
model = LogisticRegression()
model.fit(X, y['Species'])

これでとりあえず、がくと花弁それぞれの長さと幅の4つの値から Setosa か否かを予測するモデルができました。

入力値には1サンプルを想定し、予測部分は以下のようにしました。

def predictor_ayame(values, columns, model):
    x_input = pd.DataFrame(
            values, 
            index = columns).T
    result = model.predict_proba(x_input)
    result_str = str(round(result[0][0], 4) * 100) + '% の確率で Setosa です'
    return result_str

この predictor_ayame の引数に4つの値、変数名、学習済みモデルを与えれば推定結果が文字列で返ってきます。ちなみに、後の GUI での使い方を考慮してこのようなちょっと変わった関数にしています。

PySimpleGUI を導入

ロジック部分はできたので、GUI にします。PySimpleGUI はその名の通り本当にシンプルで、他の GUI 作成パッケージよりも少ないコードで GUI を作ることができます。

まず、最初に表示する文字列や入力ボックス、実行ボタンを定義し、配置していきます。

# import PySimpleGUI as sg

# GUI のテーマを設定
sg.theme('DarkAmber')

# 各パーツの配置
layout = [
    [sg.Text('がくの長さ (cm):'), sg.Input(key = 'length_s')], 
    [sg.Text('がくの幅 (cm):'), sg.Input(key = 'width_s')],
    [sg.Text('花弁の長さ (cm)'), sg.Input(key = 'length_p')],
    [sg.Text('花弁の幅 (cm)'), sg.Input(key = 'width_p')],
    [sg.Button('アヤメの種類を推定'), sg.Button('やめる')], 
    [sg.Text('推定結果:'), sg.Text("", key = 'result')]
]

今回登場するのは以下の3つです。

  • sg.Text():文字列の表示
  • sg.Input():入力ボックス
  • sg.Button():ボタン(と表示させる文字)
  • 引数 key :値を参照する時に使うキー

他にもファイルを参照するための sg.FileBrouse() やラジオボタン sg.Radio() など色々あります。詳しくはこちらをご参照ください。

このコードでは次のようなGUIとなります。

リストの要素それぞれが1行に対応しており、コードと結果の対応関係がわかりやすいです。

ここから、「値を入力してボタンを押すと、予測ロジックが走り、推定結果を出力する」という流れをコーディングしていきます。

まず、「やめる」ボタンやウィンドウを閉じるアクションがされた時に実行を終える、逆にいうと中止アクションがあるまでループさせる処理を書きます。

window = sg.Window('Setosa 検知ツール', layout)
while True:
    event, values = window.read()
    # 終了条件
    if event == sg.WIN_CLOSED or event == 'やめる':
        break
    # 推定ボタンが押された時の処理
    if event == 'アヤメの種類を推定':

もし他に実行ロジックを加えたいのであれば、同じように if event == ~~ を追加していけばよいだけです。

表示部分、実行する条件、終了条件が決まったので、もともと作っていたロジック部分を組み込みます。今回の場合は、predictor_ayame() を推定ボタンが押された時に呼び出すようにして、その結果を「推定結果:」の部分に書き足す形で反映させます。

while True:
    event, values = window.read()
    # 終了条件
    if event == sg.WIN_CLOSED or event == 'やめる':
        break
    # 推定ボタンが押された時の処理
    if event == 'アヤメの種類を推定':
        # ここに処理を追記
        input_values = [values['length_s'], values['width_s'], values['length_p'], values['width_p']]
        result = predictor_ayame(values = input_values, columns = X.columns, model = model)
        window['result'].update(result)

計算結果をウィンドウへ反映する際は、window['result'].update(result) のように、’result’ キーを持つボックスへ変数 'result’ の値を反映させる、といった書き方をします。

これまでに書いたコードをまとめればアヤメが Setosa か否かを判別する GUI ツールが完成です。これを 'ayame_gui.py’ として保存しておきます。

from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
import pandas as pd
import PySimpleGUI as sg

# iris データの読み込み
data = load_iris()
X = pd.DataFrame(data.data, columns = data.feature_names)
y = pd.DataFrame(data.target, columns = ['Species'])

# 2値分類にするため、Virginica を Versicolor と同じ1にして Setosa (Species が0) or not にする
y = y.replace({2: 1})

# フィッティング
model = LogisticRegression()
model.fit(X, y['Species'])

# 予測
def predictor_ayame(values, columns, model):
    x_input = pd.DataFrame(
            values, 
            index = columns).T
    result = model.predict_proba(x_input)
    result_str = str(round(result[0][0], 4) * 100) + '% の確率で Setosa です'
    return result_str

# GUI のテーマを設定
sg.theme('DarkAmber')

# 各パーツの配置
layout = [
    [sg.Text('がくの長さ (cm):'), sg.Input(key = 'length_s')], 
    [sg.Text('がくの幅 (cm):'), sg.Input(key = 'width_s')],
    [sg.Text('花弁の長さ (cm)'), sg.Input(key = 'length_p')],
    [sg.Text('花弁の幅 (cm)'), sg.Input(key = 'width_p')],
    [sg.Button('アヤメの種類を推定'), sg.Button('やめる')], 
    [sg.Text('推定結果:'), sg.Text("", key = 'result')]
]

# ウィンドウの表示と動作
window = sg.Window('Setosa 検知ツール', layout)
while True:
    event, values = window.read()
    # 終了条件
    if event == sg.WIN_CLOSED or event == 'やめる':
        break
    # 推定ボタンが押された時の処理
    if event == 'アヤメの種類を推定':
        input_values = [values['length_s'], values['width_s'], values['length_p'], values['width_p']]
        result = predictor_ayame(values = input_values, columns = X.columns, model = model)
        window['result'].update(result)

(全部でたったの51行!!)

これを実行すればこんな感じで遊べます。

GUI ツールができたところで、Python 環境を持たない方々でも使えるように実行ファイルにします。

Pyinstaller で実行ファイル化

今作業ディレクトリには ayame_gui.py と pyinstaller_env フォルダがある状態だと思います。この状態から、pip で pyinstaller をインストールし、実行ファイル化をします。

ただし、今回はややこしいことに sklearn が内包するデータ(iris.csv)を使っています。pyinstaller は基本的に、import を辿って必要なモジュールを同梱してくれますが、パッケージファイルのどこかにあるデータまでとってきてくれるわけではありません。つまり、「このデータを使ってください」という追加の記述が必要になります。以下の二つのスクリプトを用意し、ayame_gui.py と同じディレクトリに保存します(後述しますが、同じディレクトリでなくても後から指定可能)。

from PyInstaller.utils.hooks import collect_data_files
datas = collect_data_files('sklearn.datasets')
from PyInstaller.utils.hooks import collect_submodules
hiddenimports = collect_submodules('sklearn')

1つ目が「sklearn.datasets 内のデータを参照」、2つ目は「スクリプトにはないけど sklearn 内にあるモジュールも import」をそれぞれ表します。ちなみになぜ2つ目が必要かはわかっていませんので、詳しい方ご教示お願いします。。。

この hook-ModuleName.py についてはこちらをご参照ください。ざっくり説明すると、pyinstaller を実行するときに ayame_gui.py 内で ModuleName が import されたとき、hook-ModuleName.py を実行して追加処理を行うものです。今回の場合、ayame_gui.py 内で sklearn がインポートされているので、追加で「データファイルを集めること」「サブモジュールをインポートすること」の二つの処理が追加されます。

hook ファイルによって使用データの指定などができたので、いよいよ実行ファイル化します。コマンドは以下の通りです。

> pyinstaller ayame_gui.py --onefile --additional-hooks-dir=.

--additional-hooks-dir="hook ファイルがある場所" で読み込ませたい hook ファイル(表記ややこしくて恐縮ですが、.py です)の場所を指定します。今回は ayame_gui.py と同じディレクトリに hook-~.py を置いているため、--additional-hooks-dir=. と指定します。

ちなみに、今回のようにパッケージ内のデータにアクセスしなくてはならないなど、ややこしい事情がなければ次のコマンドですみます。

> pyinstaller ayame_gui.py --onefile

以上の手順により、作業ディレクトリに “build" と “dist" の2つのフォルダが生成されますが、出来上がった実行ファイル ayame_gui.exe は “dist" にあります。

あとはこの実行ファイルを共有すれば、誰でも簡単に Python で作られたツールを使うことができます。

PythonPython,アプリ

Posted by S