PySimpleGUI と PyInstaller で 整数リストを並び替えて出力するGUIアプリケーションを作ってみた

# 0.発端

子供(小学2年生)の冬休みの宿題として
「__掛け算九九の各段について、ランダムに並び替えた数字の順で間違えないように暗唱する__」
というものがあり、その「ランダムな数字の順」を考えるのが面倒なので、Pythonにやらせてみようと思ったのが事の発端です。
(Qiita様と投稿者の皆々様のエントリには常日頃お世話になっており、何かひとつ投稿してみたいと考え、当エントリを作成しました)

# 1.整数リストをランダムに入れ替えるスクリプト
最初に作ったのは「1から9までのリスト」をランダムに並び替える関数でした。
作っているうちに欲が出て「リストを違うパターンで並び替えて出力する」仕様も追加しました。

```RandomList.py
import sys
import random
import copy

def random_list(repeat=1, num=10):
    # 1からnumまでのリスト
    numbers = list(range(1,num))

    # 繰り返し
    numbers_list = []
    i = 0
    while i < repeat:
        # ランダマイズ
        random.shuffle(numbers)
        numbers_list.append(copy.deepcopy(numbers))
        # インクリメント
        i+=1

    return numbers_list

# コマンドライン呼び出し
if __name__ == "__main__":
    args = sys.argv # パラメータ引数
    repeat = 1      # 繰り返し出力数
    num = 10        # 配列の要素数
    if(len(args) > 1) :
        repeat = int(args[1])
    if(len(args) > 2) :
        num = int(args[2])
    
    numbers_list = random_list(repeat, num)

    for numbers in numbers_list:
        print(numbers)
```
## 全行バラバラになるはずなのに
行ごとに並びが変わる事を期待し、以下のフロー(抜粋)で実装しました。
```python
numbers = list(range(1,num))      # 1～9までの整数リスト
while i < repeat:
    random.shuffle(numbers)       # 整数リストをランダムに並び替える
    numbers_list.append(numbers)  # リストのリストに追加
```
が、予想に反し、すべての行が同じ並びで出力される事に。
原因は最後の1行で、`numbers_list.append(numbers)`でリストを直接appendしても、IDが参照で渡されているだけなので、追加後に`numbers`の順番が再抽選されると`numbers_list`のすべての行が同様に並び替わった状態になっていました。

解決策として`numbers`を参照渡しではなく`copy.deepcopy(numbers)`で複製してappendすることで期待通りの結果が得られました。

```RandomList.pyの出力結果
> Python3 .\RandomList.py 10
[8, 2, 5, 6, 9, 7, 3, 4, 1]
[4, 3, 9, 6, 2, 7, 1, 5, 8]
[8, 1, 2, 4, 9, 5, 3, 6, 7]
[5, 3, 6, 8, 2, 9, 4, 7, 1]
[2, 3, 4, 5, 8, 1, 9, 7, 6]
[1, 8, 4, 3, 6, 7, 9, 2, 5]
[1, 4, 5, 7, 9, 6, 2, 3, 8]
[9, 7, 1, 6, 5, 8, 3, 4, 2]
[7, 4, 6, 1, 5, 3, 8, 9, 2]
[4, 6, 7, 5, 2, 9, 8, 3, 1]
```
当問題の解決に際し、こちらのエントリを参考にさせていただきました。

https://qiita.com/kokorinosoba/items/e9ab9398af5b44d2ac9a

# 2.PySimpleGUIでアプリケーション化

コマンドラインで直接呼び出して数値が出るのも、子供ウケとしては悪くはありませんが、どうせならGUIアプリケーション化するほうが子供も使いやすいかもしれない。
これは学習の好機と考え、「PySimpleGUI」を用いたGUIアプリケーション化を行いました。

![Python_GuiRandomList.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/633534/1732e345-30ab-dfc8-f4e8-2bce9c47376b.png)

```GuiRandomList.py
import PySimpleGUI as sg
import RandomList as rl

# GUIウインドウテーマ
sg.theme('Python')

# 入力フレーム
frame1 = sg.Frame('入力',
  [
    [
      sg.Text('繰り返し回数'),
      sg.InputText(key='-INPUTTEXT-', default_text='10', size='3'),
      sg.Text('回')
    ],
    [
      sg.Submit(button_text='生成', key='button_create')
    ]
  ]
)

# 出力フレーム
frame2 = sg.Frame('出力',
  [
    [
      sg.MLine(size=(18,20), key='-OUTPUT-', disabled=True)
    ]
  ]
)

# レイアウト
layout = [
  [
    frame1
  ],
  [
    frame2
  ]
]

# Window生成
window = sg.Window('ランダム数字リスト生成アプリ', layout, resizable=True)

# GUI表示実行部分
while True:
  # ウインドウ表示
  event, values = window.read()

  # クローズボタン
  if event is None:
    print('exit')
    break

  # ボタンが押された場合
  if event == 'button_create':
    repeat = int(values['-INPUTTEXT-'])
    numbers_list = rl.random_list(repeat)
    output = ''
    if len(numbers_list) > 0:
      for numbers in numbers_list:
        for number in numbers:
          output += str(number) + ' '
        output += '\n'
    window['-OUTPUT-'].Update(output)

window.close()
```
概要としては
`sg.InputText`、`sg.Submit`、`sg.MLine`それぞれに`key`をつけて参照可能にした上で、
InputText の値の数だけリストを繰り返し生成しランダムに並び替え、
`output`に文字列として整形した上で MLine に出力するという処理を行っています。

