PyInstallerのImportErrorとModuleNotFoundErrorで躓いた話

~~こいついつも躓いてんな~~
PyInstallerでPythonスクリプトをexe化した際エラーが出ててんやわんやしていたのですが、ようやく解決できたのでまとめておきます。

また、仮想環境生成のためにPyCharmを使用しています。


# 更新履歴
2022/03/13 : 初版


# 環境
* WIndows10
* Python3.10
* PyCharm Community Edition 2021.2.2


# 1. exe化の際指定したファイル内でImportError

こういうエラー。
exe化の際指定したのが`myproject`パッケージ内の`main.py`ファイルで、その内部で「相対インポート使ってんじゃねーよ！」と怒られています。

```powershell:ImportError
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`します。

```Python:main.py
print(__name__, __package__)

import subprocess
subprocess.Popen('PAUSE', shell=True)

from . import sub
```

```Python:sub.py
print(__name__, __package__)
```

`__init__.py`は空ファイルです。

### 1.1.3. exe化実行

PyCharmの`Terminal`から次のコマンドでexe化を実行します。

```powershell:exe化実行
pyinstaller --onefile main.py
```

exe化......<font color="red">**できてない！**</font>

```powershell:実行結果
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
```

これの原因はすぐにわかりました。
<font color="red">**カレントディレクトリがプロジェクトのRootフォルダ**</font>になっていました。
ファイル名を`main.py`としか書かなかったので、カレントディレクトリ内の`main.py`ファイルを探してしまったんですね。
ということで、ソースコードのフォルダへ移動します。

```powershell:フォルダ移動
# これでカレントディレクトリがmyproject\myprojectになる
cd myproject
```

もう一度exe化を実行します。

```powershell:exe化実行
pyinstaller --onefile main.py
```

今度は成功したようです。

```powershell:実行結果
......
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`になりました。

```powershell:実行結果
__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](https://github.com/pyinstaller/pyinstaller/issues/2560)
「数行修正すればいいだけのことにそんな労力かけてられないよ！」みたいなことが書かれています。

## 1.3. 対策

**exe化を指示するファイル内で相対インポートは使用しない**ことが大切です。

```Python:main.py
print(__name__, __package__)

import subprocess
subprocess.Popen('PAUSE', shell=True)

from . import sub
```

`from . import sub`で相対インポートを使用していますね。
これを絶対インポートに書き直せば良いのですが、<font color="red">**次のような書き方をしないようにしましょう。**</font>

```Python:間違った書き方
import sub
```

これでexe化すると「`sub`なんてモジュールはねえよ！(`ModuleNotFoundError`)」って怒られます。
なぜなら、これは<font color="red">**暗黙的な相対インポートであって絶対インポートではないから**</font>です。
これは[次節](#2-自作モジュールに対してmodulenotfounderror)で扱います。
【参考】[Pythonのインポートを誤解して躓いた話 - Qiita](https://qiita.com/__Lily__/items/2e330cb21a438aa6fb0f)

自作モジュールに関しては、必ずパッケージ名を指定します。

```Python:正しい書き方
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`と書きました。

```Python:main.py
print(__name__, __package__)

import subprocess
subprocess.Popen('PAUSE', shell=True)

