Pythonのインポートを誤解して躓いた話

絶対インポートや相対インポートに関して誤解していたことがあるので、インポートを再分類してまとめ直してみました。
なお、環境構築にPyCharmを使用しています。


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


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


# Pythonの検索パス

インポートについて触れる前に、Pythonがどうやってインポート先のモジュールやパッケージを探しているのか？について書きたいと思います。
まず重要なこととして、この検索パスというのは<font color="red">**環境変数のPathとは別物**</font>です。

検索パスのリストは`sys.path`に格納されています。
試しに次のようなスクリプトを用意します。

```python:search_path.py
import sys
for path in sys.path:
    print(path)
```

`C:\work`にこのファイルを置き、コマンドプロンプト上で次のように実行します。

```:pythonスクリプト実行
python "C:\work\search_path.py"
```

```:実行結果
C:\work
C:\Users\username\AppData\Local\Programs\Python\Python310
C:\Users\username\AppData\Local\Programs\Python\Python310\lib
C:\Users\username\AppData\Local\Programs\Python\Python310\lib\site-packages
C:\Users\username\AppData\Local\Programs\Python\Python310\DLLs
C:\Users\username\AppData\Local\Programs\Python\Python310\Python310.zip
```

* `C:\work`
`search_path.py`を置いたフォルダ

* `C:\Users\username\AppData\Local\Programs\Python\Python310`
グローバルの`python.exe`が存在するフォルダ

* `C:\Users\username\AppData\Local\Programs\Python\Python310\lib`
グローバルの標準モジュールが存在するフォルダ

* `C:\Users\username\AppData\Local\Programs\Python\Python310\lib\site-packages`
グローバルに`pip`でインストールしたパッケージが存在するフォルダ
* などなど

