cannot import name 'xxx' from partially initialized moduleの発生条件


# 概要

パッケージ内でファイルをimportする実験のため、  
下記の構成にて`m.py`を実行すると、エラー `cannot import name 'xxx' from partially initialized
module`が発生しました。

しかし`__init__.py`の行の順番を入れ替えるとエラーが消滅し、正常に動作します。

## ファイル内容

### __init__.py

[code]

    from tsmod.m import ModuleM
    from tsmod.p import ModuleP
    
[/code]

### m.py

[code]

    from tsmod import ModuleP
    
    
    class ModuleM:
        pass
    
[/code]

### p.py

[code]

    class ModuleP:
        pass
    
[/code]

## ディレクトリ構成

[code]

    tsmod
     L __init__.py
     L m.py
     L p.py
    
[/code]

# 実行条件

## 実行環境

OS: Ubuntu 20.04.2 LTS  
Pythonバージョン: 3.8.5

PyCharm2021.1.2にて実行  
[![画像の説明をここに入力](https://i.stack.imgur.com/SSBDl.png)](https://i.stack.imgur.com/SSBDl.png)

## コマンド

[code]

    /usr/bin/python3.8 /home/USER/XXX/XXXX/tsmod/m.py
    
[/code]

## エラー内容

[code]

    Traceback (most recent call last):
      File "/home/USER/XXX/XXXX/tsmod/m.py", line 1, in <module>
        from tsmod import ModuleP
      File "/home/USER/XXX/XXXX/tsmod/__init__.py", line 1, in <module>
        from tsmod.m import ModuleM
      File "/home/USER/XXX/XXXX/tsmod/m.py", line 1, in <module>
        from tsmod import ModuleP
    ImportError: cannot import name 'ModuleP' from partially initialized module 'tsmod' (most likely due to a circular import) (/home/USER/XXX/XXXX/tsmod/__init__.py)
    
    Process finished with exit code 1
    
[/code]

## __init__.py (正常動作版)

１行目と２行目を入れ替え。

[code]

    from tsmod.p import ModuleP
    from tsmod.m import ModuleM
    
[/code]

# 疑問

  1. なぜエラーが発生しているのか？
  2. なぜ`__init__.py`の順番を入れ替えることでエラーが消滅するのか？

# 補足

pycharm上でなくターミナルにて`m.py`を実行した場合、ModuleNotFoundErrorが出ました。

## コマンド

[code]

    /usr/bin/python3.8 /home/USER/XXX/XXXX/tsmod/m.py
    
[/code]

## エラー内容

[code]

    Traceback (most recent call last):
      File "/home/USER/XXX/XXXX/tsmod/m.py", line 1, in <module>
        from tsmod import ModuleP
    ModuleNotFoundError: No module named 'tsmod'
    
[/code]




**1\. なぜエラーが発生しているのか？**

状況とエラーメッセージ内容を見ると、メッセージに書いてある通り`circular import`が発生しているためでしょう。

[code]

    ImportError: cannot import name 'ModuleP' from partially initialized module 'tsmod' (most likely due to a circular import) (/home/USER/XXX/XXXX/tsmod/__init__.py)
    
[/code]

cannot import name 'ModuleP' from partially initialized module 'tsmod'  
→部分的に初期化されたモジュール「tsmod」から名前「ModuleP」をインポートできません。

* * *

(Ubuntu上の?)PyCharmでモジュール内のあるスクリプトを実行すると、モジュールをインポートしたように見えるのは謎ですが、エラーのスタックトレースを見る限り以下のような状況でしょう。

  1. `m.py`の1行目の`from tsmod import ModuleP`が処理される
  2. `tsmod`モジュールの`__init__.py`の1行目の`from tsmod.m import ModuleM`が処理される
  3. この時点で`tsmod`モジュールの情報が(おそらくモジュール名だけ)部分的にPythonに登録される
  4. `m.py`の1行目の`from tsmod import ModuleP`が処理される
  5. Pythonに登録された部分的な`tsmod`モジュール情報内に`ModuleP`が存在しないのでエラーになる

* * *

**2\. なぜ`__init__.py`の順番を入れ替えることでエラーが消滅するのか？**

`__init__.py`内の行を逆にすると、以下のように動作すると思われます。

  1. `m.py`の1行目の`from tsmod import ModuleP`が処理される
  2. `tsmod`モジュールの`__init__.py`の1行目の`from tsmod.p import ModuleP`が処理される
  3. `p.py`の`ModuleP`が処理され、`tsmod`モジュールに登録される
  4. `tsmod`モジュールの`__init__.py`の2行目の`from tsmod.m import ModuleM`が処理される
  5. `m.py`の1行目の`from tsmod import ModuleP`が処理される
  6. `tsmod`モジュールには既に`ModuleP`が登録されているので、正常にインポートされる
  7. `m.py`の`ModuleM`が処理され、`tsmod`モジュールに登録される
  8. この時点で`tsmod`モジュールの情報が完全にPythonに登録される==`__init__.py`の処理が終了する
  9. `m.py`の`ModuleM`が処理され、`tsmod`モジュールに登録される？？？(どうなっているかは不明)

* * *

そしておそらく @oriri さん回答のように`from tsmod.p import
ModuleP`とした場合は、以下のようにPython上に登録された情報ではなく`p.py`ファイルを直接見に行くのでしょう。

  1. `m.py`の1行目の`from tsmod.p import ModuleP`が処理される
  2. `tsmod`フォルダ？の`p.py`が読み込まれて`ModuleP`が処理・登録される
  3. `m.py`の`ModuleM`が処理・登録される

* * *

例えば`__init__.py`を含む各ファイルの各行の前後・間に`print('何か挿入箇所の位置情報')`を挿入してどの順番で実行されているかを調べると、状況が分かると思われます。

* * *

こちらの記事等を参考に`circular import`となるような設計は見直した方が良いでしょう。  
[python import動作](https://ja.stackoverflow.com/q/73880/26370)




**1\. なぜエラーが発生しているのか？**

状況とエラーメッセージ内容を見ると、メッセージに書いてある通り`circular import`が発生しているためでしょう。

[code]

    ImportError: cannot import name 'ModuleP' from partially initialized module 'tsmod' (most likely due to a circular import) (/home/USER/XXX/XXXX/tsmod/__init__.py)
    
[/code]

cannot import name 'ModuleP' from partially initialized module 'tsmod'  
→部分的に初期化されたモジュール「tsmod」から名前「ModuleP」をインポートできません。

* * *

(Ubuntu上の?)PyCharmでモジュール内のあるスクリプトを実行すると、モジュールをインポートしたように見えるのは謎ですが、エラーのスタックトレースを見る限り以下のような状況でしょう。

  1. `m.py`の1行目の`from tsmod import ModuleP`が処理される
  2. `tsmod`モジュールの`__init__.py`の1行目の`from tsmod.m import ModuleM`が処理される
  3. この時点で`tsmod`モジュールの情報が(おそらくモジュール名だけ)部分的にPythonに登録される
  4. `m.py`の1行目の`from tsmod import ModuleP`が処理される
  5. Pythonに登録された部分的な`tsmod`モジュール情報内に`ModuleP`が存在しないのでエラーになる

* * *

**2\. なぜ`__init__.py`の順番を入れ替えることでエラーが消滅するのか？**

`__init__.py`内の行を逆にすると、以下のように動作すると思われます。

  1. `m.py`の1行目の`from tsmod import ModuleP`が処理される
  2. `tsmod`モジュールの`__init__.py`の1行目の`from tsmod.p import ModuleP`が処理される
  3. `p.py`の`ModuleP`が処理され、`tsmod`モジュールに登録される
  4. `tsmod`モジュールの`__init__.py`の2行目の`from tsmod.m import ModuleM`が処理される
  5. `m.py`の1行目の`from tsmod import ModuleP`が処理される
  6. `tsmod`モジュールには既に`ModuleP`が登録されているので、正常にインポートされる
  7. `m.py`の`ModuleM`が処理され、`tsmod`モジュールに登録される
  8. この時点で`tsmod`モジュールの情報が完全にPythonに登録される==`__init__.py`の処理が終了する
  9. `m.py`の`ModuleM`が処理され、`tsmod`モジュールに登録される？？？(どうなっているかは不明)

* * *

そしておそらく @oriri さん回答のように`from tsmod.p import
ModuleP`とした場合は、以下のようにPython上に登録された情報ではなく`p.py`ファイルを直接見に行くのでしょう。

  1. `m.py`の1行目の`from tsmod.p import ModuleP`が処理される
  2. `tsmod`フォルダ？の`p.py`が読み込まれて`ModuleP`が処理・登録される
  3. `m.py`の`ModuleM`が処理・登録される

* * *

例えば`__init__.py`を含む各ファイルの各行の前後・間に`print('何か挿入箇所の位置情報')`を挿入してどの順番で実行されているかを調べると、状況が分かると思われます。

* * *

こちらの記事等を参考に`circular import`となるような設計は見直した方が良いでしょう。  
[python import動作](https://ja.stackoverflow.com/q/73880/26370)

