LoginSignup
9
7

More than 1 year has passed since last update.

ちょっと思う所があったので(もちろんお仕事関係)、頑張って作ってみたよ。

また、M1 Mac ではどうなるか分からない。俺たちの楽園は失われてしまった。OS 非搭載のマシン買って Ubuntu 載せようぜ・・・

例題

Wordle というゲームが人気だそうだ。どれぐらい人気かと言うと、Google で "Wordle" と検索すると、Google のアイコンが Wordle するぐらい人気だ。

やってみたら面白かったので、自分で作ってみた。

しかしこれは今回の本題ではない(ただの自己顕示欲)。

このようにゲームっぽいものは、当然 GUI が必要である。そして私は普段 Python ばかり書いているので、あまり Python 以外の言語を使いたくない。しかし究極の目標として、どのプラットフォーム(Mac OS, Linux, Windows)でも動作する、ワンクリックだけで起動するアプリにしたい。

今回の例はゲームだけど、普通にビジネスでも「アプリを作ってくれ」という事はあり得る。こういう時にどうすれば良いのか。それを考えて試したものを、まとめたのがこの記事である。

1. Webブラウザ + Backend

通常の Web アプリ開発で backend に Python を使う場合は、以下の形が多いと思う。

  • Fontend : nginx などの web サーバ上で動くものを JavaScript や golang で作り、計算処理が必要なものは Backend に投げる
  • Frontend - Backend 間通信 : REST API
  • Backend : FlaskFastAPI で REST API の受け口を作り、内部の処理を普通に Python で書いて応答するものを作る

Flask と FastAPI のどちらが良いのかという問題は難しい。ぱっと見はどちらも同じだから。新しいし Pydantic を使っているので、私は FastAPI の方が好み。

今回は「なるべく」Python だけを使いたいので、Jinja で HTML テンプレートに値を埋め込んで、HTML を生成するところまでやることにする。

これは何も難しいことはない。ググればやり方はいくらでも出てくる。

2. Electron + Backend

まず不満なのは、Web ブラウザなので「アプリ感」が無いことである。そして動かすまでに、Backend サーバを立ち上げて、それからブラウザを立ち上げて、さらに表示する URL を叩くという3ステップが必要なので、めんどくさいというのがある。

まずは「表示する URL を叩く」というステップを減らすために、専用ブラウザを作る。

専用ブラウザと言っても、localhost の特定のポートにアクセスするだけの Electron を作るだけである。

たった 30 行の JavaScript を書くだけなので、これも簡単。

既に「Python だけ」じゃなくなってるけど、Frontend を作る大変さを思ったら不満は無い。

3. Electron から Backend を立ち上げる

さらに「Backend サーバを立ち上げる」というステップを減らすために、Electron の中から Backend サーバを立ち上げる。

具体的には、child_process パッケージを用いて fork して、Python を動かす場合には spawn()、バイナリを起動する場合には execFile() すれば良い。サンプルもあるし。

ここまででもうかなりアプリ感はある。そしてここまでで満足したならば、全人類が幸せだった。

4. Backend をバイナリ化する

究極の目的として「マルチプラットフォーム」があるので、Python で動かしている backend サーバをまるっとアプリという形にしたい。特に Python はみんな普通に仮想環境を作って動かしているだろうから、その仮想環境をまるっと全部バイナリにしたい。

これを実現するのが PyInstaller である。

ところが PyInstaller には以下の問題がある。(伝聞も含む)

  1. 作成されるバイナリのサイズがデカイ
  2. Framework としてインストールされた Python じゃなければならない
  3. FastAPI が動かない
  4. --onefile オプションを使うと起動するまで非常に時間がかかる
  5. 外部ファイルにアクセスするプログラムだと非常に不安定になる

1 番の問題に関しては、とにかく使う Python Package を限りなく少なくするしかない。今回は numpy ぐらいだったので、27MB 程度で済んでいる。

2 番目の問題は、PyInstaller を使おうとした一番最初にぶつかった壁。とりあえず使っていた仮想環境を一旦消し、env PYTHON_CONFIGURE_OPTS="--enable-shared" pyenv install 3.10.2 等として特定のバージョンの Python を Framework とすることにして、仮想環境を作り直すことで対処した。

そして悲しい 3 番目。良く分からないけど、とにかく動かない。「そのポートは既に使われてるよ」とエラーメッセージが出るので、Uvicorn の問題なのかもしれない。解決する力量は残念ながら私には無い。このために FastAPI で書いたものを全部 Flask に書き直した。(大した手間じゃない)

さらに 4 番目。--onefile を付けると起動まで 30 秒ぐらいかかる。何故は不明。--onefile をつけないとフォルダが作られて色々なライブラリが詰め込まれている状態になっているが、まぁ特に問題はない。この諦めの境地に至るまで随分と時間を浪費した。ちなみに --onefile を付けなくても、一回目の起動には何故か時間がかかる。そんなものだと諦める。Linux だとサクっと動くので、Mac OS の問題なのかもしれない。

