こいついつも躓いてんな
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
のエラーにはいろいろな原因があります。
他にも何かあれば追加していきたいと思います。