40. モジュールとインポート
#40. モジュールとインポート
モジュールは、コードの読み込み、名前空間の分離、再利用の Python の基本単位です。 CPython では、モジュールは言語レベルのオブジェクトであると同時に、インポート システムのランタイム レコードでもあります。
Python レベルでは、モジュールは以下を実行した後に得られるものです。python import math import os import json インポートされた各名前は、モジュール オブジェクト、パッケージ オブジェクト、関数、クラス、またはその他のエクスポートされたオブジェクトにバインドされます。 CPython レベルでは、インポートはバイトコード命令、インポート フック、モジュール仕様、ローダー、ファインダー、sys.modules、パッケージ パス、ファイル システム ルックアップ、バイトコード キャッシュ、インポート ロック、モジュールの実行。
インポート システムは、単純なファイル インクルード メカニズムではありません。ランタイムプロトコルです。
40.1 モジュールとは何か
モジュールは次のタイプのオブジェクトです。module。```python
import sys
print(type(sys))
print(sys.name)
出力:text
<class 'module'>
sys
モジュール オブジェクトは辞書を所有します。その辞書はモジュールのグローバル名前空間です。python
import math
print(math.dict["pi"]) print(math.dict["sqrt"])
という名前のファイルの場合、`config.py`:
```python
debug = True
port = 8080
def connect():
return port
```CPython はモジュール オブジェクトを作成し、その名前空間を準備し、その名前空間内でコンパイルされたコード オブジェクトを実行し、結果のバインディングをそのまま残します。`config.__dict__`。
概念的には:```text
module object
__dict__
"__name__" -> "config"
"__file__" -> ".../config.py"
"__spec__" -> ModuleSpec(...)
"debug" -> True
"port" -> 8080
"connect" -> function object
```したがって、モジュールは変更可能な名前空間オブジェクトです。
## 40.2 CPython のモジュール オブジェクト
CPython では、モジュール オブジェクトは次のように実装されます。`PyModuleObject`タイプ。
単純化されたメンタルモデルは次のとおりです。```c
typedef struct {
PyObject_HEAD
PyObject *md_dict;
PyObject *md_name;
PyObject *md_doc;
PyObject *md_state;
PyObject *md_weaklist;
PyModuleDef *md_def;
} PyModuleObject;
```正確なフィールドは変更される可能性がありますが、重要な点は変わりません。モジュールには関連する辞書とオプションの C レベルのモジュール定義/状態があります。
辞書には通常の Python 名が保存されます。 Python コードがモジュール内のグローバル名を評価するとき、CPython は通常、最初にそのモジュール ディクショナリを調べます。
例えば:```python
x = 10
def f():
return x
```機能`f`コピーしません`x`。関数オブジェクトを通じてモジュール グローバル ディクショナリへの参照を保存します。いつ`f`実行する`return x`、CPython が解決します`x`その辞書に対してグローバル検索を使用します。
## 40.3 インポートは実行です
Python モジュールをインポートすると、その最上位コードが実行されます。
のために`example.py`:
```python
print("loading example")
value = 42
def get_value():
return value
```最初のインポートではファイルが実行されます。```python
import example
```出力:```text
loading example
```通常、2 回目のインポートではファイルが再度実行されることはありません。```python
import example
```CPython は既存のモジュールを検索するため、出力は表示されません。`sys.modules`。
この動作は基本的なものです。モジュールのトップレベルコードが実行されるため、インポートには副作用があります。
優れたモジュールのトップレベル コードには、通常、定義と安価な初期化が含まれています。```python
CONSTANT = 100
def parse(text):
...
```危険なモジュールのトップレベル コードは、高価な作業や外部から見える作業を実行します。```python
connect_to_database()
delete_old_files()
start_threads()
make_network_request()
```このようなコードはインポート中、場合によってはアプリケーションが完全に初期化される前に実行されます。
## 40.4 インポートステートメント
声明:```python
import package.module
```は直接「このファイルを開く」という意味ではありません。
それは次のことを意味します:```text
resolve a module name
find a module specification
create or reuse a module object
initialize import-related attributes
execute the module if needed
bind a name in the caller's namespace
```声明:```python
import os.path
```通常は結合します`os`、 ない`os.path`、ローカル名前空間内:```python
import os.path
print(os)
print(os.path)
```声明:```python
from os import path
```バインドする`path`直接:```python
from os import path
print(path)
```声明:```python
from math import sqrt
```必要に応じてモジュールをインポートし、取得します`sqrt`そのモジュールから取得し、呼び出し元の名前空間にバインドします。
## インポート用の 40.5 バイトコード
CPython は import ステートメントをバイトコードにコンパイルします。
例:```python
import math
```コンパイルされたコードは、インポート関連のバイトコード命令を使用します。正確な命令シーケンスは Python のバージョンによって異なりますが、概念的には次のようになります。```text
load import machinery
import module named "math"
bind result to name "math"
```のために:```python
from math import sqrt
```バイトコードは概念的にこれを行います。```text
import module named "math"
load attribute "sqrt"
bind local/global name "sqrt"
```これを調べることができます`dis`:
```python
import dis
def f():
import math
return math.sqrt(9)
dis.dis(f)
```import ステートメントは、通常のバイトコード実行の一部です。個別のプリプロセッサ手順はありません。
##40.6`__import__`言語レベルでは、インポート ステートメントは最終的に、`builtins.__import__`。```python
import builtins
print(builtins.__import__)
```直接呼び出すことができます。```python
math_module = __import__("math")
print(math_module.sqrt(9))
```しかし、ほとんどのコードは呼び出すべきではありません`__import__`直接。使用`importlib.import_module`動的インポートの場合:```python
import importlib
mod = importlib.import_module("math")
print(mod.sqrt(9))
```機能`__import__`インポートは動的であるため、存在します。 Python コードは、実行時に文字列名によってモジュールをインポートできます。
##40.7`sys.modules`
`sys.modules`中央モジュールキャッシュです。
これは、完全修飾モジュール名をモジュール オブジェクトにマッピングする辞書です。```python
import sys
import math
print(sys.modules["math"] is math)
```出力:```text
True
```モジュールをロードする前に、インポート システムは次のチェックを行います。`sys.modules`。
概念的には:```python
if fullname in sys.modules:
return sys.modules[fullname]
else:
module = load_module(fullname)
sys.modules[fullname] = module
return module
```実際のプロセスは、パッケージ、循環インポート、失敗したインポート、ロック、ローダー プロトコルを処理する必要があるため、より慎重になります。
キーのプロパティは残ります。インポートはモジュール名によってキャッシュされます。
## 40.8 実行前にモジュールが挿入される理由
CPython は通常、モジュールを`sys.modules`コードを実行する前に。
これは循環インポートに必要です。
仮定する`a.py`含まれるもの:```python
import b
x = 1
```そして`b.py`含まれるもの:```python
import a
y = 2
```いつ`a`輸入品`b`、 そして`b`輸入品`a`、インポート システムは無限再帰を回避する必要があります。これは、部分的に初期化されたモジュール オブジェクトを`sys.modules`。
トレードオフとして、循環インポートでは不完全なモジュールが観察される可能性があります。
例:```python
# a.py
import b
x = 1
# b.py
import a
print(a.x)
```これは失敗する可能性があります。`a.x`まだ割り当てられていないとき`b`それを読みます。
循環輸入は禁止されていませんが、注意が必要です。通常の修正は、インポートを関数内に移動するか、共有定義を 3 番目のモジュールに移動するか、トップレベルの相互依存関係を回避することです。
## 40.9 モジュールの初期化シーケンス
Python ソース モジュールの簡略化されたインポート シーケンスは次のようになります。```text
1. Receive module name, such as "pkg.mod".
2. Check sys.modules.
3. Search sys.meta_path for a finder.
4. Finder returns a ModuleSpec.
5. Import machinery creates a module object.
6. Module is inserted into sys.modules.
7. Loader executes module code.
8. Import machinery returns the module object.
9. Import statement binds names in caller namespace.
```ソース ファイルの場合、実行とは次のことを意味します。```text
read source
decode source
compile source to code object
execute code object in module namespace
```拡張モジュールの場合、実行とはネイティブの初期化コードを呼び出すことを意味します。
組み込みモジュールの場合、実行には CPython にコンパイルされた組み込みの初期化ロジックが使用されます。
##40.10`ModuleSpec`最新の Python インポートは使用します`ModuleSpec`モジュールをロードする方法を記述するオブジェクト。
モジュール仕様には次のような情報が含まれます。```text
module name
loader
origin
package search locations
cached bytecode path
whether the module is a package
```モジュールの仕様を検査できます。```python
import json
print(json.__spec__)
print(json.__spec__.name)
print(json.__spec__.origin)
print(json.__spec__.loader)
print(json.__spec__.submodule_search_locations)
```通常のモジュールの場合、`submodule_search_locations`通常は`None`。
パッケージの場合、サブモジュールが見つかる可能性のあるパスが含まれます。
## 40.11 ファインダーとローダー
インポート システムは、検索と読み込みを分離します。
発見者はこう答えます。```text
Can this module name be found?
If yes, what spec describes it?
```ローダーは次のように答えます。```text
How should this module be created and executed?
```この分離により、Python はさまざまな場所からインポートできるようになります。```text
source files
bytecode files
built-in modules
extension modules
zip archives
namespace packages
custom import hooks
memory-backed module stores
remote systems, if a custom importer implements it
```標準インポート システムはプロトコルベースであるため拡張可能です。
##40.12`sys.meta_path`
`sys.meta_path`インポート システムの最初の主要なフック ポイントです。
ファインダーオブジェクトのリストです。各ファインダーは、モジュール名の処理方法を知っているかどうかを決定できます。```python
import sys
for finder in sys.meta_path:
print(finder)
```簡略化されたインポート検索では次のことが行われます。```python
for finder in sys.meta_path:
spec = finder.find_spec(fullname, path, target)
if spec is not None:
return spec
```典型的なエントリは以下を処理します。```text
built-in modules
frozen modules
path-based modules
```パスベースのファインダーは、ディレクトリおよびその他のパス エントリの検索を担当します。
##40.13`sys.path`
`sys.path`最上位モジュールのインポート検索場所のリストです。```python
import sys
for entry in sys.path:
print(entry)
```書くとき:```python
import mymodule
```そして`mymodule`が組み込まれていない、または凍結されている場合、パスベースのインポート システムはエントリを検索します。`sys.path`。
通常、エントリは次のとおりです。```text
directory of the running script
current working directory in interactive mode
standard library directories
site-packages directories
paths from PYTHONPATH
virtual environment paths
zip archives
```だからこそ変わるのです`sys.path`インポート動作を変更します。```python
import sys
sys.path.insert(0, "/custom/modules")
import mymodule
```これは、制御されたツールでは便利ですが、インポート動作が不安定になる可能性もあります。
## 40.14 パッケージ
パッケージは、サブモジュールを含めることができるモジュールです。
歴史的に、ディレクトリは、`__init__.py`ファイル:```text
pkg/
__init__.py
parser.py
lexer.py
```それから:```python
import pkg.parser
```負荷`pkg`まず、それから`pkg.parser`。
ファイル`pkg/__init__.py`パッケージがインポートされるときに実行されます。
例えば:```python
# pkg/__init__.py
print("loading package")
version = "1.0"
import pkg
print(pkg.version)
```パッケージは依然としてモジュール オブジェクトです。違いは、パッケージ検索場所があることです。
## 40.15 パッケージの属性
通常、パッケージはインポート関連の属性を定義します。```text
__name__
__package__
__path__
__spec__
__file__
__cached__
```重要なパッケージ固有の属性は次のとおりです。`__path__`。```python
import package
print(package.__path__)
__path__インポート システムに、そのパッケージ内のサブモジュールを検索する場所を指示します。
のために:python import package.submodule インポートシステムが検索しますpackage.__path__、トップレベルではありませんsys.path。
40.16 名前空間パッケージ
Python は名前空間パッケージをサポートしています。これらは何も含まれていないパッケージです。__init__.pyファイル。
名前空間パッケージは複数のディレクトリに分散できます。
例:```text dir1/ plugins/ alpha.py
dir2/
plugins/
beta.py
```両方の場合dir1そしてdir2オンですsys.path, Pythonで扱えるのはplugins名前空間パッケージとして。
そうすれば、両方とも機能する可能性があります。```python import plugins.alpha import plugins.beta
また、パッケージには複数の検索場所がある可能性があるため、インポート解決がより複雑になります。
## 40.17 絶対インポート
絶対インポートは、最上位のインポート名前空間から開始されます。```python
import package.module
from package import module
```パッケージ内では、引き続き次の名前の最上位パッケージが検索されます。`package`。
外部モジュールまたはトップレベルのモジュールを参照する場合は、明確にするために絶対インポートが推奨されます。
例:```python
from project.config import Settings
```これにより、インポートされた名前の由来が読者にわかります。
## 40.18 相対インポート
相対インポートは、現在のパッケージに対して解決されます。```python
from . import parser
from .lexer import tokenize
from ..config import Settings
```相対輸入は以下に依存します`__package__`。
これらは、モジュールがパッケージの一部として実行される場合にのみ機能します。パッケージ モジュールをスクリプトとして直接実行すると、相対的なインポートが中断される可能性があるのはこのためです。
例えば:```text
project/
app/
__init__.py
main.py
config.py
```内部`main.py`:
```python
from .config import Settings
```これは次のように実行すると機能します。```bash
python -m app.main
```次のように実行すると失敗する可能性があります。```bash
python app/main.py
```直接スクリプトを実行すると、モジュールの名前とパッケージ化の方法が変更されるためです。
##40.19`__main__`プログラムのエントリポイントとして実行されるモジュールの名前は次のとおりです。`__main__`。```python
print(__name__)
```スクリプトとして実行する場合:```bash
python script.py
```出力:```text
__main__
```インポート時:```python
import script
```モジュール名は次のとおりです。```text
script
```このため、Python プログラムでは一般的に次のものが使用されます。```python
def main():
...
if __name__ == "__main__":
main()
```ガードは、インポート中にプログラム エントリ コードが実行されるのを防ぎます。
## 40.20 によるモジュールの実行`-m`コマンド:```bash
python -m package.module
```ファイルパスではなくインポート名でモジュールを実行します。
モジュールが正しいパッケージ コンテキストを取得するため、これは重要です。
パッケージコードについては、以下を優先します。```bash
python -m package.module
```以上:```bash
python package/module.py
```の`-m`CPython はモジュールのパッケージを認識しているため、form では相対インポートが機能します。
## 40.21 バイトコード キャッシュ ファイル
CPython はコンパイルされたバイトコードをキャッシュする可能性があります`__pycache__`。
例:```text
package/
module.py
__pycache__/
module.cpython-312.pyc
```バイトコード キャッシュは、キャッシュが有効な場合、インポートのたびにソースを再コンパイルすることを回避します。
あ`.pyc`ファイルには以下が含まれます:```text
magic number
cache metadata
marshaled code object
```マジックナンバーはバイトコード形式のバージョンを識別します。 CPython がバイトコードを非互換的に変更すると、これが変化します。
CPython は、ファイルの作成方法に応じて、タイムスタンプ ベースまたはハッシュ ベースの無効化を使用してキャッシュの有効性をチェックします。
## 40.22 ソースモジュールとコードオブジェクト
通常の場合`.py`モジュールでは、ローダーがソースをコード オブジェクトにコンパイルします。
次に、モジュール ディクショナリ内のコード オブジェクトを実行します。
概念的には:```python
module = types.ModuleType("example")
code = compile(source_text, filename, "exec")
exec(code, module.__dict__)
```これは実際のモデルに近いですが、実際のインポート システムではさらに多くの詳細が処理されます。
重要な点は、モジュールの実行は、グローバル名前空間としてモジュール ディクショナリを使用した通常のコード実行であるということです。
## 40.23 内蔵モジュール
組み込みモジュールは、CPython 実行可能ファイルまたはリンクされたランタイムにコンパイルされます。
多くの場合、次のような例が挙げられます。```python
import sys
import builtins
import time
```組み込みモジュールの可用性は、プラットフォームとビルド構成によって異なります。
組み込みモジュールでは、`.py`ファイル。そのローダーは、C レベルの定義から初期化します。
組み込みモジュール名を検査できます。```python
import sys
print(sys.builtin_module_names)
```組み込みモジュールは、完全なファイルベースのインポート システムが利用可能になる前に必要なコア ランタイム サービスを提供します。
## 40.24 凍結されたモジュール
凍結モジュールは、凍結されたバイトコードまたは同等の静的データとして CPython バイナリに埋め込まれた Python モジュールです。
これらは、ファイル システム インポート システムが完全に動作する前にインポート機構をブートストラップするのに役立ちます。
これにより、ブートストラップの問題が発生します。```text
importlib implements imports
but importlib itself must be imported
so parts of importlib are frozen
```凍結モジュールは、通常のソース ファイルのインポートを行わずに、選択した Python コードを利用できるようにすることで、このサイクルを解決します。
## 40.25 拡張モジュール
拡張モジュールは、CPython にロードされるネイティブ共有ライブラリです。
Unix 系システムでは、これらは一般的に`.so`ファイル。 Windows では通常、`.pyd`ファイル。
インポートの例:```python
import _sqlite3
import _ssl
import _hashlib
```拡張モジュールは、CPython によって呼び出される初期化関数を提供します。最新の拡張モジュールは、可能な場合にはマルチフェーズ初期化を使用します。
拡張モジュールは CPython の C API ルールに従う必要があります。```text
create module object
define methods
manage reference ownership
set exceptions on failure
return initialized module
```拡張モジュールは Python プロセス内でネイティブ コードを実行するため、バグによりインタープリターがクラッシュする可能性があります。
## 40.26 単相および多相の初期化
古い拡張モジュールでは、多くの場合、単一フェーズの初期化が使用されます。初期化関数は、1 ステップでモジュール オブジェクトを作成して返します。
最新の拡張モジュールは、マルチフェーズ初期化を使用できます。このモデルでは、モジュールの作成と実行が分離されています。
これは、Python レベルのインポート セマンティクスとよりよく一致し、モジュールごとの状態をより明確にサポートします。
マルチフェーズ初期化は次の場合に重要です。```text
subinterpreter compatibility
module reloading behavior
cleaner module state
avoiding process-global mutable state
future isolation improvements
```すべての状態をグローバル C 変数に保存する C 拡張機能は、単純な場合には機能する可能性がありますが、サブインタプリタや初期化が繰り返されると動作が悪くなる可能性があります。
## 40.27 インポートロック
インポートにはロックが必要です。
ロックを行わないと、2 つのスレッドが同じモジュールを同時にインポートして初期化しようとする可能性があります。
インポート システムはロックを使用して、モジュールが複数のスレッドによって安全でない方法で同時に実行されないようにします。
これは、モジュールの初期化に副作用がある場合に特に重要です。```python
# database.py
connection_pool = create_pool()
```2 つのスレッドがロックせずにこのモジュールを同時にインポートした場合、重複したグローバル状態が作成されたり、部分的な初期化が発生したりする可能性があります。
インポート ロックは、このような多くの競合を防ぎます。
## 40.28 モジュールのリロード
Python は次を使用してモジュールをリロードできます`importlib.reload`。```python
import importlib
import config
importlib.reload(config)
```リロードすると、既存のモジュール オブジェクトを使用してモジュール コードが再実行されます。
これには微妙な影響があります。
仮定する`config.py`元々は以下が含まれています:```python
value = 1
```それを次のように編集した後、```python
value = 2
```リロード更新`config.value`。
ただし、他の場所にある既存の参照は依然として古いオブジェクトを指している可能性があります。```python
from config import value
import config
import importlib
importlib.reload(config)
print(value) # old binding
print(config.value) # new module attribute
```リロードは開発ツール、ノートブック、プラグイン システムに便利ですが、プロセス全体がリセットされるわけではありません。
## 40.29 インポートの副作用
インポートの副作用は、多くの場合、混乱を招く動作の原因となります。
このモジュールには目に見える副作用があります。```python
# noisy.py
print("imported noisy")
```このモジュールには隠れた副作用があります。```python
# registry.py
handlers = {}
def register(name, fn):
handlers[name] = fn
# plugin.py
from registry import register
def handle(x):
return x
register("plugin", handle)
```インポート中`plugin`変異する`registry.handlers`。
このパターンは、プラグイン システム、ORM、テスト フレームワーク、および Web フレームワークで一般的です。これは便利ですが、インポート順序がプログラムの動作の一部になることを意味します。
## 40.30 遅延インポート
遅延インポートでは、必要になるまでモジュールのインポートが遅延されます。
例:```python
def parse_json(text):
import json
return json.loads(text)
```これにより、起動時間を短縮したり、未使用のコード パス中のオプションの依存関係を回避したりできます。
ただし、遅延インポートにはトレードオフがあります。```text
errors appear later
first call may become slower
dependency structure becomes less visible
circular imports may be hidden rather than fixed
```遅延インポートは、意図的に使用すると便利です。これらは、不十分なモジュール構造に対するデフォルトの回避策になるべきではありません。
## 40.31 オプションのインポート
オプションのインポートは、特徴検出によく使用されます。```python
try:
import uvloop
except ImportError:
uvloop = None
```広範な例外処理には注意してください。これは多くの場合間違っています:```python
try:
import plugin
except Exception:
plugin = None
```本当のバグが内部に隠されている`plugin`。
キャッチを好む`ImportError`または`ModuleNotFoundError`必要に応じて、どのモジュールが失敗したかを確認します。```python
try:
import optional_backend
except ModuleNotFoundError as exc:
if exc.name != "optional_backend":
raise
optional_backend = None
```これにより、欠落している推移的な依存関係を隠すことがなくなります。
## 40.32 名前バインディングのインポート
インポート形式が異なれば、バインドされる名前も異なります。
|声明 |バインド名 |
|---|---|
|`import os` | `os` |
| `import os.path` | `os` |
| `import os.path as p` | `p` |
| `from os import path` | `path` |
| `from math import sqrt as s` | `s` |
| `from module import *`|たくさんの名前 |
インポート システムはモジュールをロードします。次に、import ステートメントは現在の名前空間に名前をバインドします。
これらは関連していますが、別個の操作です。
## 40.33 スターインポート
スターインポートは、エクスポートされた名前を現在の名前空間にコピーします。```python
from module import *
```モジュールが定義している場合`__all__`, Python はこれらの名前をインポートします。```python
__all__ = ["connect", "close"]
```それなし`__all__`, Python では、アンダースコアで始まらない名前がインポートされます。
スターインポートは、名前の由来がわかりにくくなるため、対話型セッションやパッケージファサードモジュール以外では通常推奨されません。
管理されたパッケージ ファサードでは、これらを慎重に使用できます。```python
# package/__init__.py
from .client import Client
from .errors import PackageError
__all__ = ["Client", "PackageError"]
```## 40.34 パッケージのファサード
パッケージはサブモジュールから名前を再エクスポートできます。```python
# library/__init__.py
from .client import Client
from .config import Config
__all__ = ["Client", "Config"]
```その後、ユーザーは次のように書くことができます。```python
from library import Client
```の代わりに:```python
from library.client import Client
```これにより API の人間工学は改善されますが、インポート コストが増加する可能性があります。もし`library.__init__`多くの重いサブモジュールをインポートし、`import library`高価になります。
パッケージのファサードが優れていると、利便性と初期コストのバランスが取れます。
## 40.35 インポートのパフォーマンス
コマンドライン ツール、サーバーのコールド スタート、テスト、および短時間実行されるスクリプトの場合、インポート時間は重要です。
輸入コストは以下から発生します。```text
file-system searches
source decoding
bytecode validation
compilation if cache is missing
module execution
transitive imports
native extension loading
top-level initialization
```以下を使用してインポートのタイミングを検査できます。```bash
python -X importtime -c "import your_package"
```これにより、インポートタイミングのツリーが出力されます。
インポートのパフォーマンスを向上させる一般的な方法:```text
avoid heavy top-level work
delay optional imports
reduce large dependency chains
avoid importing test-only modules at runtime
keep package __init__.py small
avoid broad convenience imports in hot paths
```## 40.36 インポートエラー
一般的なインポート関連の例外は次のとおりです。
|例外 |意味 |
|---|---|
|`ModuleNotFoundError`|要求されたモジュールが見つかりませんでした。
|`ImportError`|インポートは広範な理由で失敗しました |
|`AttributeError`|モジュールはロードされましたが、要求された属性が存在しません。
|`SyntaxError`|ソースモジュールをコンパイルできませんでした。
|ネイティブロードエラー |拡張モジュールのロードに失敗しました |
例:```python
from package import missing_name
```もし`package`存在しますが、`missing_name`そうでない場合、Python は発生する可能性があります`ImportError`。
例:```python
import missing_package
```通常、次のものが発生します。```text
ModuleNotFoundError
```インポート失敗診断では、以下を区別する必要があります。```text
the target module is missing
a transitive dependency is missing
the module exists but raised during execution
the requested exported name is missing
a native extension failed to load
```## 40.37 循環インポート
循環インポートは、トップレベルの実行中にモジュールが相互に依存する場合に発生します。
例:```python
# users.py
from posts import Post
class User:
...
# posts.py
from users import User
class Post:
...
```どちらかのモジュールが初期化を完了する前に、各モジュールがもう一方のモジュールを必要とするため、これは失敗する可能性があります。
一般的な修正:
1. 共有タイプを 3 番目のモジュールに移動します。```text
models/
base.py
users.py
posts.py
```2. 実行時のみの依存関係にはローカル インポートを使用します。```python
def create_post():
from posts import Post
return Post()
```3. 延期型アノテーションを使用します。```python
from __future__ import annotations
class User:
posts: list[Post]
```4. 具体的なモジュールではなくインターフェイスに依存します。
通常、最良の修正は構造的なものです。循環インポートでは、モジュールの境界が適切に選択されていないことがよくわかります。
## 40.38 インポートと型チェック
注釈がランタイム オブジェクトをインポートする場合、型ヒントによりインポート サイクルが作成される可能性があります。
よくあるパターンは次のとおりです。```python
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from posts import Post
class User:
def add_post(self, post: "Post") -> None:
...
TYPE_CHECKING実行時には false になるため、インポートは型チェッカーには表示されますが、実行中にはスキップされます。
これにより、型情報を維持しながら、実行時のインポート サイクルが短縮されます。
最新の Python は、いくつかのコンテキストで延期されたアノテーション評価もサポートしているため、入力のためのランタイム インポートがさらに削減されます。
40.39 インポートフック
インポート フックを使用すると、プログラムはインポート動作をカスタマイズできます。
カスタムファインダーを配置できますsys.meta_path。
カスタム ローダーは、非標準ソースからモジュールを作成および実行できます。
ユースケースには次のようなものがあります。text zip import plugin systems test isolation sandboxed module loading import tracing encrypted module stores remote module stores generated modules 最小限のファインダーのスケルトンは次のようになります。```python
class Finder:
def find_spec(self, fullname, path=None, target=None):
if fullname == "virtual_module":
...
return None
インポートフックは強力です。これらはプログラムの全体的な動作に影響を与えるため、範囲が狭く、予測可能である必要があります。
## 40.40`importlib`
`importlib`インポート システムへの標準ライブラリ インターフェイスです。
一般的な操作:```python
import importlib
mod = importlib.import_module("json")
mod = importlib.reload(mod)
```有用な下位レベルの部分には次のものがあります。```text
importlib.util.find_spec
importlib.util.module_from_spec
spec.loader.exec_module
importlib.machinery.PathFinder
importlib.machinery.SourceFileLoader
```手動ロードは次のようになります。```python
import importlib.util
spec = importlib.util.spec_from_file_location("custom_name", "/path/to/file.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
```これにより、特定のファイルからモジュールが作成および実行されます。
通常のアプリケーション コードの場合は、通常のインポートを優先します。手動 importlib ロードは、ツール、プラグイン システム、ローダー、またはランタイム モジュール システムを構築する場合にのみ使用してください。
## 40.41 モジュールのアイデンティティ
モジュール ID は次のキーに基づいています。`sys.modules`。
同じファイルが 2 つの異なる名前でインポートされる場合、CPython は 2 つの異なるモジュール オブジェクトを作成する可能性があります。
問題例:```text
project/
package/
__init__.py
settings.py
```プログラムの一部がインポートする場合:```python
import package.settings
```また、別のパス操作により次のような問題が発生します。```python
import settings
```その場合、同じファイルが異なる名前で 2 回ロードされる可能性があります。
これにより、モジュール グローバルが重複する可能性があります。```text
two registries
two singleton objects
two class identities
two caches
```これが直接的な理由の一つです`sys.path`操作は危険な場合があります。
## 40.42 モジュールグローバルは共有状態です
モジュール レベルの変数は、モジュールをインポートするすべてのコードで共有されます。```python
# state.py
count = 0
def increment():
global count
count += 1
return count
```すべてのインポーターは同じモジュール オブジェクトを監視します。```python
import state
state.increment()
state.increment()
```これは、定数、レジストリ、キャッシュ、シングルトンに役立ちます。また、インポート間で状態が保持されるため、テストが難しくなる可能性があります。
テストではモジュールの状態を明示的にリセットする必要がある場合があります。```python
import state
def test_increment():
state.count = 0
assert state.increment() == 1
```または、変更可能なモジュール グローバルを回避して動作を分離します。
## 40.43 インポート時の構成
モジュールはインポート時に設定を読み取ることがよくあります。```python
import os
DEBUG = os.environ.get("DEBUG") == "1"
```これにより、インポート時に構成が固定されます。あと環境が変わったら、`DEBUG`自動的には更新されません。
より柔軟な設計では、必要に応じて構成を読み取ります。```python
import os
def debug_enabled():
return os.environ.get("DEBUG") == "1"
```または設定の読み込みを一元化します。```python
class Settings:
def __init__(self):
self.debug = os.environ.get("DEBUG") == "1"
settings = Settings()
```インポート時の構成は単純ですが、テストや長時間実行されるプログラムに予期せぬ影響を与える可能性があります。
## 40.44 モジュールレベル`__getattr__`モジュールで定義できるのは、`__getattr__`欠落している名前の属性アクセスをカスタマイズします。```python
# package/__init__.py
def __getattr__(name):
if name == "heavy":
from . import heavy
return heavy
raise AttributeError(name)
```これにより、遅延エクスポートを実装できます。
それから:```python
import package
package.heavy
```要求された場合にのみ重いサブモジュールをインポートします。
モジュールレベル`__getattr__`互換性シム、非推奨、遅延読み込みに役立ちます。通常の属性アクセス動作が変更されるため、シンプルなままにする必要があります。
## 40.45 モジュールレベル`__dir__`モジュールで定義することもできます`__dir__`。```python
def __dir__():
return ["Client", "Config", "connect"]
```これにより、以下に表示される内容が制御されます。```python
dir(module)
```これは主に動的属性を持つモジュールに役立ちます。
## 40.46 起動時のインポートシステム
CPython の起動中、インポート システム自体を慎重に初期化する必要があります。
ランタイムには標準ライブラリをロードするのに十分なインポート機構が必要ですが、インポート システムの多くは Python で書かれています。
ブートストラップ シーケンスは、組み込みモジュールと凍結モジュールを使用して`importlib`。
概念的には:```text
initialize runtime
initialize builtins and sys
initialize frozen importlib bootstrap code
configure import machinery
initialize sys.path
load site if enabled
start executing user code
```これが、起動時の内部設定が通常のランタイム インポートよりも制約される理由です。
## 40.47`site`および環境設定
コアのインポート機構が初期化された後、CPython は通常、`site`モジュールを無効にしない限り`-S`。
の`site`モジュールは、サイト パッケージ ディレクトリを含む追加のインポート パスを構成します。
以下の処理も行う場合があります。```text
.pth files
user site-packages
virtual environment path adjustments
sitecustomize
usercustomize
```つまり、アプリケーション起動時のインポート環境は、インタープリター フラグ、仮想環境、インストール レイアウト、および環境変数に依存します。
## 40.48 仮想環境とインポート
Python がインストールされたパッケージを検索する仮想環境が変わります。
通常は次のように変更されます。```text
sys.prefix
sys.exec_prefix
site-packages paths
script entry points
```インタプリタ バイナリは共有またはコピーできますが、インポート環境は仮想環境のパッケージ ディレクトリを指します。
その理由は次のとおりです。```bash
python -m pip install requests
```仮想環境内で`requests`その環境内でのみインポート可能です。
輸入システム自体は同じです。検索パスが異なります。
## 40.49 ZIP ファイルからのインポート
アーカイブが有効であれば、Python は zip アーカイブからモジュールをインポートできます`sys.path`。
例:```bash
python app.zip
```または:```python
import sys
sys.path.insert(0, "modules.zip")
import mymodule
```Zip インポートでは、アーカイブ内のモジュール ファイルを検索する方法を認識しているインポーターを使用します。
これは、インポートがディレクトリベースのみではなくパスエントリベースである理由を示しています。あ`sys.path`エントリはカスタム パス フックで処理できます。
## 40.50 パスフックとパスインポーター
パスベースのインポートの場合、CPython はパスフックを使用して`sys.path`インポーターオブジェクトへのエントリー。
概念的には:```text
sys.path entry
↓
sys.path_hooks
↓
path importer
↓
find module spec
```キャッシュ`sys.path_importer_cache`パスエントリのインポーターオブジェクトを保存します。```python
import sys
print(sys.path_hooks)
print(sys.path_importer_cache)
```これにより、インポーター オブジェクトを繰り返し再構築することがなくなります。
## 40.51 インポートのセキュリティ上の懸念
インポートはパスを検索します。そのため、パスの順序はセキュリティに依存します。
現在のディレクトリが標準ライブラリの前にある場合、ローカル ファイルは標準モジュールをシャドウすることができます。
例:```text
project/
json.py
```それから:```python
import json
```ローカルをインポートする場合があります`json.py`標準ライブラリの代わりに`json`。
これにより、バグやセキュリティ上の問題が発生する可能性があります。
守備の練習:```text
avoid naming files after standard library modules
avoid unsafe sys.path insertion
run applications from expected working directories
use virtual environments
inspect module.__file__ when debugging
prefer python -m package.module for package code
```インポートされたものを検査するには:```python
import json
print(json.__file__)
```## 40.52 インポートとテスト
テストではインポート動作に重点を置くことがよくあります。
一般的なテストの問題は次のとおりです。```text
tests depend on working directory
local files shadow installed packages
package imported twice under different names
module global state leaks between tests
environment variables read at import time
plugins register themselves during import
```堅牢なテスト設定では、ユーザーと同じ方法でパッケージをインポートします。
インストールされたパッケージの動作をテストすることを優先します。```bash
python -m pytest
```偶然のパスレイアウトに依存するのではなく、クリーンな環境から。
## 40.53 インポートとアプリケーションの設計
優れた Python アプリケーション構造により、インポートの複雑さが軽減されます。
一般的なレイアウト:```text
project/
pyproject.toml
src/
app/
__init__.py
main.py
config.py
service.py
storage.py
tests/
test_service.py
```の`src`レイアウトは、リポジトリ ルートからの誤ったインポートを検出するのに役立ちます。
きれいなモジュール依存関係グラフは内側を向いています。```text
main
depends on service
service
depends on storage and config
storage
depends on database driver
config
depends on environment parsing
```低レベルのモジュールが高レベルのアプリケーション エントリ ポイントをインポートする設計は避けてください。
## 40.54 インポートとパブリック API の設計
パッケージのインポート サーフェスは、そのパブリック API の一部です。
例えば:```python
from library import Client
```文書化されていれば公的契約となります。
パッケージ ファサードがパブリック インポートを保持している場合、内部モジュールの場所を変更してもユーザーが中断されることはありません。```python
# library/__init__.py
from ._client import Client
__all__ = ["Client"]
```プライベート モジュールでは、先頭にアンダースコアが使用されることがよくあります。```text
library/
__init__.py
_client.py
_protocol.py
public.py
```これは規約であり、アクセス制限ではありません。
## 40.55 インポートデバッグチェックリスト
インポート動作がわかりにくい場合は、次の値を調べてください。```python
import sys
import module
print(module)
print(module.__name__)
print(getattr(module, "__file__", None))
print(getattr(module, "__spec__", None))
print(getattr(module, "__package__", None))
print(sys.path)
```パッケージの問題の場合:```python
import package
print(package.__path__)
print(package.__spec__.submodule_search_locations)
```キャッシュの問題の場合:```python
import sys
print(sys.modules.get("module_name"))
```タイミングについて:```bash
python -X importtime -c "import module_name"
```直接解決の場合:```python
import importlib.util
print(importlib.util.find_spec("module_name"))
```## 40.56 最小限のインポートアルゴリズム
簡略化されたインポート関数は次のように記述できます。```python
def import_module(fullname):
if fullname in sys.modules:
return sys.modules[fullname]
spec = find_spec(fullname)
if spec is None:
raise ModuleNotFoundError(fullname)
module = module_from_spec(spec)
sys.modules[fullname] = module
try:
spec.loader.exec_module(module)
except Exception:
del sys.modules[fullname]
raise
return module
```実際の CPython インポート システムはさらに複雑ですが、このスケルトンは中心的なフローを捉えています。```text
cache lookup
spec discovery
module creation
cache insertion
module execution
error cleanup
return module
```## 40.57 一般的な障害: モジュールが部分的に初期化されました
一般的なエラーは次のようになります。```text
AttributeError: partially initialized module 'x' has no attribute 'y'
```これは多くの場合、循環インポートまたはモジュール シャドウイングの問題を意味します。
循環インポートの例:```python
# a.py
import b
class A:
...
# b.py
import a
class B(a.A):
...
```いつ`b`読む`a.A`、モジュール`a`に存在します`sys.modules`、しかしそのクラス`A`まだ定義されていません。
修正は通常、トップレベルの実行中にクラス定義が相互にインポートする必要がないようにモジュールを再構築することです。
## 40.58 よくある失敗: シャドウイング
ファイルに標準ライブラリ モジュールの名前が付けられている場合、インポートによって間違ったファイルが解決される可能性があります。
例:```text
random.py
```その中に:```python
import random
```これは、標準ライブラリの代わりにそれ自体をインポートする可能性があります`random`。
症状には次のようなものがあります。```text
partially initialized module
missing expected attributes
recursive import behavior
strange module.__file__
```チェック:```python
import random
print(random.__file__)
```必要に応じて、ローカル ファイルの名前を変更し、古いキャッシュ ファイルを削除します。
## 40.59 よくある失敗: パッケージ ファイルを直接実行する
与えられる:```text
app/
__init__.py
main.py
config.py
```内部`main.py`:
```python
from .config import Settings
```これは失敗する可能性があります:```bash
python app/main.py
```なぜなら`main.py`として実行されます`__main__`としてではなく`app.main`。
使用:```bash
python -m app.main
```を含むディレクトリから`app`。
これにより、パッケージのコンテキストが保持され、相対インポートが機能します。
## 40.60 重要なポイント
モジュールは、名前空間辞書を持つランタイム オブジェクトです。
モジュールをインポートすると、その最上位コードがモジュール名ごとに 1 回実行されます。`sys.modules`。
インポート システムは、ファインダー、ローダー、モジュール仕様、パス フック、キャッシュを中心に構築されています。
パッケージは、サブモジュールの検索場所を持つモジュールです。
CPython はモジュールを挿入するため、循環インポートでは部分的に初期化されたモジュールが公開されます。`sys.modules`処刑前。
インポート システムはプログラム可能です。`importlib`、`sys.meta_path`、パスフック、ローダー。
インポートの問題のほとんどは、循環依存関係、パス シャドウイング、パッケージ ファイルの直接実行、インポート時の副作用、またはモジュール ID の重複によって発生します。