4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-03-06

初投稿です。
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. 各ファイルの内容

mainsubmypackage.func1を、mypackage.func1mypackage.func2をインポートします。

ここに書かれたimport文は間違っているので注意

今後の話に絡むので、冒頭で__name____package__を出力します。

main.py
print(__name__, __package__)
from myproject import sub
from myproject.mypackage import func1
sub.py
print(__name__, __package__)
mypackage\func1.py
print(__name__, __package__)
import func2
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. 解決!(?)

色々原因を調べて、解決策に辿り着きます。

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

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

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. 解決!(?)

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

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
【参考】[Python] importの躓きどころ - Qiita
【参考】Pythonのパッケージとモジュールを理解してみる - Qiita

1.3. PyCharmでmain.pyを実行からのエラーで発生したエラーをもう一度見てみましょう。

main.py
print(__name__, __package__)
from myproject import sub
from myproject.mypackage import func1
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. 解決!(?)func1.pyを指定して実行したときはエラーになりませんでした。

今度は1.5. PyCharmでfunc1.pyを実行からのエラーで発生したエラーを見てみます。

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.pymyproject.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を選択します。
右枠上部に+ボタンがあるので、これをクリックします。

external_tools.PNG

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

create_tool.PNG

  • 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$を受け取ることを想定しています。

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

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

config.PNG

  • 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)も使用できます。

register.PNG

3.2.4 注意点

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

run_debug.PNG

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

4. まとめ

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

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?