import sub
```

```Python:sub.py
print(__name__, __package__)
```

`__init__.py`は空ファイルです。

### 2.1.3. exe化実行

PyCharmの`Terminal`から次のコマンドでexe化を実行します。

```powershell:exe化実行
cd myproject
pyinstaller --onefile main.py
```

### 2.1.4. exeファイルを実行するとModuleNotFoundError
`myproject\myproject\dist\main.exe`を実行すると`ModuleNotFoundError`になりました。

```powershell:実行結果
__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化を指定するファイルに関しては[前述](#1-exe化の際指定したファイル内でimporterror)の通り、絶対インポートを使用してください。

```Python:main.py
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`で試してみます。

```Python:main.py
print(__name__, __package__)
import numpy
```

### 3.1.3. exe化実行
**コマンドプロンプトから**次のコマンドで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](https://qiita.com/__Lily__/items/71c75566f067c19c18aa)

## 3.3. 対策

方法は3つありますので、順に紹介します。
ただその前に注意すべきなのが、`pyinstaller`と`PyInstaller`は<font color="red">**別物**</font>だということです。

* `pyinstaller`
`Scripts\pyinstaller.exe`を示しています。
**exeファイルです。**
なので、`python -m pyinstaller --onefile main.py`とか書くと<font color="red">**当然エラー**</font>です。

* `PyInstaller`
`Lib\site-packages\PyInstaller`を示しています。
**Pythonパッケージです。**
なので、`PyInstaller --onefile main.py`とか書くと当然エラー......と思いきや、<font color="red">**PowerShellやCMDではコマンド名の大文字小文字が区別されない**</font>ので実行できてしまいます。
その場合、実行されるのは`pyinstaller.exe`の方です。
`python -m pyinstaller`の`pyinstaller`はコマンド名ではないので、こちらはエラーです。

### 3.3.1. 仮想環境上のpyinstaller.exeを使用

グローバルに`pyinstaller`をインストールしている場合、予めアンインストールしておいてください。

PyCharmのTerminalを開きます。
このとき仮想環境が起動していることを確認してください。
次のコマンドで`pyinstaller`をインストールします。

```powershell:pyinstallerインストール
python -m pip install pyinstaller
```

インストールが終わったら、続けて次のコマンドを試してみてください。

```powershell:pyinstaller.exeの場所（TerminalがPowerShellの場合）
Get-Command pyinstaller
```

```:pyinstaller.exeの場所（TerminalがCMDの場合）
where pyinstaller
```

仮想環境中の`pyinstaller.exe`を示していれば成功です。

```:実行結果
C:\work\myproject\venv\Scripts\pyinstaller.exe
```

exe化する際は、<font color="red">**仮想環境起動中のPyCharmのTerminalで**</font>次のようにして実行します。

```powershell:pyinstaller実行
cd myproject
pyinstaller --onefile main.py
```

### 3.3.2. 仮想環境上のPyInstallerパッケージを使用

グローバルに`pyinstaller`をインストールしている場合、予めアンインストールしておいてください。

PyCharmのTerminalを開きます。
このとき仮想環境が起動していることを確認してください。
次のコマンドで`pyinstaller`をインストールします。

```powershell:pyinstallerインストール
python -m pip install pyinstaller
```

インストールが終わったら、続けて次のコマンドを試してみてください。

```powershell:PyInstallerの場所
 python -c "import PyInstaller; print(PyInstaller.__file__)
```

仮想環境中の`PyInstaller`パッケージを示していれば成功です。

```:実行結果
C:\work\myproject\venv\lib\siteｰpackages\PyInstaller\__init__.py
```

exe化する際は、<font color="red">**仮想環境起動中のPyCharmのTerminalで**</font>次のようにして実行します。

```powershell:PyInstaller実行
cd myproject
python -m PyInstaller --onefile main.py
```

### 3.3.3. グローバルのPyInstallerパッケージを使用

この方法を使用する場合、PyCharmのPython Interpreter生成時`Inherit global site-packages`にチェックを入れている必要があります。

コマンドプロンプトを開き、グローバルに`pyinstaller`をインストールします。

```:pyinstallerインストール
python -m pip install pyinstaller
```

exe化する際は、<font color="red">**仮想環境起動中のPyCharmのTerminalで**</font>次のようにして実行します。

```powershell:PyInstaller実行
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が出る](https://nekoy3.net/2022/03/06/pyinstaller-bug-fix/)

## 4.2. 対策

`Lib\dis.py`内の`_unpack_opargs`関数を探します。
次のような記述があるので、`extended_arg = 0`を追加します。

```python:Lib\\dis.py\\_unpack_opargs
else:
    arg = None
    extended_arg = 0 
yield (i, op, arg)
```

【参考】[IndexError: tuple index out of range when I try to create an executable from a python script using auto-py-to-exe - stackoverflow](https://stackoverflow.com/questions/69776492/indexerror-tuple-index-out-of-range-when-i-try-to-create-an-executable-from-a-p)


# 5. まとめ

`pyinstaller`のエラーにはいろいろな原因があります。
他にも何かあれば追加していきたいと思います。