PySimpleGUIの使用に際し、以下のエントリを参考にさせて頂きました。

https://qiita.com/konitech913/items/61dc715ddaad54505a29

https://qiita.com/ku_a_i/items/20004800adc78fa94a0b

# 3.PyInstallerでスクリプトのexe化

GUIアプリケーションとしての体裁が整ったところで、今度は単独で動かせるようにexe化を試みました。
PyInstallerを用いることでexe化が可能だと知り、導入しようとしたのですが…

## PyInstallerが実行できない?
Windows10のPower Shell上でpipを用いてPyInstallerをインストール。
```python:console
pip install PyInstaller
```
他のエントリでも書かれているように、以下のコマンドを実行してもpyinstallerが見つからない。
```python:console
pyinstaller GuiRandomList.py   # pyinstaller.exe が Not Found
```
PATHを通せばよいのかもとも思ったのですが、ネットの海を漁ってみると
「__PyInstallerは仮想環境で実行してexe化する__」
という情報にたどり着きました。

https://qiita.com/kunishou/items/b2e8754886107292926d

:::note info
### なぜ仮想環境上でexe化するほうがいいの?

PyInstallerは、自身を実行した環境にインストールされているすべてのモジュールを内包した状態でexe化するらしく、通常の環境のままだと不必要なモジュールを内包した状態でexeを作成するため、exeが肥大化するそうです。
なので、仮想環境で必要最低限のモジュールをインストールした状態でexe化する方が良いそうで。
:::

## 仮想環境の構築とモジュールのインストール

pythonのvenvコマンドで、PyInstaller用の仮想環境`GuiRandomListEnv`を作成。
```python:console
python -m venv GuiRandomListEnv
```
作成された仮想環境を起動。
```python:console
.\GuiRandomListEnv\Scripts\activate
```
pipを最新化し、今回使用しているモジュール`PySimpleGUI`と`PyInstaller`をインストール。
```python:console(仮想環境)
pip install -U pip        # pipの最新化
pip install PySimpleGUI
pip install PyInstaller
```

## PyInstaller実行によるexe化
仮にスクリプト`GuiRandomList.py`が`c:\example\scripts\`に配置されているとして、以下の手順でexe化します。
```Python:console(仮想環境)
cd c:\example\scripts\
pyinstaller GuiRandomList.py
```
上記コマンド実行後、`GuiRandomList.py`と同じフォルダ内に、以下のフォルダ・ファイルが生成されます。
```python:c:\example\scripts\
build\              # ビルド時の一時ファイルの格納フォルダ
dist\               # 生成された実行ファイルの格納フォルダ
GuiRandomList.spec  # ビルド中間ファイル
```
オプションを設定しなければ、`dist\`配下に`GuiRandomList.exe`と共に、実行に必要なファイルが吐き出されます。
下記オプションで出力内容を変える事も可能です。
```python:pyinstaller実行オプション
--onefile (-Fでも可)   # 出力ファイルを1つにまとめる
--onedir  (-Dでも可)   # 出力フォルダを1つにまとめる
--noconsole (-wでも可) # 実行時にコンソール画面を表示しない
```
個人的には「exeファイルひとつ」が「コンソールが開くことなく」実行できて欲しいので、下記オプションで出力しました。短縮版はハイフンの後に複数続けて書く事も可能です。
```python:pyinstaller実行オプションサンプル
pyinstaller -wF GuiRandomList.py
```
実行結果として、`dist\`フォルダ下に、以下の実行ファイルが生成されました。
無事起動を確認し「__整数リストを並び替えて出力するGUIアプリケーションのexe__」が完成しました。
![GuiRandomList.exe.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/633534/fbcd0b5c-10a8-9604-e867-d116c8d96ccf.png)

# 3'. PyInstallerによるMac向けアプリ化
Pythonの仮想環境上でPySimpleGUIとPyInstallerをinstallしてpyinstallerすればいいだけなら、Macでも同様の手順でアプリケーションが作れるのでは?
と思い、GitHub経由でリソースをMacに持ってきて、アプリケーション化を試みました。

仮想環境の構築手順は変わらず。
```Python:console
python -m venv GuiRandomListEnv
```
仮想環境に入る際のコマンドやフォルダ構成がWindowsのPower Shellとは違いました。
```Python:console
source GuiRandomListEnv¥bin¥activate
```
入ってしまえばあとは変わらず。
```Python:console(仮想環境上)
pip -U install pip
pip install PySimpleGUI
pip install PyInstaller
```
```Python:console(仮想環境上)
pyinstaller -wF GuiRandomList.py
```
これで、Windows同様`dist¥`フォルダ下にアプリケーション`GuiRandomList`が生成されました。
ダブルクリックで起動も確認出来ました。
![スクリーンショット 2022-12-28 21.39.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/633534/d7b87173-baa0-1151-a19b-6f5501b3d42a.png)