Python+Nimのハイブリッド環境(Nimporter)で、PyInstallerによる実行ファイル(EXE)を作成する
ずっと気になっていたハイブリッド環境
こちらの記事にて、nimporterなるクールなライブラリがあると知り、ほんわかなコメントをしたものの、ずっと気になっていたので検証してみました。
サンプルソースはこちら。
PyInstallerの記事もQiita内にたくさんありますので、そちらも参照いただければと。
環境
PyInstallerとpyenvやらvirtualenv系とは相性が悪いらしいので、Pythonは仮想環境なしで動作させています。
- Windows 10
- Nim 1.4.2
- Python 3.9
- PyInstaller 4.1
- Nimporter 1.0.2
- Visual Studio Community 2019
開発時のトラブルなど
- NimporterがWindows環境だとVCCを使う設定になっているようなので、VisualStudioのCommunity Editionを入れました。
- PyInstallerが起動しないため、PythonインストールフォルダのScriptsフォルダにもパスを通したりしていました。
まずはNimporterで遊ぶ
今回のサンプルのディレクトリ構成はこちら。
ファイル・フォルダ | 説明 |
---|---|
└─ nimporter-sample | プロジェクトディレクトリ |
├─nimutils | nimソース用パッケージ |
│ ├─__init__.py
|
お約束ファイル |
│ ├─ calc.nim | 計算サンプル |
│ ├─ thread_test.nim | スレッドサンプル |
│ └─ uuid.nim | 別ライブラリ呼び出しサンプル |
├─ nimporter_sample.py | メインのPythonスクリプト |
└─ nimporter_sample.spec.sample | PyInstaller用Specファイルのサンプル |
Pythonからパッケージ内のNimモジュールをインポートする
Pythonファイルと同じディレクトリになくても、パッケージ(フォルダ)内にあるNimのメソッドへのアクセスも、通常のPythonと同じようにできます。
import nimpy
import strformat
proc add(a: int, b: int): int {.exportpy.} =
echo fmt("{ a + b = }")
return a + b
import nimporter
from nimutils import calc # nimutils/calc.nimをインポート
# call nim method
print(calc.add(2, 4)) # 6
Nimbleでインストールしたモジュールを利用してみる
あらかじめnimbleコマンドでインストールしたnuuid
というモジュールを、nimソースでインポートし、実行結果をPythonに返すということもできます。
# モジュールインストール
$ nimble install nuuid
import nimpy
import strformat
import nuuid # import uuid library
proc generate(): string {.exportpy.} =
return generateUUID()
import nimporter
from nimutils import uuid
print(uuid.generate())
PythonからNimのマルチスレッドを実行してみる
こちらも問題なく動作しました。
import nimpy
import strformat
import os
proc threadFunc(param: tuple[a, b: int]) {.thread.} =
echo fmt("This is Thread-{param.a}")
proc threadTest(): int {.exportpy.} =
var thr: array[0..1, Thread[tuple[a, b: int]]]
echo "start threads"
defer:
echo "wait threads"
thr[0].createThread(threadFunc, (1, 1000))
thr[1].createThread(threadFunc, (2, 1000))
sleep(1000)
joinThreads(thr)
from nimutils import thread_test
# スレッド生成&実行しているメソッドを呼び出す
thread_test.threadTest()
ただし、PythonのスレッドからNimのメソッドは呼べないようです。(Issueはこちら)
そのため、現在のところNimporterを利用する場面においては、Pythonのメインスレッドからしか呼べないようです。
Webフレームワークでリクエストハンドラの中からNimのモジュールを呼ぶっていうことはできないみたいですね。
PyInstallerによるシングルExeファイルの作成
上記3パターンの呼び出しを行ったPythonファイルを、PyinstallerにてExe化し、別のWindows10環境でも動作することを確認します。
1発でExeが出来上がらないので、以下の手順で作成していきます。
- Pyinstallerを起動し、Specファイルを作成
- Pydファイル情報をSpecファイルに追加する
- PyinstallerをSpecファイルで起動し、Exeファイルを作成
- エラーが出たら足りないモジュールをSpecファイルに追加
以下、Exe起動時にエラーが出なくなるまで、3,4を繰り返します。
Specファイルの作成
シンプルな構成のPythonスクリプトであれば、Pyinstallerで1発でEXEファイルができるかもしれませんが、Pyinstallerを起動して生成されるSpecファイルを適切に修正して、Exeを作る環境を整えていきます。
まずは、エントリポイントとなるPythonスクリプトを指定して、PyInstallerを実行すると、スクリプトと同じフォルダにspecファイルが生成され、distフォルダにもExeファイルが出来上がります。
ただし、出来上がったExeファイルを起動してもエラーが出て終了してしまうため、生成されたSpecファイルに足りないモジュールなどを記述していきます。
$ pyinstaller nimporter_sample.py --onefile
・・・
$ dir dist
2021/01/04 19:36 <DIR> .
2021/01/04 19:36 <DIR> ..
2021/01/04 19:36 7,725,805 nimporter_sample.exe
1 File(s) 7,725,805 bytes
2 Dir(s) 232,853,856,256 bytes free
$ dist\nimporter_sample.exe
# 起動するとエラーが発生してしまう
Traceback (most recent call last):
File "nimporter_sample.py", line 1, in <module>
import nimporter
File "c:\apps\python\python39\lib\site-packages\PyInstaller\loader\pyimod03_importers.py", line 493, in exec_module
exec(bytecode, module.__dict__)
File "nimporter.py", line 13, in <module>
File "c:\apps\python\python39\lib\site-packages\PyInstaller\loader\pyimod03_importers.py", line 493, in exec_module
exec(bytecode, module.__dict__)
File "setuptools\__init__.py", line 24, in <module>
File "c:\apps\python\python39\lib\site-packages\PyInstaller\loader\pyimod03_importers.py", line 493, in exec_module
exec(bytecode, module.__dict__)
File "setuptools\depends.py", line 6, in <module>
ModuleNotFoundError: No module named 'setuptools.py33compat'
[431764] Failed to execute script nimporter_sample
PydファイルをSpecのbinariesに追加する
Nimporterが生成するのは、Python拡張モジュール(拡張子がpyd)なので、これをPyinstallerがExeモジュールに含めるように修正します。
Specファイルは、実際はPythonスクリプトであるため、Pythonのコードを記述して設定ファイルを修正することができます。
今回のサンプルでは、nimutils
フォルダ内にNimファイルを配置したので、nimutils\__pycache__
フォルダにpyd
が出力されたので、Specファイルを以下のように修正します。
# -*- mode: python ; coding: utf-8 -*-
import os
curDir = os.getcwd()
cacheDir = os.path.join(curDir, 'nimutils', '__pycache__')
pydDir = os.path.join('.', 'nimutils')
block_cipher = None
a = Analysis(['nimporter_sample.py'],
pathex=[curDir],
# nimporter が生成したpydファイルをバイナリとして追加する
binaries=[
(os.path.join(cacheDir, 'calc.pyd'), pydDir),
(os.path.join(cacheDir, 'thread_test.pyd'), pydDir),
(os.path.join(cacheDir, 'uuid.pyd'), pydDir),
],
バイナリファイルの設定は複数指定でき、ファイル毎にタプルで指定(Pydファイルの場所, EXE起動時にどこに配置するか)
します。
今回の例だと、nimutils/__pycache__/
に入っているpydファイルを、Exe起動時にはnimutilsフォルダに展開せよという指定です。
展開後も__pycache__
フォルダなのでは?と思ったのですが、Exe実行時は__pycache__
フォルダにpyd
があったとしてもそちらからは読み込まないようです。
PyinstallerをSpecファイルで起動し、Exeファイルを作成
PyInstaller の引数に、Specファイルを指定して実行します。
# Specファイルを指定して実行
$ pyinstaller nimporter_sample.spec
# distにできたExeを起動する
$ dist\nimporter_sample.exe
エラーが出たら足りないモジュールをSpecファイルに追加
PyInstallerで作成したExeを起動した際に、以下のようなエラーが出た場合、PyInstallerのSpecファイルのhiddenimports
に、モジュール名を追加することで、エラーが解消されます。
ModuleNotFoundError: No module named 'setuptools.py33compat'
何度か繰り返し、2つのモジュールをhiddenimportsに追加することでエラーが出なくなりました。
hiddenimports=['setuptools.py33compat','setuptools.py27compat'],
実行の様子
雑なキャプチャですが、ご参考まで。
まとめ
Nimporterを利用したPythonスクリプトをPyInstallerでExe化する手順を紹介しました。
ポイントとしては、Nimporterが生成したPydファイルをバイナリとして含めてあげることでしたね。
PythonのマルチスレッドからはNimporterで作成したモジュールは呼べない制限があるものの、Pythonの魅力的なライブラリを利用し、高速処理させたい部分をNimで作成するなど、ちょっとしたユーティリティをExeとして配布できるのは魅力的な環境ではないでしょうか。
とはいえ、本番で利用できる技術ではなく、ホビーユース(趣味的)な利用に限られるとは思いますが。
参考にしたサイト
Author And Source
この問題について(Python+Nimのハイブリッド環境(Nimporter)で、PyInstallerによる実行ファイル(EXE)を作成する), 我々は、より多くの情報をここで見つけました https://qiita.com/6in/items/c58e4153fc6046743507著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .