Pythonのモジュールとパッケージ
shopという小さなパッケージで、初回の読込み、名前の公開、起動方法を確認します。最後に、二つのファイルが互いの定義を待ってしまう循環importを直します。
説明
Pythonでは、別ファイルにある定義をimportで利用できます。読込み時にはファイルの先頭から文が実行されます。関数を呼んでいなくても、初期化や表示が起きる場合があります。
shopという小さなパッケージで、初回の読込み、名前の公開、起動方法を確認します。最後に、二つのファイルが互いの定義を待ってしまう循環importを直します。
1パッケージ内のファイル
shopには初期化用の__init__.py、計算用のpricing.py、起動用の__main__.pyがあります。まず初期化と計算のコードを確認します。
head -n 20ファイル内容を表示 shop/__init__.pyパッケージの初期化 shop/pricing.py単価と計算関数__init__.pyの相対importは、同じshop内のpricingからtotalを取り込みます。pricingのrateは100で、totalは呼ばれたときのrateに個数を掛けます。先頭のprintは読込みの順序を調べるためのものです。
2同じプロセスで二度importする
一つのPythonプロセスでshopを二度importし、total(3)を呼びます。
python3 -c文字列をPythonで実行 "import shop; import shop同じパッケージを二度読み込む; print(shop.total(3))個数3の合計を表示"load shopとload pricingはそれぞれ一度だけです。通常の再importではsys.modulesに保持されたモジュールを再利用します。二度目のimportで初期化コードを繰り返すわけではありません。
3読込み済みモジュールの名前
別のPythonプロセスを起動し、shop関係で登録されたモジュール名を表示します。
python3 -c新しいPythonプロセスを起動 "import shop, sysパッケージと実行系情報; print(sorted(n for n in sys.modules読込み済みモジュールの辞書 if n == 'shop' or n.startswith('shop.')))"新しいプロセスなので初期化の表示も再び出ます。登録名はshopとshop.pricingです。pricing.pyを読み込む際の名前は単なるpricingではなく、パッケージ名を含みます。
4fromで取り込んだ値の更新
pricing.rateの整数を現在の名前空間にも取り込み、その後でモジュール側のrateを120へ変更します。
python3 -c "import shop.pricing as pモジュールをpという名前で参照; from shop.pricing import rate現在の値をrateへ束縛; p.rate = 120モジュール側の単価を変更; print(rate, p.rate, p.total(3))取り込み先と計算結果を比較"表示は100、120、360です。fromで取り込んだrateは最初の整数100を指しています。p.rateの再代入がその名前まで書き換えるわけではありません。一方、totalはpricing内のrateを参照するため360を返します。
5公開名の指定とアクセス
__all__はfrom shop import *が取り込む名前を指定します。取り込まれた名前と、内部用の名前への明示的なアクセスを確認します。
python3 -c "from shop import *__all__で指定された名前を取り込む; print(sorted(n for n in globals()現在の名前空間 if not n.startswith('_'))); import shop; print(shop._internal_note内部用の名前を明示して参照)"*で取り込まれるのはtotalだけです。ただし_internal_noteも、明示すればdraftを取得できます。__all__や先頭の下線は公開範囲の意図を示しますが、アクセス権の制御ではありません。
6パッケージの起動用コード
__main__.pyは、パッケージをプログラムとして起動したときの入口です。
cat shop/__main__.py起動用のコードを表示同じパッケージからtotalを取り込み、__name__が__main__の場合だけ合計を表示します。この条件により、通常のimportで起動処理を走らせずに済みます。
7パッケージ名で起動
-mにパッケージ名を渡して起動します。
python3 -m shopshopをパッケージとして起動shopの初期化後にshop/__main__.pyが実行され、total: 300が表示されます。この起動方法ではパッケージの文脈が設定されるため、from . import totalの相対importが成立します。
8ファイルだけを実行した場合
同じ入口ファイルをrunpy.run_pathで直接実行し、相対importが成立しない場合のエラーを表示します。
python3 -標準入力のPythonコードを実行 <<'PYCODE'
import runpy
try:
runpy.run_pathファイルを直接実行("shop/__main__.py", run_name="__main__")
except ImportError読込みエラーを表示 as error:
print(type(error).__name__ + ": " + str(error))
PYCODEファイルの直接実行では、親パッケージを特定できません。ファイルが同じ場所にあることだけでは相対importの文脈は作られないため、この入口はpython3 -m shopで起動します。
9互いの初期化を待つコード
badcycle.aはnameを定義する前にbを読み込みます。bがそのnameを要求するとどうなるか、コードと結果を続けて確認します。
head -n 20 badcycle/a.py badcycle/b.py循環する二つのコード
python3 -読込みを実行 <<'PYCODE'
try:
import badcycle.aaから読込みを開始
except ImportError初期化中の失敗を確認 as error:
print(type(error).__name__)
print("partially initialized" in str(error))
PYCODEImportErrorが出て、部分的に初期化されたモジュールを示す説明も含まれます。a→b→aと戻った時点では、aのnameへの代入はまだ実行されていません。互いを参照する構造すべてが必ず失敗するのではなく、未定義の名前を初期化中に要求することが今回の原因です。
10共通の定義を別ファイルへ移す
共通のnameとfeeをcommon.pyへ移します。aとbは互いを読み込まず、共通の定義だけを読み込む構成です。
head -n 20 goodcycle/common.py goodcycle/a.py goodcycle/b.py共通定義と利用側のコード
python3 -c修正後を実行 "from goodcycle import a, b二つのモジュールを読み込む; print(a.describe()); print(b.label())両方の関数を呼ぶ"book: 100とbook (100)の両方を取得できます。共通データへの依存がa→common、b→commonとなり、初期化中に相手の未定義名を要求する経路がなくなりました。
まとめ
import時には初期化コードが動き、通常の再importは同一プロセス内の登録済みモジュールを再利用します。
fromによる名前の束縛、__all__による公開名、パッケージとしての起動は、それぞれ異なる仕組みです。
循環importでは、名前が必要になる時点と定義の完了順を確認します。共通部分の切り出しで依存の循環を解消できます。
操作履歴と解説を振り返れます。
シェル / Python 3
head -n 20 shop/__init__.py shop/pricing.py==> shop/__init__.py <==
print("load shop")
from .pricing import total
__all__ = ["total"]
_internal_note = "draft"
==> shop/pricing.py <==
print("load pricing")
rate = 100
def total(quantity):
return rate * quantitypython3 -c "import shop; import shop; print(shop.total(3))"load shop load pricing 300
python3 -c "import shop, sys; print(sorted(n for n in sys.modules if n == 'shop' or n.startswith('shop.')))"load shop load pricing ['shop', 'shop.pricing']
python3 -c "import shop.pricing as p; from shop.pricing import rate; p.rate = 120; print(rate, p.rate, p.total(3))"load shop load pricing 100 120 360
python3 -c "from shop import *; print(sorted(n for n in globals() if not n.startswith('_'))); import shop; print(shop._internal_note)"load shop load pricing ['total'] draft
cat shop/__main__.pyfrom . import total
if __name__ == "__main__":
print("total:", total(3))python3 -m shopload shop load pricing total: 300
python3 - <<'PYCODE'
import runpy
try:
runpy.run_path("shop/__main__.py", run_name="__main__")
except ImportError as error:
print(type(error).__name__ + ": " + str(error))
PYCODEImportError: attempted relative import with no known parent package
head -n 20 badcycle/a.py badcycle/b.py
python3 - <<'PYCODE'
try:
import badcycle.a
except ImportError as error:
print(type(error).__name__)
print("partially initialized" in str(error))
PYCODE==> badcycle/a.py <== from .b import fee name = "book" ==> badcycle/b.py <== from .a import name fee = 100 ImportError True
head -n 20 goodcycle/common.py goodcycle/a.py goodcycle/b.py
python3 -c "from goodcycle import a, b; print(a.describe()); print(b.label())"==> goodcycle/common.py <==
name = "book"
fee = 100
==> goodcycle/a.py <==
from .common import name, fee
def describe():
return f"{name}: {fee}"
==> goodcycle/b.py <==
from .common import name, fee
def label():
return f"{name} ({fee})"
book: 100
book (100)演習完了