Python のモジュール
◆ Python のモジュールとインポート
最近なぜか Python で Node.js 風に親フォルダのファイルをインポートするというあまり一般的でない特殊なことしてる記事がアクセスランキング 1 位になってます
そういえば Python で普通のモジュールのインポートについては書いてなかったなと思ったのでまとめです
モジュールを置くフォルダが事前に決まっていて そこから名前でインポートされます
その置く場所は sys.path で指定します
デフォルトの sys.path はこんな感じです
インストール方法や OS などの違いもあり これは CentOS8 の例です
最初がルートパスになっていますが これはメインモジュールのフォルダになります
-m で実行した場合はカレントディレクトリになります
以降のフォルダは標準のモジュールや pip でインストールするモジュールが配置されるところです
python3.8 フォルダには python で書かれたモジュールがあります
http, json, os, site, urllib などはここに入ってます
sys は特殊なのでここにはありません
lib-dynload は dynamic load library で so ファイルが入ってます
Windows だと DLLs フォルダで dll ファイルが入ってます
site-packages (一部ディストリビューションでは dist-packages) はサードパーティのモジュール置き場で pip でインストールしたモジュールはここに入ります
デフォルトのモジュール置き場はこういう感じで用途が決まってるので 自作のモジュールを配置するなら これらに混ぜるよりは自作モジュール置き場を用意して sys.path に追加するほうが良さそうです
sys.path の追加は環境変数からできます
PYTHONPATH に 「:」 区切りでパスを指定します
実行中に直接リストを書き換えることもできます
[/opt/py/m1.py]
インポートできてます
-m でも呼び出せます
この方法だと実行中に sys.path を追加できないので環境変数を使う方法にします
また何も出力がないと分かりづらいので print 処理を追加します
[/opt/py/m2.py]
__init__.py があるフォルダはモジュールとしてインポートできます
インポートするときに __init__.py が実行されます
[/opt/py/m3/__init__.py]
[/tmp/main.py]
ファイルでもフォルダでも Python ではモジュールですが フォルダの場合はパッケージでもあります
「.」 で階層を作って名前が重複しないようにできる仕組みのことをパッケージと呼んでるようです
フォルダに a.py を置いてみます
[/opt/py/m3/a.py]
これだけだと a.py は使われません
[/tmp/main.py]
a にアクセスしても a 属性はないというエラーです
a.py のモジュールも使うなら __init__.py でインポートする必要があります
[/opt/py/m3/__init__.py]
import では 「from . import a」 のようにして相対指定にします
import だけだと sys.path のパスから検索されるので a.py は見つかりません
ならインポートできますが 「m3」 のようにパッケージルートからのパスを書かないといけなくなります
単純に書くのが面倒な上に コピペして別のところで使うときに パッケージ名も変えないといけないので 自分を指す 「.」 からの相対インポートとしたほうが扱いやすいです
また m3.a だと m3 もインポートされて m3 という名前が使われてしまうデメリットもあります
m3 モジュールを使うときに a.py のモジュールが必須ではないなら __init__.py で常に a.py までインポートしてしまうと無駄になる場合がありえます
使う側にインポートしてもらうなら __init__.py では書かないこともできます
[/opt/py/m3/__init__.py]
[/opt/py/m3/a.py]
[/tmp/main.py]
「m3.a」 をインポートすれば 「m3.a」 で a.py のモジュールにアクセスできます
また m3/__init__.py も自動でインポートされるので m3.value にもアクセスできます
[/opt/py/m3/__init__.py]
fn 関数内で a.value にアクセスしていますが a は定義していません
なので
[/tmp/main.py]
を実行しても
となります
ですが m3.a までインポートすると
[/tmp/main.py]
とエラーはなく動きます
問題となるのは a をすでに定義しているケースです
[/opt/py/m3/__init__.py]
のように上書きされて fn の結果が変わります
こういうことがあるので基本的には m3 フォルダの中に a.py があるなら m3/__init__.py では a という変数をグローバルで使わないようにしておくべきだと思います
逆に m3.a などをインポートしたときに関数の動きを変えたいという場合には使えるかもしれません
また from を使って
でインポートすると そのモジュールの変数に m3 は作られませんが m3/__init__.py は実行されています
パッケージの場合 __init__.py をインポートするときは同じ階層にあるものとしてインポートできますが その __init__.py からは 「.」 だとフォルダ内のモジュールが対象になります
パッケージのインポート元と同じ階層にあるモジュールをインポートするには 「..」 が必要になります
import の仕方で考えるよりフォルダ階層を見たほうがわかりやすいです
__init__.py はフォルダの中にあるので一つ内側にあるという感じです
という順でインポートします
[/tmp/main.py]
[/opt/py/m4/__init__.py]
[/opt/py/m4/a.py]
[/opt/py/m4/b.py]
[/opt/py/m4/c/__init__.py]
[/opt/py/m4/d.py]
c のインポートでは 「from .」 ですが d のインポートでは 「from ..」 です
「..」 で戻れるのはパッケージルートまでになっていて パッケージの外に出ようとするとエラーです
[/tmp/main.py]
[/opt/py/m5/__init__.py]
また パッケージ外のモジュールで 「from .」 や 「from ..」 を使って相対インポートしようとしてもエラーになります
[/tmp/main.py]
[/opt/py/m6.py]
キャッシュ済みのモジュールのインポートはここの参照を変数に代入するだけです
[/tmp/main.py]
[/opt/py/m7/__init__.py]
[/opt/py/m7/a.py]
ユーザが指定したファイルを実行する前の内部的な初期化で Python の処理が実行されているので そこでインポートされるモジュールが最初から sys.modules に入っています
sys.modules は dict 型で 「m7.a」 のような 「.」 区切りもそのまま文字列としてキーになっています
「sys.modules["m7.a"]」 を直接扱うこともできます
sys.modules にキーを追加したり インポート済みのモジュールを変更しても問題なく動きます
[/tmp/main.py]
フォルダ自体をモジュールとせず py ファイルを指定するのであれば __init__.py を使わなくてもインポートできます
[/tmp/main.py]
[/opt/py/m9/a.py]
sys.path に /opt/py1 と /opt/py2 を追加して その両方に foo フォルダを作ります
__init__.py があれば sys.path の順番が先の /opt/py1/foo がインポート対象になり /opt/py2/foo は対象になりません
ですが __init__.py がないので両方からモジュールをインポートできます
[/opt/py1/foo/bar.py]
[/opt/py2/foo/baz.py]
[/tmp/main.py]
py1 と py2 からインポートできています
foo モジュール自体はどうなっているかというと インポートパスの代わりに (namespace) となっていました
現状がこのタイプのパッケージが特に使われてないからであって 標準のライブラリがこのタイプになっていれば pip で追加するライブラリで標準パッケージを拡張ということはできるかもしれません
ただ わざわざ同じパッケージ内のモジュールにしなくても 別パッケージでもいいように思います
関数やモジュールオブジェクトを渡せば済むことがほとんどだと思いますし
結果的に __init__.py を作らなくても sys.path に追加したフォルダから py ファイルまでのパスだけでインポートできるようになったのが一番のメリットだと思ってます
Python ならではの フォルダをモジュールとしてインポートするというちょっと変わったインポートシステムより単純にファイルへのパスでインポートできるほうが扱いやすいですし
しかし __init__.py の省略はデメリットもあります
__init__.py がそのフォルダが Python パッケージであるというマーカーの役割を果たしています
ツールによっては __init__.py を確認してフォルダの扱いを変えるものもあるようで 省略すると正しく動かないケースもあるようです
個人的には それで困ってはないですし その場限りのスクリプトみたいのでは __init__.py を書かないほうが多いです
__init__.py がある理由はドキュメントによると __init__.py なしでもパッケージとみなしたら Python のパッケージとするつもりのないただのフォルダまでパッケージ扱いされてしまって インポートしたいパッケージが隠されてしまうことがあるので __init__.py があるフォルダだけをパッケージとみなすことでそれを防げるということが挙げられていました
http とか json みたいなフォルダ名はパッケージ以外で使うこともありそうですからね
__init__.py を使わないインポート方法ではフォルダではインポートせず py ファイルを指定することになるので この問題には影響しないようです
「/opt/py1」 と 「/opt/py2」 を sys.path に加えた状態で 両方に pkg1 フォルダを作って py2 の方にだけ __init__.py を設置します
py1/pkg1 は無視され py2/pkg1 が pkg1 としてインポートされています
pkg1.a のインポート結果も py2 の方になっています
デフォルトの importer の中身はここで見れます
https://github.com/python/cpython/blob/v3.8.10/Lib/importlib/_bootstrap.py
https://github.com/python/cpython/blob/v3.8.10/Lib/importlib/_bootstrap_external.py
そういえば Python で普通のモジュールのインポートについては書いてなかったなと思ったのでまとめです
sys.path
Python ではファイルパスを指定してモジュールをインポートするという考え方ではないですモジュールを置くフォルダが事前に決まっていて そこから名前でインポートされます
その置く場所は sys.path で指定します
デフォルトの sys.path はこんな感じです
[root@3e7a14ca52d0 /]# python3 -m site
sys.path = [
'/',
'/usr/lib64/python38.zip',
'/usr/lib64/python3.8',
'/usr/lib64/python3.8/lib-dynload',
'/usr/lib64/python3.8/site-packages',
'/usr/lib/python3.8/site-packages',
]
USER_BASE: '/root/.local' (doesn't exist)
USER_SITE: '/root/.local/lib/python3.8/site-packages' (doesn't exist)
ENABLE_USER_SITE: True
インストール方法や OS などの違いもあり これは CentOS8 の例です
最初がルートパスになっていますが これはメインモジュールのフォルダになります
-m で実行した場合はカレントディレクトリになります
[root@3e7a14ca52d0 ~]# cd /tmp
[root@3e7a14ca52d0 tmp]# python3 -m site
sys.path = [
'/tmp',
'/usr/lib64/python38.zip',
'/usr/lib64/python3.8',
'/usr/lib64/python3.8/lib-dynload',
'/usr/lib64/python3.8/site-packages',
'/usr/lib/python3.8/site-packages',
]
USER_BASE: '/root/.local' (doesn't exist)
USER_SITE: '/root/.local/lib/python3.8/site-packages' (doesn't exist)
ENABLE_USER_SITE: True
[root@a601ef191f7d tmp]# echo "import sys;print(sys.path)" > /root/1.py
[root@a601ef191f7d tmp]# python3 /root/1.py
['/root', '/usr/lib64/python38.zip', '/usr/lib64/python3.8', '/usr/lib64/python3.8/lib-dynload',
'/usr/lib64/python3.8/site-packages', '/usr/lib/python3.8/site-packages']
以降のフォルダは標準のモジュールや pip でインストールするモジュールが配置されるところです
python3.8 フォルダには python で書かれたモジュールがあります
[root@a601ef191f7d tmp]# ls -l /usr/lib64/python3.8/
total 4780
-rw-r--r-- 1 root root 12775 May 13 2020 LICENSE.txt
-rw-r--r-- 1 root root 5109 May 13 2020 __future__.py
-rw-r--r-- 1 root root 64 May 13 2020 __phello__.foo.py
drwxr-xr-x 2 root root 36864 May 14 12:22 __pycache__
-rw-r--r-- 1 root root 1801 May 13 2020 _bootlocale.py
-rw-r--r-- 1 root root 26100 May 13 2020 _collections_abc.py
-rw-r--r-- 1 root root 8749 May 13 2020 _compat_pickle.py
-rw-r--r-- 1 root root 5340 May 13 2020 _compression.py
-rw-r--r-- 1 root root 6027 May 13 2020 _dummy_thread.py
-rw-r--r-- 1 root root 14598 May 13 2020 _markupbase.py
-rw-r--r-- 1 root root 19600 May 13 2020 _osx_support.py
-rw-r--r-- 1 root root 6189 May 13 2020 _py_abc.py
-rw-r--r-- 1 root root 228666 May 13 2020 _pydecimal.py
-rw-r--r-- 1 root root 93177 May 13 2020 _pyio.py
-rw-r--r-- 1 root root 3115 May 13 2020 _sitebuiltins.py
-rw-r--r-- 1 root root 25268 May 13 2020 _strptime.py
-rw-r--r-- 1 root root 38417 Aug 31 2020 _sysconfigdata__linux_x86_64-linux-gnu.py
-rw-r--r-- 1 root root 38144 Aug 31 2020 _sysconfigdata_d_linux_x86_64-linux-gnu.py
-rw-r--r-- 1 root root 7220 May 13 2020 _threading_local.py
-rw-r--r-- 1 root root 5735 May 13 2020 _weakrefset.py
-rw-r--r-- 1 root root 4489 May 13 2020 abc.py
-rw-r--r-- 1 root root 32814 May 13 2020 aifc.py
-rw-r--r-- 1 root root 477 May 13 2020 antigravity.py
-rw-r--r-- 1 root root 96015 May 13 2020 argparse.py
-rw-r--r-- 1 root root 18608 May 13 2020 ast.py
-rw-r--r-- 1 root root 11328 May 13 2020 asynchat.py
drwxr-xr-x 3 root root 4096 May 14 12:22 asyncio
-rw-r--r-- 1 root root 20094 May 13 2020 asyncore.py
-rwxr-xr-x 1 root root 20380 May 13 2020 base64.py
略
http, json, os, site, urllib などはここに入ってます
sys は特殊なのでここにはありません
lib-dynload は dynamic load library で so ファイルが入ってます
Windows だと DLLs フォルダで dll ファイルが入ってます
site-packages (一部ディストリビューションでは dist-packages) はサードパーティのモジュール置き場で pip でインストールしたモジュールはここに入ります
デフォルトのモジュール置き場はこういう感じで用途が決まってるので 自作のモジュールを配置するなら これらに混ぜるよりは自作モジュール置き場を用意して sys.path に追加するほうが良さそうです
sys.path の追加は環境変数からできます
PYTHONPATH に 「:」 区切りでパスを指定します
[root@3e7a14ca52d0 /]# PYTHONPATH="/tmp/foo:/tmp/bar" python3 -m site
sys.path = [
'/',
'/tmp/foo',
'/tmp/bar',
'/usr/lib64/python38.zip',
'/usr/lib64/python3.8',
'/usr/lib64/python3.8/lib-dynload',
'/usr/lib64/python3.8/site-packages',
'/usr/lib/python3.8/site-packages',
]
USER_BASE: '/root/.local' (doesn't exist)
USER_SITE: '/root/.local/lib/python3.8/site-packages' (doesn't exist)
ENABLE_USER_SITE: True
実行中に直接リストを書き換えることもできます
>>> import sys
>>> sys.path = [*sys.path, "/tmp/bar"]
>>> sys.path
['', '/usr/lib64/python38.zip', '/usr/lib64/python3.8', '/usr/lib64/python3.8/lib-dynload',
'/usr/lib64/python3.8/site-packages', '/usr/lib/python3.8/site-packages', '/tmp/bar']
モジュール
「/opt/py」 というフォルダを作ってここを自作モジュールを置き場とすることにします[/opt/py/m1.py]
value = "this is m1.py"
[root@a601ef191f7d tmp]# python3
Python 3.8.3 (default, Aug 31 2020, 16:03:14)
[GCC 8.3.1 20191121 (Red Hat 8.3.1-5)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.path = [*sys.path, "/opt/py"]
>>> import m1
>>> m1.value
'this is m1.py'
インポートできてます
-m でも呼び出せます
この方法だと実行中に sys.path を追加できないので環境変数を使う方法にします
また何も出力がないと分かりづらいので print 処理を追加します
[/opt/py/m2.py]
value = "this is m2.py"
print(value)
[root@a601ef191f7d tmp]# PYTHONPATH="/opt/py" python3 -m m2
this is m2.py
フォルダ
フォルダもモジュールにできます__init__.py があるフォルダはモジュールとしてインポートできます
インポートするときに __init__.py が実行されます
[/opt/py/m3/__init__.py]
value = "this is m3/__init__.py"
[/tmp/main.py]
import sys
sys.path.append("/opt/py")
import m3
print(m3.value)
[root@a601ef191f7d tmp]# python3 main.py
this is m3/__init__.py
ファイルでもフォルダでも Python ではモジュールですが フォルダの場合はパッケージでもあります
「.」 で階層を作って名前が重複しないようにできる仕組みのことをパッケージと呼んでるようです
パッケージ
__init__.py しか使わないのだとせっかくフォルダにしたのに ファイルのときと一緒ですフォルダに a.py を置いてみます
[/opt/py/m3/a.py]
value = "this is m3/a.py"
これだけだと a.py は使われません
[/tmp/main.py]
import sys
sys.path.append("/opt/py")
import m3
print(m3.a.value)
[root@a601ef191f7d tmp]# python3 main.py
Traceback (most recent call last):
File "main.py", line 5, in <module>
print(m3.a.value)
AttributeError: module 'm3' has no attribute 'a'
a にアクセスしても a 属性はないというエラーです
a.py のモジュールも使うなら __init__.py でインポートする必要があります
[/opt/py/m3/__init__.py]
from . import a
value = "this is m3/__init__.py"
[root@a601ef191f7d tmp]# python3 main.py
this is m3/a.py
import では 「from . import a」 のようにして相対指定にします
import だけだと sys.path のパスから検索されるので a.py は見つかりません
import m3.a
ならインポートできますが 「m3」 のようにパッケージルートからのパスを書かないといけなくなります
単純に書くのが面倒な上に コピペして別のところで使うときに パッケージ名も変えないといけないので 自分を指す 「.」 からの相対インポートとしたほうが扱いやすいです
また m3.a だと m3 もインポートされて m3 という名前が使われてしまうデメリットもあります
m3 モジュールを使うときに a.py のモジュールが必須ではないなら __init__.py で常に a.py までインポートしてしまうと無駄になる場合がありえます
使う側にインポートしてもらうなら __init__.py では書かないこともできます
[/opt/py/m3/__init__.py]
value = "this is m3/__init__.py"
[/opt/py/m3/a.py]
value = "this is m3/a.py"
[/tmp/main.py]
import sys
sys.path.append("/opt/py")
import m3.a
print(m3.value)
print(m3.a.value)
[root@a601ef191f7d tmp]# python3 main.py
this is m3/__init__.py
this is m3/a.py
「m3.a」 をインポートすれば 「m3.a」 で a.py のモジュールにアクセスできます
また m3/__init__.py も自動でインポートされるので m3.value にもアクセスできます
サブモジュールの注意点
ただこれには注意するところもあって 「m3.a」 でアクセスできるということは m3/__init__.py のモジュールのグローバル変数の a も作られるということです[/opt/py/m3/__init__.py]
value = "this is m3/__init__.py"
def fn():
print(a.value)
fn 関数内で a.value にアクセスしていますが a は定義していません
なので
[/tmp/main.py]
import sys
sys.path.append("/opt/py")
import m3
m3.fn()
を実行しても
[root@a601ef191f7d tmp]# python3 main.py
Traceback (most recent call last):
File "main.py", line 5, in <module>
m3.fn()
File "/opt/py/m3/__init__.py", line 4, in fn
print(a.value)
NameError: name 'a' is not defined
となります
ですが m3.a までインポートすると
[/tmp/main.py]
import sys
sys.path.append("/opt/py")
import m3.a
m3.fn()
[root@a601ef191f7d tmp]# python3 main.py
this is m3/a.py
とエラーはなく動きます
問題となるのは a をすでに定義しているケースです
[/opt/py/m3/__init__.py]
value = "this is m3/__init__.py"
class A:
value = "A"
a = A()
def fn():
print(a.value)
fn()
[root@a601ef191f7d tmp]# python3 main.py
A
this is m3/a.py
のように上書きされて fn の結果が変わります
こういうことがあるので基本的には m3 フォルダの中に a.py があるなら m3/__init__.py では a という変数をグローバルで使わないようにしておくべきだと思います
逆に m3.a などをインポートしたときに関数の動きを変えたいという場合には使えるかもしれません
また from を使って
from m3 import a
でインポートすると そのモジュールの変数に m3 は作られませんが m3/__init__.py は実行されています
相対インポート
from に 「.」 を使うと同じ階層(同パッケージ)からの相対インポート 「..」 を使うと上の階層(親パッケージ)からの相対インポートになりますパッケージの場合 __init__.py をインポートするときは同じ階層にあるものとしてインポートできますが その __init__.py からは 「.」 だとフォルダ内のモジュールが対象になります
パッケージのインポート元と同じ階層にあるモジュールをインポートするには 「..」 が必要になります
import の仕方で考えるよりフォルダ階層を見たほうがわかりやすいです
__init__.py はフォルダの中にあるので一つ内側にあるという感じです
m4 (m4/__init__.py)
m4.a (m4/a.py)
m4.b (m4/b.py)
m4.c (m4/c/__init__.py)
m4.d (m4/d.py)
という順でインポートします
[/tmp/main.py]
import sys
sys.path.append("/opt/py")
import m4
[/opt/py/m4/__init__.py]
from . import a
[/opt/py/m4/a.py]
from . import b
[/opt/py/m4/b.py]
from . import c
[/opt/py/m4/c/__init__.py]
from .. import d
[/opt/py/m4/d.py]
print("d.py is imported")
[root@a601ef191f7d tmp]# python3 main.py
d.py is imported
c のインポートでは 「from .」 ですが d のインポートでは 「from ..」 です
「..」 で戻れるのはパッケージルートまでになっていて パッケージの外に出ようとするとエラーです
[/tmp/main.py]
import sys
sys.path.append("/opt/py")
import m5
[/opt/py/m5/__init__.py]
from .. import m1
[root@a601ef191f7d tmp]# python3 main.py
Traceback (most recent call last):
File "main.py", line 4, in <module>
import m5
File "/opt/py/m5/__init__.py", line 1, in <module>
from .. import m1
ValueError: attempted relative import beyond top-level package
また パッケージ外のモジュールで 「from .」 や 「from ..」 を使って相対インポートしようとしてもエラーになります
[/tmp/main.py]
import sys
sys.path.append("/opt/py")
import m6
[/opt/py/m6.py]
from . import m1
[root@a601ef191f7d tmp]# python3 main.py
Traceback (most recent call last):
File "main.py", line 4, in <module>
import m6
File "/opt/py/m6.py", line 1, in <module>
from . import m1
ImportError: attempted relative import with no known parent package
sys.modules
インポート時にロードされたモジュールは sys.modules にキャッシュされますキャッシュ済みのモジュールのインポートはここの参照を変数に代入するだけです
[/tmp/main.py]
import sys
sys.path.append("/opt/py")
import m7.a
print(sys.modules)
print(sys.modules["m7"].value)
print(sys.modules["m7.a"].value)
[/opt/py/m7/__init__.py]
value = "this is m7/__init__.py"
[/opt/py/m7/a.py]
value = "this is m7/a.py"
[root@a601ef191f7d tmp]# python3 main.py
{'__main__': <module '__main__' from 'main.py'>,
'_abc': <module '_abc' (built-in)>,
'_codecs': <module '_codecs' (built-in)>,
(略)
'm7': <module 'm7' from '/opt/py/m7/__init__.py'>,
'm7.a': <module 'm7.a' from '/opt/py/m7/a.py'>,
(略)
'sys': <module 'sys' (built-in)>,
'time': <module 'time' (built-in)>,
'types': <module 'types' from '/usr/lib64/python3.8/types.py'>,
'zipimport': <module 'zipimport' (frozen)>}
this is m7/__init__.py
this is m7/a.py
ユーザが指定したファイルを実行する前の内部的な初期化で Python の処理が実行されているので そこでインポートされるモジュールが最初から sys.modules に入っています
sys.modules は dict 型で 「m7.a」 のような 「.」 区切りもそのまま文字列としてキーになっています
「sys.modules["m7.a"]」 を直接扱うこともできます
sys.modules にキーを追加したり インポート済みのモジュールを変更しても問題なく動きます
[/tmp/main.py]
import sys
sys.modules["m8"] = "FOO"
import m8
print(m8)
[root@a601ef191f7d tmp]# python3 main.py
FOO
__init__.py なしパッケージ
フォルダをモジュールとするために __init__.py を使ってきましたが これは昔ながらの方法ですフォルダ自体をモジュールとせず py ファイルを指定するのであれば __init__.py を使わなくてもインポートできます
[/tmp/main.py]
import sys
sys.path.append("/opt/py")
import m9.a
print(m9.a.value)
[/opt/py/m9/a.py]
value = "this is m9/a.py"
[root@a601ef191f7d tmp]# python3 main.py
this is m9/a.py
sys.path に /opt/py1 と /opt/py2 を追加して その両方に foo フォルダを作ります
__init__.py があれば sys.path の順番が先の /opt/py1/foo がインポート対象になり /opt/py2/foo は対象になりません
ですが __init__.py がないので両方からモジュールをインポートできます
[/opt/py1/foo/bar.py]
value = "this is py1/foo/bar.py"
[/opt/py2/foo/baz.py]
value = "this is py2/foo/baz.py"
[/tmp/main.py]
import sys
sys.path = ["/opt/py1", "/opt/py2", *sys.path]
import foo.bar
import foo.baz
print(foo.bar.value)
print(foo.baz.value)
print(foo.bar)
print(foo.baz)
print(foo)
[root@a601ef191f7d tmp]# python3 main.py
this is py1/foo/bar.py
this is py2/foo/baz.py
<module 'foo.bar' from '/opt/py1/foo/bar.py'>
<module 'foo.baz' from '/opt/py2/foo/baz.py'>
<module 'foo' (namespace)>
py1 と py2 からインポートできています
foo モジュール自体はどうなっているかというと インポートパスの代わりに (namespace) となっていました
メリットとデメリット
sys.path で指定するモジュール置き場のフォルダが違っても同じパッケージにまとめられるメリットはあるのですが それが必要になるシーンはこれと言って思いつきません現状がこのタイプのパッケージが特に使われてないからであって 標準のライブラリがこのタイプになっていれば pip で追加するライブラリで標準パッケージを拡張ということはできるかもしれません
ただ わざわざ同じパッケージ内のモジュールにしなくても 別パッケージでもいいように思います
関数やモジュールオブジェクトを渡せば済むことがほとんどだと思いますし
結果的に __init__.py を作らなくても sys.path に追加したフォルダから py ファイルまでのパスだけでインポートできるようになったのが一番のメリットだと思ってます
Python ならではの フォルダをモジュールとしてインポートするというちょっと変わったインポートシステムより単純にファイルへのパスでインポートできるほうが扱いやすいですし
しかし __init__.py の省略はデメリットもあります
__init__.py がそのフォルダが Python パッケージであるというマーカーの役割を果たしています
ツールによっては __init__.py を確認してフォルダの扱いを変えるものもあるようで 省略すると正しく動かないケースもあるようです
個人的には それで困ってはないですし その場限りのスクリプトみたいのでは __init__.py を書かないほうが多いです
__init__.py がある理由はドキュメントによると __init__.py なしでもパッケージとみなしたら Python のパッケージとするつもりのないただのフォルダまでパッケージ扱いされてしまって インポートしたいパッケージが隠されてしまうことがあるので __init__.py があるフォルダだけをパッケージとみなすことでそれを防げるということが挙げられていました
http とか json みたいなフォルダ名はパッケージ以外で使うこともありそうですからね
__init__.py を使わないインポート方法ではフォルダではインポートせず py ファイルを指定することになるので この問題には影響しないようです
「/opt/py1」 と 「/opt/py2」 を sys.path に加えた状態で 両方に pkg1 フォルダを作って py2 の方にだけ __init__.py を設置します
[root@a601ef191f7d tmp]# mkdir -p /opt/py1/pkg1 /opt/py2/pkg1
[root@a601ef191f7d tmp]# echo "value = 'py1'" > /opt/py1/pkg1/a.py
[root@a601ef191f7d tmp]# echo "value = 'py2'" > /opt/py2/pkg1/a.py
[root@a601ef191f7d tmp]# touch /opt/py2/pkg1/__init__.py
[root@a601ef191f7d tmp]# PYTHONPATH="/opt/py1:/opt/py2" python3
Python 3.8.3 (default, Aug 31 2020, 16:03:14)
[GCC 8.3.1 20191121 (Red Hat 8.3.1-5)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pkg1
>>> print(pkg1)
<module 'pkg1' from '/opt/py2/pkg1/__init__.py'>
>>> import pkg1.a
>>> print(pkg1.a.value)
py2
py1/pkg1 は無視され py2/pkg1 が pkg1 としてインポートされています
pkg1.a のインポート結果も py2 の方になっています
その他
他にも sys.meta_path に設定されている importer を置き換えることで インポートシステムの仕組みを変更できる機能もあります>>> import sys, pprint
>>> pprint.pprint(sys.meta_path)
[<class '_frozen_importlib.BuiltinImporter'>,
<class '_frozen_importlib.FrozenImporter'>,
<class '_frozen_importlib_external.PathFinder'>]
デフォルトの importer の中身はここで見れます
https://github.com/python/cpython/blob/v3.8.10/Lib/importlib/_bootstrap.py
https://github.com/python/cpython/blob/v3.8.10/Lib/importlib/_bootstrap_external.py