最後に 5 番目。これが最もハマった。

--onefile オプションをつけて全体のひとつのファイルにまとめたり、そうでなくても仮想環境を丸ごと複製しているので、ファイルの場所は当然変わってくる。まぁしかし、実行するメインのバイナリとの相対ファイルパスが同じであれば(同じ様に参照できるならば)問題は無い。はずである。

実際に --add-data オプションがあり、ご丁寧にも生成前後のパスを指定しなければならないので、ちゃんと指定すれば良いだけのはずである。しかし、使うファイルのパスには以下のパターンがある。

./ (このディレクトリで python -m some_app と起動する)
├── another_module/
│   ├── __init__.py
│   ├── hogehoge.py
│   └── outer_data.txt
└── some_app/
    ├── __init__.py
    ├── __main__.py
    └── data/
        └── inner_data.txt

今回でいうと、Wordle で使う単語を収集するスクリプトを Wordle 本体とは別のモジュールとして作成し、そのデータを Wordle 本体に読み込ませていた。この時、上の例で言えば some_apppython -m some_app というコマンドで起動することにしてしまえば、some_app から outer_data.txtanother_module/outer_data.txt というパスで参照できる。

Wordle は他にも読み込むデータ(ファイル)がある。HTML テンプレートや CSS ファイルである。これらは(Flask の慣例的に)モジュール本体のディレクトリ内部に置かれている。(上の例で言えば inner_data.txt に相当)

じゃあこれを PyInstaller でバイナリ化したいので両方とも取り込みたいってなった時に、それぞれ --add-data にどうやって指定するかと言うと、試行錯誤の結果、こうなった。

pyinstaller --add-data some_app/data:data --add-data another_module:another_module some_app/__main__.py

よく考えればおかしい話で、本体の下のディレクトリは本体のディレクトリを省いて指定している。要するに pyinstaller で固めた先は、このモジュールの中の世界(some_app というディレクトリが基本となっている世界)のはずだ。しかし本体の外にあるものは、そのままのパスで指定している。要するに、python を叩いたディレクトリが基本になってしまっている。通常の Python ならば後者が当然なのだが、pyinstaller でバイナリ化した後はどちらが本来あるべき姿なのか。

理由をすっ飛ばして結論から言えば、前者が正しいのだろう。だって outer_data.txt に相当するものを Python スクリプトの中に強引に埋め込んで some_app の中に入れたら、すごい快適に動くようになった。

Linux でもそうだったので、Mac OS に限定した話ではないと思う。

  1. 外部のファイルを読み込むな。Python にデータを埋め込んで、それを読ませろ
  2. どうしても外部のファイルを読みたい場合は、同じモジュール内(同じディレクトリ内)に置け

PyInsterller を使う際には守りたいルール。

5. 全部をまとめてインストーラーを作る

後はこれらを electron-builder を使ってパッケージを作るだけだ・・・と思ったのだが・・・

まだ罠はあった。electron-builder の圧縮ツールである asar の問題。

asar での圧縮を OFF にするとビルドの際に怒られる。しかし圧縮すると、動作が重くなる、プロセス名が分からなくなって終了時に backend を落とせなくなるなどの問題が発生した。恐らくは PyInstaller でバイナリ化してるのに、さらに asar で圧縮しているのが良い無いのかなぁと。

下に状況をまとめてみた。

PyInstaller --onefile electron-builder asar dmg のサイズ 起動 backendの終了
なし なし 103MB 初回だけ遅い 可能
あり なし 103MB 遅い 可能
なり あり 103MB 遅い 出来ない
あり あり 103MB 遅い 出来ない

サイズもあんまり変わらないので、PyInstaller --onefile も electron-builder の asar もやらないことにした。具体的には、package.jsonbuild セクションに "asar": false を追加するだけである。

6. Tkinter を使う

やっぱり Python と Electron を混ぜて作るのは難しい、俺は Python only でやりたいんだ、という場合は、Electron をすっぱりやめて Python 製 GUI を使うしかない。有名かつマルチプラットフォームなのは以下だろうか。

一番モダンそうなのは Kivy だが、とりあえず一番資料が多くて作れそうな Tkinter でやってみる。

が、ここにも落とし穴があった。Mac OS だと Button の色が変えられない。

tkmacosx を使うことで何とか回避した。

ということで、これも作っております。

作った感想:

  • Flask, PyInstaller, electrion, electron-builder と複雑に組み合わせ作っていたのが、非常にコンパクトになった感がある
  • 実際キビキビと動く
  • でも GUI 作るのホントめんどい。HTML + CSS ってなんて偉大な発明だったんだと気付く
  • どうしても作りきれない所がある
  • でも頑張って作ると、これはこれで良いんじゃないかと思えてくる
  • 複雑なことは出来ないので、簡単な GUI で良ければ充分選択肢になる
  • Mac OS だと PyInstaller に --windowed オプションを付けると、Wordle.app というアプリの形にしてくれる

まとめ

あ、Windows のことスッカリ忘れてたネ(作為的)

9
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
7