初投稿です。
PyCharmを使っていて相対インポートのエラーに悩まされていたのですが、一応解決したのでまとめておこうと思います。
※デフォルトのPyCharm設定を使用している想定です。
変更している場合結果が異なる可能性があります。
更新履歴
2022/03/06 : 初版
2022/03/08 : タグに「インポート」を追加
2022/03/11 : 一部"MyProject"を"myproject"へ変更
3.2.2 Run/Debug設定コメント追記
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
をインポートします。
ここに書かれたimport文は間違っているので注意
今後の話に絡むので、冒頭で__name__
と__package__
を出力します。
print(__name__, __package__)
from myproject import sub
from myproject.mypackage import func1
print(__name__, __package__)
print(__name__, __package__)
import func2
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. 解決!(?)
色々原因を調べて、解決策に辿り着きます。
「ふむふむ、パッケージ内では相対インポートを使う必要があるらしい」
「でも暗黙的な相対インポートというやつは禁止されているらしい」
つまり、次のように明示的な相対インポートを使用する必要があります。
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. 解決!(?)
怒られてしまったので、大人しく相対インポートをなくしました。
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
【参考】[Python] importの躓きどころ - Qiita
【参考】Pythonのパッケージとモジュールを理解してみる - Qiita
1.3. PyCharmでmain.pyを実行からのエラーで発生したエラーをもう一度見てみましょう。
print(__name__, __package__)
from myproject import sub
from myproject.mypackage import func1
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. 解決!(?)でfunc1.py
を指定して実行したときはエラーになりませんでした。
今度は1.5. PyCharmで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のスクリプト実行仕様についてで記述したように、この実行方法では明示的な相対インポートが使用できません。
3. 解決策
PyCharmが「ファイル名を指定して実行」しているため相対インポートが使用できないということまでわかりました。
方針としては、PyCharmのPythonスクリプト実行方法を「モジュール名を指定して実行」に変更してやればいいです。
方法はこちらの記事で解説されていますが、バージョンアップで少し変化しているので改めてまとめてみます。
【参考】相対importを使ったPythonファイルをPyCharmで実行する - Qiita
【参考】How to debug a Python package in PyCharm - stackoverflow
3.1 PyCharmのExternal Tools機能を利用する
Pythonスクリプトを「モジュール名を指定して実行」するコマンドを用意しておき、それを実行するという方法です。
※この方法ではPythonスクリプトの実行はできますが、デバッグはできません
(もしかしたらできるのかもしれませんが...)
3.1.1 External Tools追加
File/Settings
メニューを開き、Tools
タブのExternal Tools
を選択します。
右枠上部に+
ボタンがあるので、これをクリックします。
Create Tool
というウィンドウが開かれるので、次のように設定します。
-
Name
自由にわかりやすい名前を(例ではpython_m) -
Program
使用するpython.exeを指定します。
入力欄右の+
ボタンを押すとマクロの編集が可能で、
$ModuleSdkPath$
を選択すれば状況に応じて自動入力してくれます。 -
Arguments
Programに渡す引数を指定します。
ここでは次のように入力します。
-c "import runpy;runpy.run_module('{}.{}'.format('$FileDirRelativeToSourcepath$'.replace('\\', '.'), '$FileNameWithoutAllExtensions$'),{},__name__,1)"
# 続く文字列を実行させるオプション
-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機能を利用するで記述したコマンドを、外部ファイルに書き出してみます。
ファイルの保存フォルダはC:\work
など、今後変更しなくて済むようなフォルダにしておきましょう。
このファイルへの引数1で$FileDirRelativeToSourcepath$
を、引数2で$FileNameWithoutAllExtensions$
を受け取ることを想定しています。
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
を選択します。
次のようなウィンドウが表示されるので、左上の+
ボタンをクリックします。
Add New Configuration
というタブが表示されるのでPython
を選択します。
すると右側に入力欄が現れるので、次のように入力します。
-
Name
自由にわかりやすい名前を(例ではRun/Debug
) -
Script path
3.2.1 実行前処理用ファイル作成で用意したファイルへのフルパスを入力します。 -
Parameters
次のように入力します。
$FileDirRelativeToSourcepath$ $FileNameWithoutAllExtensions$
Soursepathは3.1.2 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)
も使用できます。
3.2.4 注意点
これで目的は全て達成できたのですが、1点注意があります。
それは、Run
やDebug
実行時に下部のRun/Debug
画面に
フォーカスしていてはいけないということです。
ここにフォーカスしていると、Run
やDebug
実行時に$FileDirRelativeToSourcepath$
や$FileNameWithoutAllExtensions$
が空文字になってしまい、エラーになります。
4. まとめ
PyCharmで相対インポートを含むファイルを実行するときは色々大変ですって話でした。
対策が見つかるまでに何日潰したことか......
どなたかの参考になれば幸いです。