こいついつも躓いてんな
PyInstallerでPythonスクリプトをexe化した際エラーが出ててんやわんやしていたのですが、ようやく解決できたのでまとめておきます。
また、仮想環境生成のためにPyCharmを使用しています。
更新履歴
2022/03/13 : 初版
環境
- WIndows10
- Python3.10
- PyCharm Community Edition 2021.2.2
1. exe化の際指定したファイル内でImportError
こういうエラー。
exe化の際指定したのがmyprojectパッケージ内のmain.pyファイルで、その内部で「相対インポート使ってんじゃねーよ!」と怒られています。
Traceback (most recent call last):
File "myproject\main.py", line 4, in <module>
ImportError: attempted relative import with no known parent package
1.1. 状況の再現
1.1.1. プロジェクト構成
プロジェクト全体を格納するフォルダ(Root)
myproject
その下にソースコードを格納する、プロジェクト名と同名のフォルダ
myproject
└ myproject
パッケージ化のため__init__.pyを追加します。
メインとなるモジュール(main.py)と、その中でインポートするモジュール(sub.py)を用意します。
myproject
└ myproject
├ __init__.py
├ main.py
└ sub.py
1.1.2. 各ファイルの内容
mainでsubをインポートします。
パッケージ内なので相対インポート!ということで、from . import subと書いてみます。
参考の為、__name__と__package__を出力しておきます。
コマンドプロンプトが一瞬で消えるのを回避するため、subprocessでPAUSEします。
print(__name__, __package__)
import subprocess
subprocess.Popen('PAUSE', shell=True)
from . import sub
print(__name__, __package__)
__init__.pyは空ファイルです。
1.1.3. exe化実行
PyCharmのTerminalから次のコマンドでexe化を実行します。
pyinstaller --onefile main.py
exe化......できてない!
55 INFO: PyInstaller: 4.10
55 INFO: Python: 3.10.0
60 INFO: Platform: Windows-10-10.0.19043-SP0
61 INFO: wrote C:\work\myproject\main.spec
63 INFO: UPX is not available.
script 'C:\work\myproject\main.py' not found
これの原因はすぐにわかりました。
カレントディレクトリがプロジェクトのRootフォルダになっていました。
ファイル名をmain.pyとしか書かなかったので、カレントディレクトリ内のmain.pyファイルを探してしまったんですね。
ということで、ソースコードのフォルダへ移動します。
# これでカレントディレクトリがmyproject\myprojectになる
cd myproject
もう一度exe化を実行します。
pyinstaller --onefile main.py
今度は成功したようです。
......
5945 INFO: Updating resource type 24 name 1 language 0
5946 INFO: Appending PKG archive to EXE
7455 INFO: Building EXE from EXE-00.toc completed successfully.
1.1.4. exeファイルを実行するとImportError
myproject\myproject\dist\main.exeを実行するとImportErrorになりました。
__main__ None
Traceback (most recent call last):
......
ImportError: attempted relative import with no known parent package
......
1.2. 原因
PyInstallerの仕様として、exe化を指示したファイルはパッケージに属さないものとして扱われます。
そのため、パッケージに属さないmain.py内で相対インポートを使おうにも、どこからの相対なのかという基準がわからずエラーとなったのです。
「setup.pyでentry_points指定すればパッケージ構造も実行ファイルも情報渡せるから何とかならん?」とか思ったりもしたのですが、そういう訳にもいかない様子。
【参考】Pyinstall a package with __main__.py - GitHub
「数行修正すればいいだけのことにそんな労力かけてられないよ!」みたいなことが書かれています。
1.3. 対策
exe化を指示するファイル内で相対インポートは使用しないことが大切です。
print(__name__, __package__)
import subprocess
subprocess.Popen('PAUSE', shell=True)
from . import sub
from . import subで相対インポートを使用していますね。
これを絶対インポートに書き直せば良いのですが、次のような書き方をしないようにしましょう。
import sub
これでexe化すると「subなんてモジュールはねえよ!(ModuleNotFoundError)」って怒られます。
なぜなら、これは暗黙的な相対インポートであって絶対インポートではないからです。
これは次節で扱います。
【参考】Pythonのインポートを誤解して躓いた話 - Qiita
自作モジュールに関しては、必ずパッケージ名を指定します。
from myproject import sub
なお、exe化を指示するファイル以外では相対インポートの使用はOKです。
1.4 まとめ
- exe化の際指定するファイル内で相対インポートは使用しない
-
from myproject import subのようにパッケージ名を指定した絶対インポートを使用する
2. 自作モジュールに対してModuleNotFoundError
こういうエラー。
exe化で指定したmain.pyと同じフォルダにsub.pyがあるはずだけど、「mainなんてモジュールはねーよ!」と怒られています。
Traceback (most recent call last):
File "myproject\main.py", line 3, in <module>
ModuleNotFoundError: No module named 'sub'
2.1. 状況の再現
2.1.1. プロジェクト構成
プロジェクト全体を格納するフォルダ(Root)
myproject
その下にソースコードを格納する、プロジェクト名と同名のフォルダ
myproject
└ myproject
パッケージ化のため__init__.pyを追加します。
メインとなるモジュール(main.py)と、その中でインポートするモジュール(sub.py)を用意します。
myproject
└ myproject
├ __init__.py
├ main.py
└ sub.py
2.1.2. 各ファイルの内容
mainでsubをインポートします。
このとき「mainを実行するんだからmyproject\myprojectにPython検索パスが通るはずで、だったらこう書けるだろう」と考え、import subと書きました。
print(__name__, __package__)
import subprocess
subprocess.Popen('PAUSE', shell=True)
import sub
print(__name__, __package__)
__init__.pyは空ファイルです。
2.1.3. exe化実行
PyCharmのTerminalから次のコマンドでexe化を実行します。
cd myproject
pyinstaller --onefile main.py
2.1.4. exeファイルを実行するとModuleNotFoundError
myproject\myproject\dist\main.exeを実行するとModuleNotFoundErrorになりました。
__main__ None
Traceback (most recent call last):
File "myproject\main.py", line 4, in <module>
ModuleNotFoundError: No module named 'sub'
2.2. 原因
pyinstallerを介した時点でmainは実行ファイルではありません(=Python検索パスは通りません)。
なので、import subという書き方は絶対インポートではなく暗黙的な相対インポートという扱いになります。
そしてPython3では暗黙的な相対インポートは禁止されてますから、エラーとなります。
2.3. 対策
絶対インポートまたは明示的な相対インポートを使用します。
ただし、exe化を指定するファイルに関しては前述の通り、絶対インポートを使用してください。
print(__name__, __package__)
import subprocess
subprocess.Popen('PAUSE', shell=True)
from myproject import sub
2.4. まとめ
- 暗黙的な相対インポートになっていないか確認
- exe化を指定するファイルでは絶対インポート
- それ以外では絶対インポートか明示的な相対インポート
3. 仮想環境上のパッケージに対してModuleNotFoundError
こういうエラー。
仮想環境上でnumpyをインストールしたけど、「numpyなんてモジュールはねーよ!」と怒られています。
Traceback (most recent call last):
File "myproject\__main__.py", line 3, in <module>
ModuleNotFoundError: No module named 'numpy'
3.1. 状況の再現
3.1.1. プロジェクト構成
プロジェクト全体を格納するフォルダ(Root)
myproject
その下にソースコードを格納する、プロジェクト名と同名のフォルダ
myproject
└ myproject
パッケージ化のため__init__.pyを追加します。
メインとなるモジュール(main.py)を用意します。
myproject
└ myproject
├ __init__.py
└ main.py
3.1.2. 各ファイルの内容
main.pyで仮想環境上にしかインストールしていないパッケージをインポートします。
今回はnumpyで試してみます。
print(__name__, __package__)
import numpy
3.1.3. exe化実行
コマンドプロンプトから次のコマンドでexe化を実行します。
cd myproject
pyinstaller --onefile main.py
3.1.4. exeファイルを実行するとModuleNotFoundError
myproject\myproject\dist\main.exeを実行するとModuleNotFoundErrorになりました。
__main__ None
Traceback (most recent call last):
File "myproject\main.py", line 4, in <module>
ModuleNotFoundError: No module named 'numpy'
3.2. 原因
仮想環境上のプロジェクトに対してグローバルのpyinstallerを実行したためです。
このとき仮想環境中のパッケージはグローバルのPython検索パス上に存在しないため、読み込まれないのです。
なお、今回pyinstallerをコマンドプロンプトから実行しましたが、同様の問題はPyCharmのTerminalから実行したときでも発生する可能性があります。
それは、「PyCharmのTerminalで仮想環境が起動していないとき」です。
詳しくはこちらをご覧ください。
【参考】PyCharmのTerminalで仮想環境が自動起動せずに躓いた話 - Qiita
3.3. 対策
方法は3つありますので、順に紹介します。
ただその前に注意すべきなのが、pyinstallerとPyInstallerは別物だということです。
-
pyinstaller
Scripts\pyinstaller.exeを示しています。
exeファイルです。
なので、python -m pyinstaller --onefile main.pyとか書くと当然エラーです。 -
PyInstaller
Lib\site-packages\PyInstallerを示しています。
Pythonパッケージです。
なので、PyInstaller --onefile main.pyとか書くと当然エラー......と思いきや、PowerShellやCMDではコマンド名の大文字小文字が区別されないので実行できてしまいます。
その場合、実行されるのはpyinstaller.exeの方です。
python -m pyinstallerのpyinstallerはコマンド名ではないので、こちらはエラーです。
3.3.1. 仮想環境上のpyinstaller.exeを使用
グローバルにpyinstallerをインストールしている場合、予めアンインストールしておいてください。
PyCharmのTerminalを開きます。
このとき仮想環境が起動していることを確認してください。
次のコマンドでpyinstallerをインストールします。
python -m pip install pyinstaller
インストールが終わったら、続けて次のコマンドを試してみてください。
Get-Command pyinstaller
where pyinstaller
仮想環境中のpyinstaller.exeを示していれば成功です。
C:\work\myproject\venv\Scripts\pyinstaller.exe
exe化する際は、仮想環境起動中のPyCharmのTerminalで次のようにして実行します。
cd myproject
pyinstaller --onefile main.py
3.3.2. 仮想環境上のPyInstallerパッケージを使用
グローバルにpyinstallerをインストールしている場合、予めアンインストールしておいてください。
PyCharmのTerminalを開きます。
このとき仮想環境が起動していることを確認してください。
次のコマンドでpyinstallerをインストールします。
python -m pip install pyinstaller
インストールが終わったら、続けて次のコマンドを試してみてください。
python -c "import PyInstaller; print(PyInstaller.__file__)
仮想環境中のPyInstallerパッケージを示していれば成功です。
C:\work\myproject\venv\lib\siteーpackages\PyInstaller\__init__.py
exe化する際は、仮想環境起動中のPyCharmのTerminalで次のようにして実行します。
cd myproject
python -m PyInstaller --onefile main.py
3.3.3. グローバルのPyInstallerパッケージを使用
この方法を使用する場合、PyCharmのPython Interpreter生成時Inherit global site-packagesにチェックを入れている必要があります。
コマンドプロンプトを開き、グローバルにpyinstallerをインストールします。
python -m pip install pyinstaller
exe化する際は、仮想環境起動中のPyCharmのTerminalで次のようにして実行します。
cd myproject
python -m PyInstaller --onefile main.py
仮想環境上にはpyinstallerをインストールしていませんが、グローバルにインストールされていれば使用可能です。
このとき実行しているpython.exeが仮想環境上のものになることがポイントで、この場合仮想環境上のsite-packagesもPython検索パスに追加されます。
3.3.4. 補足
pyinstaller.exeとpython.exe&PyInstallerのパターンは上記以外にもありますが、全てダメです。
pyinstaller.exeまたはpython.exeがグローバルのものである場合、仮想環境上のsite-packagesがPython検索パスに追加されず、exe実行時にエラーとなります。
3.4. まとめ
うまくいくのは次のパターン
- 仮想環境上の
pyinstaller.exeを使用する - 仮想環境上の
python.exeとPyInstallerパッケージを使用する - Python Interpreter生成時に
Inherit global site-packagesにチェックを入れた上で、仮想環境上のpython.exeとグローバルのPyInstallerパッケージを使用する
4. exe化の処理中にIndexError
おまけ
4.1. 原因
Python3.10のバグのようです。
【参考】【Python3.10】pyinstallerでindex out of rangeが出る
4.2. 対策
Lib\dis.py内の_unpack_opargs関数を探します。
次のような記述があるので、extended_arg = 0を追加します。
else:
arg = None
extended_arg = 0
yield (i, op, arg)
5. まとめ
pyinstallerのエラーにはいろいろな原因があります。
他にも何かあれば追加していきたいと思います。