LoginSignup
3
1

More than 3 years have passed since last update.

Python+Nimのハイブリッド環境(Nimporter)で、PyInstallerによる実行ファイル(EXE)を作成する

Last updated at Posted at 2021-01-05

ずっと気になっていたハイブリッド環境

こちらの記事にて、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と同じようにできます。

nimutils/calc.nim
import nimpy
import strformat

proc add(a: int, b: int): int {.exportpy.} =
    echo fmt("{ a + b = }")
    return a + b
nimporter_sample.py
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
nimutils/uuid.nim
import nimpy
import strformat
import nuuid      # import uuid library 

proc generate(): string {.exportpy.} =
    return generateUUID()
nimporter_sample.py
import nimporter
from nimutils import uuid

print(uuid.generate())

PythonからNimのマルチスレッドを実行してみる

こちらも問題なく動作しました。

nimutils/calc.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)
nimporter_sample.py
from nimutils import thread_test

# スレッド生成&実行しているメソッドを呼び出す
thread_test.threadTest()

ただし、PythonのスレッドからNimのメソッドは呼べないようです。(Issueはこちら)
そのため、現在のところNimporterを利用する場面においては、Pythonのメインスレッドからしか呼べないようです。
Webフレームワークでリクエストハンドラの中からNimのモジュールを呼ぶっていうことはできないみたいですね。

PyInstallerによるシングルExeファイルの作成

上記3パターンの呼び出しを行ったPythonファイルを、PyinstallerにてExe化し、別のWindows10環境でも動作することを確認します。

1発でExeが出来上がらないので、以下の手順で作成していきます。

  1. Pyinstallerを起動し、Specファイルを作成
  2. Pydファイル情報をSpecファイルに追加する
  3. PyinstallerをSpecファイルで起動し、Exeファイルを作成
  4. エラーが出たら足りないモジュールを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ファイルを以下のように修正します。

nimporter_sample.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に追加することでエラーが出なくなりました。

nimporter_sample.spec
  hiddenimports=['setuptools.py33compat','setuptools.py27compat'],

実行の様子

雑なキャプチャですが、ご参考まで。

pyinstaller.gif

まとめ

Nimporterを利用したPythonスクリプトをPyInstallerでExe化する手順を紹介しました。
ポイントとしては、Nimporterが生成したPydファイルをバイナリとして含めてあげることでしたね。

PythonのマルチスレッドからはNimporterで作成したモジュールは呼べない制限があるものの、Pythonの魅力的なライブラリを利用し、高速処理させたい部分をNimで作成するなど、ちょっとしたユーティリティをExeとして配布できるのは魅力的な環境ではないでしょうか。

とはいえ、本番で利用できる技術ではなく、ホビーユース(趣味的)な利用に限られるとは思いますが。

参考にしたサイト

3
1
2

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
3
1