今度はPyCharmのTerminalから実行してみます。
`myproject`というプロジェクトを作成し、Terminalを開きます。
<font color="red">**仮想環境が起動していることを確認してください。**</font>
【参考】[PyCharmのTerminalで仮想環境が自動起動せずに躓いた話](https://qiita.com/__Lily__/items/71c75566f067c19c18aa)

Python Interpreter生成時に`Inherit global site-packages`にチェックを入れていなかった場合、次のようになると思います。

```:実行結果
C:\work
C:\Users\username\AppData\Local\Programs\Python\Python310
C:\Users\username\AppData\Local\Programs\Python\Python310\lib
C:\Users\username\AppData\Local\Programs\Python\Python310\DLLs
C:\Users\username\AppData\Local\Programs\Python\Python310\Python310.zip
C:\work\myproject\venv
C:\work\myproject\venv\lib\site-packages
```

グローバルの`site-packages`が消え、代わりに仮想環境の`site-packages`が追加されています。

Python Interpreter生成時に`Inherit global site-packages`にチェックを入れていた場合、次のようになると思います。

```:実行結果
C:\work
C:\Users\username\AppData\Local\Programs\Python\Python310
C:\Users\username\AppData\Local\Programs\Python\Python310\lib
C:\Users\username\AppData\Local\Programs\Python\Python310\lib\site-packages
C:\Users\username\AppData\Local\Programs\Python\Python310\DLLs
C:\Users\username\AppData\Local\Programs\Python\Python310\Python310.zip
C:\work\myproject\venv
C:\work\myproject\venv\lib\site-packages
```

グローバルの`site-packages`と仮想環境の`site-packages`が両方存在しますね。

Pythonでは、これらのフォルダの中にインポート先が存在するかを検索している訳です。
ちなみにこの検索パスは自分で追加することもできます。
【参考】[Python内のデフォルトパスを通す方法（Windows, Linux）- Qiita](https://qiita.com/maech/items/72559402556eb2af73ad)


# Pythonのインポートの種類

Pythonには3種類のインポート方法があります。

* 絶対インポート
* 明示的な相対インポート
* 暗黙的な相対インポート

ですが、この3つで考えることが混乱のもとになると思うので、ここでは以下の4種類にわけて考えてみます。

* [1. 外部からのインポート](#1-外部からのインポート)
* [2. （内部）プロジェクト基準のインポート](#2-内部プロジェクト基準のインポート)
* [3. （内部）パッケージ基準のインポート](#3-内部パッケージ基準のインポート)
* [4. （内部）ファイル基準のインポート](#4-内部ファイル基準のインポート)

## 1. 外部からのインポート

以下の3点を「外部からのインポート」の定義とします。

* 標準モジュールからのインポート
* `pip`等でインストールしたパッケージからのインポート
* 自分でPython検索パスに追加したパッケージからのインポート

一般的に言う絶対インポートの一部であり、こういうやつです。

```python:外部からのインポートの例
import os
import sys
from tkinter import filedialog

import numpy
from openpyxl import load_workbook
```

これらは作業中のプロジェクトとは全く別のものなので、「外部」として切り出しました。

## 2. （内部）プロジェクト基準のインポート

スタートを**プロジェクトのソースフォルダ**とするインポート方法を「プロジェクト基準のインポート」と呼ぶことにします。
一般的に言う絶対インポートの一部であり、こういうやつです。

```:プロジェクト構成
myproject
    └ myproject
        ├ __init__.py
        ├ main.py
        ├ sub.py
        └ mypackage
            ├ __init__.py
            ├ module1.py
            └ module2.py
```

```Python:main.py
from myproject import sub
from myproject.mypackage import module1
```

```Python:sub.py
from myproject.mypackage import module2
```

```Python:mypackage\\module1.py
from myproject.mypackage import module2
```

上の例ですと`myproject\myproject`がソースフォルダに相当します。
プロジェクト基準のインポートを使用するには、<font color="red">**ソースフォルダが検索パス上に存在する必要があります。**</font>
この方法では、自分がソースフォルダ内のどの場所にいるか、ということは関係ありません。
実際、`sub`と`mypackage.module1`で`mypackage.module2`をインポートする書き方が同じになってますね。
また、ソースフォルダも一種のパッケージですから、<font color="red">**自分が何らかのパッケージに属していなければプロジェクト基準のインポートは使用できません。**</font>

## 3. （内部）パッケージ基準のインポート

スタートを**自分が属するパッケージ**とするインポート方法を「パッケージ基準のインポート」と呼ぶことにします。
一般的に言う明示的な相対インポートであり、こういうやつです。

```:プロジェクト構成
myproject
    └ myproject
        ├ __init__.py
        ├ main.py
        ├ sub.py
        └ mypackage
            ├ __init__.py
            ├ module1.py
            └ module2.py
```

```Python:main.py
from . import sub
from .mypackage import module1
```

```Python:sub.py
from .mypackage import module2
```

```Python:mypackage\\module1.py
from . import module2
```

上の例ですと`main.py`, `sub.py`であれば`myproject`が、`module1.py`, `module2.py`であれば`myproject.mypackage`が自分が属するパッケージに相当します。
そして、自分が属するパッケージを`import`文中では1点ピリオド`.`で表します。
当然のことですが、<font color="red">**自分が何らかのパッケージに属していなければパッケージ基準のインポートは使用できません。**</font>
また、パッケージ構造の情報は呼び出し元から与える必要があります。

プロジェクト基準のインポートと比較してみましょう。
`sub.py`から見て`module2.py`は、プロジェクト基準であれば`myproject.mypackage`にあり、パッケージ基準でも`(myproject).mypackage`にあります。
一方`module1.py`から見て`module2.py`は、プロジェクト基準であれば`myproject.mypackage`にありましたが、パッケージ基準では両者は同じパッケージ`(myproject.mypackage).`に属していることになります。

## 4. （内部）ファイル基準のインポート

スタートを**自分自身のファイル**とするインポート方法を「ファイル基準のインポート」と呼ぶことにします。
一般的に言う暗黙的な相対インポートであり、こういうやつです。

```:プロジェクト構成
myproject
    └ myproject
        ├ __init__.py
        ├ main.py
        ├ sub.py
        └ mypackage
            ├ __init__.py
            ├ module1.py
            └ module2.py
```

```Python:main.py
import sub
from mypackage import module1
```

```Python:sub.py
from mypackage import module2
```

```Python:mypackage\\module1.py
import module2
```

インポート先が同フォルダにある場合、`from`が省略されます。

なおこの方法については、<font color="red">**Python3では使用が禁止されています。**</font>


## 補足

* 絶対インポート
= 外部からのインポート 
\+ プロジェクト基準のインポート 
\+ **パッケージ外における**ファイル基準のインポート

* 明示的な相対インポート
= パッケージ基準のインポート

* 暗黙的な相対インポート
= **パッケージ内における**ファイル基準のインポート

* ファイル基準のインポートがPython3で禁止されているというのは少し嘘で、正確には「<font color="red">**パッケージ内における**</font>ファイル基準のインポート（＝暗黙的な相対インポート）」がPython3で禁止されています。

* 「**パッケージ外における**ファイル基準のインポート（絶対インポートの一部）」であれば使用可能ではありますが、**そもそも外部以外の関連モジュールが2つ以上になった時点でパッケージ化すべき**だと思います。
（暗黙的な相対インポートを禁止した理由を考慮すれば。）

* なので、いっそのことファイル基準のインポート自体を（ルールとして）禁止して「1ファイルから成るスクリプト」か「パッケージ」しか構築しないようにしても良いのではと考えています。
実行ファイルのフォルダには検索パスが通ってしまうので、システムとして防ぐということができないですからね。

* また、Pythonスクリプトを「ファイル名を指定して実行」する場合、そのファイルはどのパッケージにも属さないものとして扱われます。
そのため、「プロジェクト基準のインポート」「パッケージ基準のインポート」を使用しているファイルを「ファイル名を指定して実行」することはできません。


# まとめ

どうすれば混乱を防げるか？を考えた結果、このような解釈に辿り着きました。
まずは「ざっくりとルールを把握（ファイル基準のインポート禁止）」し、慣れてきたら「注意が必要なこと（パッケージ外におけるファイル基準のインポート）」を理解する......というのが良いのかなと思います。