PyCharmの相対インポートで躓いた話

初投稿です。
PyCharmを使っていて相対インポートのエラーに悩まされていたのですが、一応解決したのでまとめておこうと思います。

※デフォルトのPyCharm設定を使用している想定です。
　変更している場合結果が異なる可能性があります。


# 更新履歴
2022/03/06 : 初版
2022/03/08 : タグに「インポート」を追加
2022/03/11 : 一部"MyProject"を"myproject"へ変更
　　　　　　[3.2.2 Run/Debug設定](#322-rundebug設定)コメント追記
2022/03/13 : インポートの記述不備を修正


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


# 1. 状況の再現

## 1.1. プロジェクト構成

プロジェクト全体を格納するフォルダ(Root)

```:プロジェクト構成
myproject
```

その下にソースコードを格納する、プロジェクト名と同名のフォルダ

```:プロジェクト構成
myproject
└ myproject
```

メインとなるモジュール(`main.py`)と、その中でインポートするモジュール(`sub.py`)及びパッケージ(`mypackage`)
パッケージ中にファイル(`func1.py`, `func2.py`)を用意

```:プロジェクト構成
myproject
└ myproject
    ├ main.py
    ├ sub.py
    └ mypackage
        ├ func1.py
        └ func2.py
```

## 1.2. 各ファイルの内容

`main`で`sub`と`mypackage.func1`を、`mypackage.func1`で`mypackage.func2`をインポートします。

<font color=red>**ここに書かれたimport文は間違っているので注意**</font>

今後の話に絡むので、冒頭で`__name__`と`__package__`を出力します。

```Python:main.py
print(__name__, __package__)
from myproject import sub
from myproject.mypackage import func1
```

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

```Python:mypackage\\func1.py
print(__name__, __package__)
import func2
```

```Python:mypackage\\func2.py
print(__name__, __package__)
```

## 1.3. PyCharmでmain.pyを実行からのエラー

PyCharmで`main.py`を開き`Run(Alt+Shift+F10)`します。
すると`Edit Configurations...`みたいなのが書かれたウィンドウが出るので、`main`を選択して実行します。
結果は恐らく次のようになると思います。

```:実行結果
__main__ None
myproject.sub myproject
myproject.mypackage.func1 mypackage
Traceback (most recent call last):
    ......
ModuleNotFoundError: No module named 'func2'
```

`main`から`sub`, `mypackage.func1`のimportには成功していますが、`mypackage.func1`から`mypackage.func2`のimportに失敗しているようです。

## 1.4. 解決！（？）
色々原因を調べて、解決策に辿り着きます。

「ふむふむ、パッケージ内では**相対インポート**を使う必要があるらしい」
「でも**暗黙的な相対インポート**というやつは**禁止**されているらしい」

つまり、次のように明示的な相対インポートを使用する必要があります。

```Python:mypackage\\func1.py
print(__name__, __package__)
from . import func2
```

そしてもう一度`main.py`を実行すると、無事成功しました。
やったね！

```:実行結果
__main__ None
myproject.sub myproject
myproject.mypackage.func1 mypackage
myproject.mypackage.func2 mypackage
```

## 1.5. PyCharmでfunc1.pyを実行からのエラー

`mypackage.func1`の処理を変更する必要が出てきました。
色々作業している中で、少し動作確認をしたくなりました。
今度はPyCharmで`mypackage\func1.py`を開きRun(`Alt+Shift+F10`)します。
表示されたウィンドウで`func1`を選択すると......

```:実行結果
__main__ None
Traceback (most recent call last):
  ......
ImportError: attempted relative import with no known parent package
```

「相対インポートなんてしてんじゃねーよ！」って怒られます。

## 1.6. 解決！（？）

怒られてしまったので、大人しく相対インポートをなくしました。

```Python:mypackage\\func1.py
print(__name__, __package__)
import func2
```

もう一度実行すると、エラーは消えました。やったね！

```:実行結果
__main__ None
func2
```

## 1.7. 無限ループって怖くね？

処理の変更作業が終わったので、全体を通しでテストしましょう。
PyCharmで`main.py`を開き`Run(Alt+Shift+F10)`します。
表示されたウィンドウで`main`を選択して実行します。

```:実行結果
__main__ None
sub 
mypackage.func1 mypackage
Traceback (most recent call last):
    ......
ModuleNotFoundError: No module named 'func2'
```

以降1.3から1.6の繰り返し


# 2. 問題の原因

結論としてはPyCharmがPythonスクリプトを実行時`-m`オプションを付けていないということなのですが、少し掘り下げてみます。

## 2.1. Pythonのインポート仕様について

Pythonには絶対インポートと相対インポートがあり、それぞれに条件があります。

* 絶対インポート
環境変数のPYTHONPATHに登録されたフォルダ内に相手が存在する
* 明示的な相対インポート
自分が何らかのパッケージに属している
* 暗黙的な相対インポート
Python3では使用禁止

【参考】[Pythonのインポートを誤解して躓いた話 - Qiita](https://qiita.com/__Lily__/items/2e330cb21a438aa6fb0f)
【参考】[[Python] importの躓きどころ - Qiita](https://qiita.com/ysk24ok/items/2711295d83218c699276)
【参考】[Pythonのパッケージとモジュールを理解してみる - Qiita](https://qiita.com/ganyariya/items/5232ee1a3713f107d9fc)

[1.3. PyCharmでmain.pyを実行からのエラー](#13-pycharmでmainpyを実行からのエラー)で発生したエラーをもう一度見てみましょう。

```Python:main.py
print(__name__, __package__)
from myproject import sub
from myproject.mypackage import func1
```

```Python:mypackage\\func1.py
print(__name__, __package__)
import func2
```

```:実行結果
__main__ None
sub 
mypackage.func1 mypackage
Traceback (most recent call last):
    ......
ModuleNotFoundError: No module named 'func2'
```

`mypackage\func1.py`内で使用しているのは、`main.py`から見れば暗黙的な相対インポートです。
なので、これを実行したときにエラーとなりました。
しかし、`func1.py`本人から見れば絶対インポートです。
なので、[1.4. 解決！（？）](#14-解決)で`func1.py`を指定して実行したときはエラーになりませんでした。

今度は[1.5. PyCharmでfunc1.pyを実行からのエラー](#15-pycharmでfunc1pyを実行からのエラー)で発生したエラーを見てみます。

```Python:mypackage\\func1.py
print(__name__, __package__)
from . import func2
```

```:実行結果
Traceback (most recent call last):
  ......
ImportError: attempted relative import with no known parent package
__main__ None
```

`mypackage\func1.py`の1行目は実行できていて、`__package__`が`None`となっていることがわかります。
これは自分がどのパッケージにも属していないことを表し、明示的な相対インポートの条件を満たせずエラーとなったのです。

## 2.2 Pythonのスクリプト実行仕様について

Pythonでスクリプトを実行する方法は大きく2つあります。

* ファイル名を指定して実行

```:例
python myproject\mypackage\func1.py
```

このとき、`func1.py`はどのパッケージにも属さないものとして扱われます。
つまり、**この実行方法では明示的な相対インポートが使用できません**。

* モジュール名を指定して実行

```:例
python -m myproject.mypackage.func1
```

Pythonスクリプト実行時に`-m`オプションを使用します。
このとき、`func1.py`は`myproject.mypackage`パッケージに属するものとして扱われます。
つまり、**この実行方法では明示的な相対インポートが使用できます**。

## 2.3 PyCharmの仕様について

PyCharmで`Run(Alt+Shift+F10)`したときの画面を見てみましょう。
先頭行に恐らくこのような記述があります。

```:実行結果
C:\work\myproject\venv\Scripts\python.exe C:/work/myproject/myproject/mypackage/func1.py
```

少しごちゃっとしているので整理しましょう。
つまりは次のコマンドと同等です。
（カレントディレクトリは適切に調整済みとします）

```:実行結果
python mypackage/func1.py
```

Pythonの「**ファイル名を指定して実行**」コマンドです。
[2.2 Pythonのスクリプト実行仕様について](#22-Pythonのスクリプト実行仕様について)で記述したように、この実行方法では明示的な相対インポートが使用できません。


# 3. 解決策

PyCharmが「ファイル名を指定して実行」しているため相対インポートが使用できないということまでわかりました。
方針としては、PyCharmのPythonスクリプト実行方法を「モジュール名を指定して実行」に変更してやればいいです。

方法はこちらの記事で解説されていますが、バージョンアップで少し変化しているので改めてまとめてみます。
【参考】[相対importを使ったPythonファイルをPyCharmで実行する - Qiita](https://qiita.com/kzm4269/items/ce4736b8d621713e451f)
【参考】[How to debug a Python package in PyCharm - stackoverflow](https://stackoverflow.com/questions/37944296/how-to-debug-a-python-package-in-pycharm)

## 3.1 PyCharmのExternal Tools機能を利用する
Pythonスクリプトを「モジュール名を指定して実行」するコマンドを用意しておき、それを実行するという方法です。
**※この方法ではPythonスクリプトの実行はできますが、デバッグはできません**
（もしかしたらできるのかもしれませんが...）

### 3.1.1 External Tools追加

`File/Settings`メニューを開き、`Tools`タブの`External Tools`を選択します。
右枠上部に`+`ボタンがあるので、これをクリックします。

![external_tools.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2573710/5d3155c3-90bd-19d9-fc0b-ae658ddd34b5.png)

`Create Tool`というウィンドウが開かれるので、次のように設定します。

![create_tool.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2573710/47e6f731-5029-1622-8c25-f693daede928.png)

* Name
自由にわかりやすい名前を（例ではpython_m）

* Program
使用するpython.exeを指定します。
入力欄右の`+`ボタンを押すとマクロの編集が可能で、
`$ModuleSdkPath$`を選択すれば状況に応じて自動入力してくれます。

* Arguments
Programに渡す引数を指定します。
ここでは次のように入力します。    
`-c "import runpy;runpy.run_module('{}.{}'.format('$FileDirRelativeToSourcepath$'.replace('\\', '.'), '$FileNameWithoutAllExtensions$'),{},__name__,1)"`

```Python:コマンドを書き直したもの
# 続く文字列を実行させるオプション
-c

# runpyモジュールのインポート
import runpy

# Sourcepathから見た実行ファイル格納フォルダの相対パス
folder_path_relative = $FileDirRelativeToSourcepath$

# folder_path_relativeのパス区切り文字を'.'に置換
# OSによってパス区切り文字が異なるので注意！
# OS差異を気にしたくないならimport osしてos.sepを使用
package_name = folder_path_relative.replace('\\', '.')

# 実行ファイルのファイル名（拡張子無し）
file_name_without_ext = $FileNameWithoutAllExtensions$

# 実行モジュール名
module_name = '{}.{}'.format(package_name, module_name)

# 実行コマンド
runpy.run_module(module_name, {}, __name__, 1)
```

* Working Directory
`$Sourcepath$`を指定します。

### 3.1.2 Sourcepath設定

PyCharmの左側`Project`タブを開きます。
一番上の階層にあるProject Rootフォルダを右クリックし、一番下の`Mark Directory as`から`Sources Root`を選択します。
設定するのは`myproject\myprojeect`ではなく`myproject`です。

### 3.1.3 実行

**実行したいファイルを開いて**上部メニュー`tools/External Tools/python_m`を選択します。
（最後の名前は設定したNameが入ります）
実行ファイルに相対インポートが入っていても実行できるはずです。

## 3.2 実行前処理用ファイルを用意する

一応これで実行自体はできるようになったものの、デバッグができないという問題が残っています。
そのため、別の案を考えてみます。

### 3.2.1 実行前処理用ファイル作成

[3.1 PyCharmのExternal Tools機能を利用する](#31-pycharmのexternal-tools機能を利用する)で記述したコマンドを、外部ファイルに書き出してみます。
ファイルの保存フォルダは`C:\work`など、今後変更しなくて済むようなフォルダにしておきましょう。
このファイルへの引数1で`$FileDirRelativeToSourcepath$`を、引数2で`$FileNameWithoutAllExtensions$`を受け取ることを想定しています。

```Python:python_m.py
import runpy
import sys

# モジュール名生成
module_name = sys.argv[1].replace('\\', '.') + '.' + sys.argv[2]
runpy.run_module(module_name, {}, __name__, True)
```

### 3.2.2 Run/Debug設定

上部メニューから`Run/Edit Configurations`を選択します。
次のようなウィンドウが表示されるので、左上の`+`ボタンをクリックします。

![run_debug_configuration.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2573710/483212ac-c225-04e3-d380-7f12018d8d12.png)

`Add New Configuration`というタブが表示されるので`Python`を選択します。
すると右側に入力欄が現れるので、次のように入力します。

![config.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2573710/d4483b54-2f10-5c05-c339-1e72024720d3.png)

* Name
自由にわかりやすい名前を（例では`Run/Debug`）

* Script path
[3.2.1 実行前処理用ファイル作成](#321-実行前処理用ファイル作成)で用意したファイルへのフルパスを入力します。

* Parameters
次のように入力します。
`$FileDirRelativeToSourcepath$ $FileNameWithoutAllExtensions$`
Soursepathは[3.1.2 Sourcepath設定](#312-sourcepath設定)と同様に設定してください。

【2022/03/11追記】
`$FileDirRelativeToSourcepath$`の代わりに`$FileDirRelativeToProjectRoot$`を使用すればプロジェクトのRootフォルダから辿ってくれます。
こちらはSoursepathの設定が不要なので、状況に応じて使い分けるのが良さそうです。

### 3.2.3 実行

**実行したいファイルを開いて**`Run(Alt+Shift+F10)`します。
表示されたウィンドウで先程編集した設定（例では`Run/Debug`）を選択します。
実行ファイルに相対インポートが入っていても実行できるはずです。
今度は`Debug(Alt+Shift+F9)`します。
こちらも問題なく実行できるはずです。
右上部メニューにこの`Run/Debug`が登録されていれば、`Run(Shift+F10)`や`Debug(Shift+F9)`も使用できます。

![register.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2573710/739854ac-06b5-5344-7f38-387c29f410cd.png)

### 3.2.4 注意点

これで目的は全て達成できたのですが、1点注意があります。
それは、`Run`や`Debug`実行時に下部の`Run/Debug`画面に
フォーカスしていてはいけないということです。

![run_debug.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2573710/98c77f5c-42a8-e7b8-009a-7748ebf08a8d.png)

ここにフォーカスしていると、`Run`や`Debug`実行時に`$FileDirRelativeToSourcepath$`や`$FileNameWithoutAllExtensions$`が空文字になってしまい、エラーになります。


# 4. まとめ
PyCharmで相対インポートを含むファイルを実行するときは色々大変ですって話でした。
対策が見つかるまでに何日潰したことか......
どなたかの参考になれば幸いです。