Pythonのimport内部実装 〜複雑怪奇なimportlib内部実装へようこそ〜

[Pythonその2 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/python2) 8日目の記事です。よろしくおねがいします。

# 記事の趣旨と対象読者

Pythonのプログラムを書くときに必ず登場するのが、以下のような**import文**です。

```py
import numpy as np
```

一見、JavaScript ES Modulesにおけるimport文

```js
import randomSquare from './modules/square.js';
```

に似ているようにも思われるのですが、実はPythonにおける`import`文は、Pythonがインタプリタ言語であることと相まって、他の言語と比べて非常に複雑な動作をします。その仕組みを、CPythonインタプリタや標準ライブラリのソースコードまで遡って調べるのがこの記事の趣旨です。

この記事は以下のような人には役立つかもしれません。

- CPythonの内部実装に興味がある人
- プログラムの動作を詳しく調べる方法を知りたい人
- Python標準のモジュールローダを上書きして、独自のモジュールローダを実装したい人

また、以下のキーワードについても記事中で説明します。

`import`文, `__file__`, `_import()`, `sys.path`, `sys.path_hooks`, `sys.meta_path`, `sys.sys.path_importer_cache`

**注意** 本文中に掲載するコードは、わかりやすさのため変更を加えています。

# 解説の流れ

本稿では、実際のCPythonでのimport文の実行の様子を、以下の流れで解説します。

- CPythonのコンパイル部分(ソースコードからバイトコードの生成)
- CPythonのバイトコード実行部分
- Pythonで記述された関数`__import__`の実行部分
- `__import__`がモジュール名解決のために使用するFinder, Spec, Locatorについて

# `import`文

Pythonでは、[`import`文](https://docs.python.org/ja/3.6/reference/simple_stmts.html#import)を用いることにより、モジュールを読み込むことができます。

```py
import my_module
```

モジュールの多くはファイルを実体に持ちます。(ただし以下に述べるように例外もあります。)例えば上の例では、現在のファイルと同じディレクトリ(REPL環境の場合は作業ディレクトリ)又は`sys.path`に示されたディレクトリに`my_module.py`というファイルがあるか、`my_module`というディレクトリがありその中に`__init__.py`というファイルがあります[^1]。以下のように確認できます[^2]。

```py
import my_module
print(my_module.__file__)
# "/path/to/my_module.py" もしくは "/path/to/my_module/__init__.py"
```

なお、モジュールがファイル`my_module/__init__.py`で定義されている場合は、パッケージと呼ばれます[^3]。その場合、ディレクトリ`my_module/`内にファイル`my_sub.py`を配置することで、サブモジュールとして扱うことができます。

```py
# import my_module は書かなくても自動で実行される
import my_module.my_sub
print(my_module.__path__) # "/path/to/my_module"
print(my_module.__file__) # "/path/to/my_module/__init__.py"
print(my_module.my_sub.__file__) # "/path/to/my_module/my_sub.py"
```

コマンドラインから直接実行されるモジュールは`"__main__"`という名前になります。一度読み込まれたモジュールは`sys.modules`に辞書形式で格納されるため、以下のようにすればコマンドラインから実行したファイル名を取得できます。

```py
import sys
print(sys.modules['__main__'].__file__)
```

# import文の内部実装

## バイトコードの生成

`import`文はCPythonによってどのように実行されるのでしょうか。
まず、CPythonは入力されたPythonスクリプトを抽象構文木に変換します。その後、抽象構文木を解釈しPythonの内部バイトコードに置き換えます。
[Python/compile.c:compiler_import()](https://github.com/python/cpython/blob/d112c600ab3f5e2910345bcc4bfc39439917ed87/Python/compile.c#L3171)において、`import`文を表現する抽象構文木からバイトコードを生成しています。

**注意** 実際は、`compiler_import`以外にも、`compiler_from_import`が存在します(それぞれ、`import x`, `from x import y`に相当)。ここでは`compiler_import`のみを考えます。

```Python/compiler.c
static int compiler_import(struct compiler *c, stmt_ty s)
{
    Py_ssize_t i, n = asdl_seq_LEN(s->v.Import.names);
    for (i = 0; i < n; i++) {
        alias_ty alias = (alias_ty)asdl_seq_GET(s->v.Import.names, i);
        int r;
        ADDOP_LOAD_CONST(c, _PyLong_Zero);
        ADDOP_LOAD_CONST(c, Py_None);
        ADDOP_NAME(c, IMPORT_NAME, alias->name, names);
        if (alias->asname) {
            ...
        }
        else {
            identifier tmp = alias->name;
            Py_ssize_t dot = PyUnicode_FindChar(
                alias->name, '.', 0, PyUnicode_GET_LENGTH(alias->name), 1);
            if (dot != -1) {
                tmp = PyUnicode_Substring(alias->name, 0, dot);
                ...
            }
            r = compiler_nameop(c, tmp, Store);
            ...
        }
    }
    return 1;
}
```

`stmt_ty *s`はInclude/Python-ast.hで宣言された構造体で、構文木を表します。`s`が`import`文を表す場合、`s->Import`に構文木の情報が入力されています。

```Include/Python-ast.h
typedef struct _stmt *stmt_ty;
struct _stmt {
    enum _stmt_kind kind;
    union {
        ...
        struct {
            asdl_seq *names;
        } Import;
    } v;
};
...
typedef struct _alias *alias_ty;
struct _alias {
    identifier name;
    identifier asname;
};
```

```Include/asdl.h
typedef PyObject * identifier;
```

`s->v.Import.names`が構造体`alias_ty`のリストになっており、`alias_ty`は`name`及び`asname`の組です。すなわち、`import name1 as asname1, name2 as asname2, name3`という文は

```c
struct _alias { name: "name1", asname: "asname1" },
struct _alias { name: "name2", asname: "asname2" },
struct _alias { name: "name3", asname: NULL }
```

のような配列として表現されています。これらの個々の要素を順に`asdl_seq_GET()`を用いて取得します。
次に、`ADDOP_LOAD_CONST()`を用いて定数を2つ生成しています(これらはそれぞれ`IMPORT_NAME`命令に渡される`level`, `fromlist`を表しています)。また`ADDOP_NAME()`を用いて`IMPORT_NAME`命令を生成します。さらに、`name`が`.`を含んでいる場合、最初の部分のみを取り出します。これが、読み込んだモジュールを格納するための変数名となります。最後に`compiler_nameop()`を呼び出し、変数を登録(普通は`STORE_GLOBAL`命令の生成)しています。

## `IMPORT_NAME`命令の実行

CPythonの内部バイトコードが生成されると、CPythonはこれを実行器`Python/ceval.c`を用いて実行します。

```Python/eval.c
PyObject*
_PyEval_EvalFrameDefault(PyThreadState *tstate, PyFrameObject *f, int throwflag)
{
    ...
main_loop:
    for (;;) {
        ...
    dispatch_opcode:
        switch (opcode) {
            ...
        case TARGET(IMPORT_NAME): {
            PyObject *name = GETITEM(names, oparg);
            PyObject *fromlist = POP();
            PyObject *level = TOP();
            PyObject *res;
            res = import_name(tstate, f, name, fromlist, level);
            Py_DECREF(level);
            Py_DECREF(fromlist);
            SET_TOP(res);
            if (res == NULL)
                goto error;
            DISPATCH();
        }
            ...
        }
        ...
    }
    ...
}
...
static PyObject *
import_name(PyThreadState *tstate, PyFrameObject *f,
            PyObject *name, PyObject *fromlist, PyObject *level)
{
    PyObject *import_func, *res;
    PyObject* stack[5];
    import_func = _PyDict_GetItemIdWithError(f->f_builtins, &PyId___import__);
    ...
    Py_INCREF(import_func);
    stack[0] = name;
    stack[1] = f->f_globals;
    stack[2] = f->f_locals == NULL ? Py_None : f->f_locals;
    stack[3] = fromlist;
    stack[4] = level;
    res = _PyObject_FastCall(import_func, stack, 5);
    Py_DECREF(import_func);
    return res;
}
```

CPythonにおける実行器の実体は`_PyEval_EvalFrameDefault()`で、これは無限ループの中に巨大な`switch`文が入っているような構造になっています。その中で`IMPORT_NAME`命令に関係する部分を抜き出しました。

まず、`GETITEM()`および`POP()`を用いて、構文解析で生成した引数`name`, `fromlist`, `level`を取り出します。これらを用いて[import_name()](https://github.com/python/cpython/blob/50e6e991781db761c496561a995541ca8d83ff87/Python/ceval.c#L5074)を実行します。`import_name()`では、まず`_PyDict_GetItemIdWithError()`によってPythonで書かれた関数`__import__()`を取得しています。次に配列`stack[]`を用いて`__import__()`に渡す引数を用意し、`_PyObject_FastCall()`を用いて`__import__()`を実行します。

## Pythonで書かれた部分: `__import__()`

`import`文を実行すると、結局はPythonで記述された`__import__()`が実行されることがわかりました。以下ではこの先の処理がどうなっているか見てみます。

### 注意

`__import__()`の実体は[Lib/importlib/_bootstrap.py](https://github.com/python/cpython/blob/79d1c2e6c9d1bc1cf41ec3041801ca1a2b9a995b/Lib/importlib/_bootstrap.py#L1073)にありますが、実際にはこのファイルが直接実行されるわけではなく、実際にはCPythonのバイトコードに変換された`_frozen_importlib/__import__()`が実行されます。そのためLib/importlib/\_bootstrap.pyやLib/importlib/\_bootstrap_external.pyを変更した場合は、

```shell
$ make Programs/_freeze_importlib
$ ./Programs/_freeze_importlib importlib_bootstrap Lib/importlib/_bootstrap.py Python/importlib.h
$ ./Programs/_freeze_importlib importlib_bootstrap_external Lib/importlib/_bootstrap_external.py Python/importlib_external.h
```

等として、`Python/importlib.h`, `Python/importlib_external.h`を手動で再生成した上で、

```shell
$ make
```

を実行してpythonコマンドを再ビルドする必要があります。

### `__import__()`関数のフォーマット

`__import__()`は以下のように定義されています。

```Lib/importlib/_bootstrap.py
def __import__(name, globals=None, locals=None, fromlist=(), level=0):
```

| 引数名 | `import`文から呼ばれたときの値 |
|:-:|:-:|
| name | 読み込むモジュール名 |
| globals | 現在のフレームのグローバルオブジェクト(現在のモジュール) |
| locals | 現在のフレームのローカルオブジェクト |
| fromlist | None |
| level | 0 |

- name ... 
読み込むモジュール名(例えば`pub.sub`や`..a.b.c`など)を指定します。例えば`pub.sub`を与えた場合、`__import__()`はモジュール`pub`を読み込み、モジュール`pub.sub`を読み込み、モジュール`pub.sub`をモジュール`pub`の`sub`属性として追加し、モジュール`pub`を返します。
- globals ... 相対的なモジュール名を指定した場合の起点となるモジュール。
- locals ... 無視されます。
- fromlist ... `from-import`文で使われる、読み込むべき属性のリストです。
- level ... `globals`で指定したモジュールを起点として何個親に戻るか。

例えば、`__main__`モジュールから`import test.test2`を実行したときは以下のように展開されます(`locals`は考えない)。

```py
test = __import__("test.test2", sys.modules["__main__"], None, None, 0) 
```

### `__import__`の処理の中身

```Lib/importlib/_bootstrap.py
def __import__(name, globals=None, locals=None, fromlist=(), level=0):
    package = globals['__name__']
    module = _gcd_import(name, package, level)
    if not fromlist:
        if level == 0:
            return _gcd_import(name.partition('.')[0])
        else:
            cut_off = len(name) - len(name.partition('.')[0])
            return sys.modules[module.__name__[:len(module.__name__)-cut_off]]
    else:
        ...
```

まず、相対的なモジュール名の起点となるパッケージの名前を`package`に取得します。そして`_gcd_import()`を用いてパッケージを読み込みます。
`__import__()`で重要な点は、読み込んだパッケージをそのまま返すわけではない、ということです。例えば、モジュール`pub.sub`を`__import__()`で読み込む場合、パッケージ`pub`とモジュール`pub.sub`を両方読み込み、パッケージ`pub`を表すPyObjectを返します。この処理を`if not fromlist:`の中で行っています。すなわち、`level = 0`の時、モジュール`pub.sub`のロードは以下のように行われます。

```py
_gcd_import("pub.sub", package, 0) # 戻り値はモジュール`pub.sub`
return _gcd_import("pub", package, 0) # 戻り値はパッケージ`pub`
```

1度の`__import__()`の呼び出しで`_gcd_import()`を2回実行しているため、非効率に思えます。実際、1回目の`_gcd_import()`の呼び出しで、パッケージ`pub`とモジュール`pub.sub`の読み込みが行われます。しかしながら一度読み込まれたモジュールは`sys.modules[]`にキャッシュされるため、2回目の呼び出しではこのキャッシュから取得されるだけです。

```Lib/importlib/_bootstrap.py
def _gcd_import(name, package=None, level=0):
    ...
    if level > 0:
        name = _resolve_name(name, package, level)
    return _find_and_load(name)

def _resolve_name(name, package, level):
    bits = package.rsplit('.', level - 1)
    ...
    return '{}.{}'.format(bits[0], name)

def _find_and_load(name):
    ...
    module = sys.modules.get(name, _NEEDS_LOADING)
    if module is _NEEDS_LOADING:
        return _find_and_load_unlocked(name)
    ...
    return module

def _find_and_load_unlocked(name):
    path = None
    parent = name.rpartition('.')[0]
    if parent:
        if parent not in sys.modules:
            _gcd_import(parent)
        if name in sys.modules:
            return sys.modules[name]
        parent_module = sys.modules[parent]
        path = parent_module.__path__
    spec = _find_spec(name, path)
    module = _load_unlocked(spec)
    if parent:
        parent_module = sys.modules[parent]
        child = name.rpartition('.')[2]
        setattr(parent_module, child, module)
    return module
```

`_gcd_import()`ではまず`resolve_path()`を用いて`name`, `package`, `level`から読み込むべきモジュール名`name`を求めます。次に`_find_and_load()`が呼びだされ、求められたモジュールがすでに`sys.modules`に存在しない場合は、`_find_and_load_unlocked()`が呼び出されます。

`_find_and_load_unlocked()`では、まず親パッケージが読み込まれたいない場合は`_gcd_import()`を再帰的に呼び出して読み込みます。そして`_find_spec()`及び`_load_unlocked()`を呼び出してモジュールを取得します。ここで、`__path__`はファイルシステム上のディレクトリのパスです(パッケージは必ず`__path__`を持つ)。最後に親パッケージにパッケージ名の属性を追加し、モジュールを返します。

```Lib/importlib/_bootstrap.py
def _find_spec(name, path, target=None):
    ...
    for finder in sys.meta_path:
        spec = finder.find_spec(name, path, target)
        if spec is not None:
            ...
            return spec
    ...
```

`_find_spec()`では、`sys.meta_path`を参照しています。後で述べるように、`sys.meta_path`はfinderオブジェクトの配列です。`_find_spec()`では、`sys.meta_path`の各要素に順に`find_spec()`を実行し、`spec`を取得します。`spec`がNoneである場合は次のfinderを参照します。

```Lib/importlib/_bootstrap.py
def _load_unlocked(spec):
    module = module_from_spec(spec)
    sys.modules[spec.name] = module
    if spec.loader is None:
        # A namespace package so do nothing.
    else:
        spec.loader.exec_module(module)
    return module

def module_from_spec(spec):
    module = None
    if hasattr(spec.loader, 'create_module'):
        module = spec.loader.create_module(spec)
    ...
    if module is None:
        module = _new_module(spec.name)
    _init_module_attrs(spec, module)
    return module

def _new_module(name):
    return type(sys)(name)

def _init_module_attrs(spec, module):
    if getattr(module, '__name__', None) is None:
        module.__name__ = spec.name
    if getattr(module, '__loader__', None) is None:
        loader = spec.loader
    if getattr(module, '__package__', None) is None:
        module.__package__ = spec.parent
    module.__spec__ = spec
    if getattr(module, '__path__', None) is None:
        if spec.submodule_search_locations is not None:
            module.__path__ = spec.submodule_search_locations
    if spec.has_location:
        if getattr(module, '__file__', None) is None:
            module.__file__ = spec.origin
        if getattr(module, '__cached__', None) is None:
            if spec.cached is not None:
                module.__cached__ = spec.cached
    return module
```

`_load_unlocked()`では`module_from_spec()`を呼び出してモジュールを生成します。その後`sys.modules`にモジュールを追加し、`spec.loader.exec_module()`を実行します。
`module_from_spec()`では、`spec.loader.create_module()`が存在する場合はそれを実行し、存在しない場合は`spec.name`を名前に持つオブジェクトを作成しモジュールを得ます。その後`_init_module_attrs()`を実行してモジュールに必要な属性をセットします。

## Finder, Spec, Loader

ここまでのまとめとして、Finder, Spec, Loaderに求められる要件を述べておきます。

### Finder

`sys.meta_path`に登録されるオブジェクト。Specを返す関数`finder.find_spec()`を持つ。

### Spec

`finder.find_spec()`によって返されるオブジェクトであり、読み込むべきモジュールと対応している。通常はLoaderクラスのインスタンスである。

- `spec.name` : 対応するモジュールの名前。
- `spec.submodule_search_locations` : specがパッケージに対応する場合、パッケージの存在するディレクトリ。
- `spec.origin` : 対応するモジュールのファイル名。
- `spec.parent` : 親のパッケージ名。
- `spec.loader` : 後述(名前空間パッケージではNone)

### Loader

`spec.loader`に格納されるオブジェクトであり、関数`create_module()`をもつ。

# Finderの実装

前章で、配列`sys.meta_path`が重要な役割を果たしていることがわかりました。
CPythonのREPL環境で`sys.meta_path`を確認してみます。(通常のスクリプトでも同様の結果になります)

```py
>>> import sys
>>> sys.meta_path
[<class '_frozen_importlib.BuiltinImporter'>, <class '_frozen_importlib.FrozenImporter'>, <class '_frozen_importlib_external.PathFinder'>]
```

すなわち、`sys.meta_path`は

- BuiltinImporter
- FrozenImporter
- PathFinder

という3つのFinderから構成されていることがわかります。

## `BuiltinImporter`

名前のとおり、組み込みパッケージを探すFinderです。

### 組み込みパッケージとは
組み込みパッケージはC言語で記述され、CPythonのバイナリに含まれています。

### `find_spec()`

`BuiltinImporter`の`find_spec()`は以下のようになっています。

```Lib/importlib/_bootstrap.py
class BuiltinImporter:
    @classmethod
    def find_spec(cls, fullname, path=None, target=None):
        if path is not None:
            return None
        if _imp.is_builtin(fullname):
            return ModuleSpec(fullname, cls, origin="built-in", is_package=False)
        else:
            return None

    @classmethod
    def create_module(self, spec):
        return _imp.create_builtin(spec)

    @classmethod
    def exec_module(self, module):
        _imp.exec_builtin(module)

class ModuleSpec:
    def __init__(self, name, loader, *, origin=None, loader_state=None,
                 is_package=None):
        self.name = name
        self.loader = loader
        self.origin = origin
        self.loader_state = loader_state
        self.submodule_search_locations = [] if is_package else None
        # file-location attributes
        self._set_fileattr = False
        self._cached = None
```

例えば`spec = BuiltinImporter.find_spec("_imp")`によって生成されるSpecオブジェクトは以下に示すものになります。

```py
ModuleSpec{name: "_imp", loader: <BuiltinImporter>, origin: "built-in"}
```

`BuiltinImporter`の`find_spec`によってつくられたspecに対して`create_module()`を実行すると、`_imp.create_builtin()`が実行されます。

```Modules/config.c
struct _inittab _PyImport_Inittab[] = {
    // ... (外部モジュールの定義)
    {"marshal", PyMarshal_Init},
    {"_imp", PyInit__imp},
    {"_ast", PyInit__ast},
    {"builtins", NULL},
    {"sys", NULL},
    {"gc", PyInit_gc},
    {"_warnings", _PyWarnings_Init},
    {"_string", PyInit__string},
    {0, 0}
};
```

```Python/import.c
static PyObject *
_imp_create_builtin(PyObject *module, PyObject *spec)
{
    PyThreadState *tstate = _PyThreadState_GET();
    struct _inittab *p;
    PyObject *name;
    const char *namestr;
    PyObject *mod;
    name = PyObject_GetAttrString(spec, "name");
    namestr = PyUnicode_AsUTF8(name);
    for (p = PyImport_Inittab; p->name != NULL; p++) {
        PyModuleDef *def;
        if (_PyUnicode_EqualToASCIIString(name, p->name)) {
            if (p->initfunc == NULL) {
                /* 'sys' 及び 'builtin'の場合はsys.modulesから取得して返す */
            }
            mod = (*p->initfunc)();
            // ...
            return mod;
        }
    }
}

PyMODINIT_FUNC
PyInit__imp(void)
{
    PyObject *m = PyModule_Create(&impmodule);
    PyObject *d = PyModule_GetDict(m);
    const wchar_t *mode = _PyInterpreterState_GET_UNSAFE()->config.check_hash_pycs_mode;
    PyObject *pyc_mode = PyUnicode_FromWideChar(mode, -1);
    PyDict_SetItemString(d, "check_hash_based_pycs", pyc_mode) < 0)
    Py_DECREF(pyc_mode);j
    return m;
}

static struct PyModuleDef impmodule = {
    PyModuleDef_HEAD_INIT,
    "_imp",
    "(Extremely) low-level import machinery bits as used by importlib and imp.",
    0,
    imp_methods,
    NULL,
    NULL,
    NULL,
    NULL
};

static PyMethodDef imp_methods[] = {
    {"extension_suffixes", (PyCFunction)_imp_extension_suffixes, METH_NOARGS, _imp_extension_suffixes__doc__},
    {"lock_held", (PyCFunction)_imp_lock_held, METH_NOARGS, _imp_lock_held__doc__},
    {"acquire_lock", (PyCFunction)_imp_acquire_lock, METH_NOARGS, _imp_acquire_lock__doc__},
    {"release_lock", (PyCFunction)_imp_release_lock, METH_NOARGS, _imp_release_lock__doc__},
    {"get_frozen_object", (PyCFunction)_imp_get_frozen_object, METH_O, _imp_get_frozen_object__doc__},
    {"is_frozen_package", (PyCFunction)_imp_is_frozen_package, METH_O, _imp_is_frozen_package__doc__},
    {"create_builtin", (PyCFunction)_imp_create_builtin, METH_O, _imp_create_builtin__doc__},
    {"init_frozen", (PyCFunction)_imp_init_frozen, METH_O, _imp_init_frozen__doc__},
    {"is_builtin", (PyCFunction)_imp_is_builtin, METH_O, _imp_is_builtin__doc__},
    {"is_frozen", (PyCFunction)_imp_is_frozen, METH_O, _imp_is_frozen__doc__},
    {"exec_builtin", (PyCFunction)_imp_exec_builtin, METH_O, _imp_exec_builtin__doc__},
    {"_fix_co_filename", (PyCFunction)(void(*)(void))_imp__fix_co_filename, METH_FASTCALL, _imp__fix_co_filename__doc__},
    {"source_hash", (PyCFunction)(void(*)(void))_imp_source_hash, METH_FASTCALL|METH_KEYWORDS, _imp_source_hash__doc__},
    {NULL, NULL}
};
```

`_imp_create_builtin()`では`PyImport_Inittab[]`を参照し、読み込みたいモジュール名に対応する`initfunc()`を実行します。例えば、`_imp`モジュールに対応するinitfuncは`PyInit__imp()`で、これを実行すると`imp_methods[]`に含まれる関数を持つモジュールが作成されます。また`check_hash_based_pycs`という属性を追加しています。

## `FrozenImporter`

名前のとおり、FrozenPackageを探すFinderです。

### FrozenPackageとは
FrozenPackageは主にPythonで記述されていますが、高速化のためCPythonのバイトコードに変換された上でCPythonのバイナリにバンドルされています(変換は`Programs/freeze_importlib.c`を用いて行われます)。以下にFrozenPackageの一覧を示します。(Python/frozen.cを参照) 

| FrozenPackage名 | バイトコードを含むファイル | 変換元のPythonスクリプト |
|:-:|:-:|:-:|
| _frozen_importlib  | `Python/importlib.h` |  `Lib/importlib/_bootstrap.py` |
|  `_frozen_importlib_external` | `Python/importlib_external.h`  | `Lib/importlib/_bootstrap_external.py` |
| `zipimport`  |  `Python/importlib_zipimport.h` | `Lib/zipimport.py` |
| `__hello__`  | `Python/frozen.c:M___hello__` | `Tools/freeze/flag.py` (手動で変換) |
| `__phello__`  | `__hello__`と同じ |  |
| `__phello__.spam`  | `__hello__`と同じ |   |

### 実装

```Lib/importlib/_bootstrap.py
class FrozenImporter:
    @classmethod
    def find_spec(cls, fullname, path=none, target=none):
        if _imp.is_frozen(fullname):
            is_package = _imp.is_frozen_package(fullname)
            return modulespec(fullname, cls, origin="frozen", is_package=is_package)
        else:
            return none

    @classmethod
    def create_module(cls, spec):
        """use default semantics for module creation."""

    @staticmethod
    def exec_module(module):
        name = module.__spec__.name
        code = _imp.get_frozen_object(name)
        exec(code, module.__dict__)
```

処理の中身は`BuiltinImporter`の場合と似ていますが、`FrozenImporter`では`create_module()`の実装を省略しています。これにより、空のモジュールが生成されます。その代わりに実装の実体は`exec_module()`内の`_imp.get_frozen_object()`及び`exec()`にあります。

```Pythjon/import.c
static PyObject *
is_frozen_package(PyObject *name)
{
    const struct _frozen *p = find_frozen(name);
    if (p->size < 0)
        Py_RETURN_TRUE;
    else
        Py_RETURN_FALSE;
}

static const struct _frozen *
find_frozen(PyObject *name)
{
    const struct _frozen *p;
    for (p = PyImport_FrozenModules; ; p++) {
        if (p->name == NULL) return NULL;
        if (_PyUnicode_EqualToASCIIString(name, p->name))
            break;
    }
    return p;
}

static PyObject *
get_frozen_object(PyObject *name)
{
    const struct _frozen *p = find_frozen(name);
    int size = p->size;
    if (size < 0) size = -size;
    return PyMarshal_ReadObjectFromString((const char *)p->code, size);
}
```

```Python/frozen.c
static const struct _frozen _PyImport_FrozenModules[] = {
    {"_frozen_importlib", _Py_M__importlib_bootstrap,
        (int)sizeof(_Py_M__importlib_bootstrap)},
    {"_frozen_importlib_external", _Py_M__importlib_bootstrap_external,
        (int)sizeof(_Py_M__importlib_bootstrap_external)},
    {"zipimport", _Py_M__zipimport,
        (int)sizeof(_Py_M__zipimport)},
    // ...
    {0, 0, 0} /* sentinel */
};
```

```Python/importlib.h
const unsigned char _Py_M__importlib_bootstrap[] = {
    99,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
    0,4,0,0,0,64,0,0,0,115,194,1,0,0,100,0, // ...
};
```

このように、例えば`frozen_importlib`の実体は配列`_Py_M__importlib_bootstrap[]`に格納されていることがわかります。

## `PathFinder`

`PathFinder`の中身はシンプルです。

```Lib/importlib/_bootstrap_external.py
class PathFinder:
    @classmethod
    def find_spec(cls, fullname, target=None):
        for entry in sys.path:
            if entry == '':
                entry = os.getcwd()
            try:
                finder = sys.path_importer_cache[entry]
            except KeyError:
                finder = None
                for hook in sys.path_hooks:
                    try:
                        finder = hook(entry)
                    except ImportError:
                        continue
                sys.path_importer_cache[entry] = finder
            if finder is not None:
                spec = finder.find_spec(fullname, target)
                if spec.loader is not None:
                    break
        else:
            # ... namespace packageのための処理
        return spec
```

`PathFinder`の`find_spec()`では直接`spec`を生成することは行いません。代わりに`sys.path[]`に格納されたパスについて、`sys.path_hooks[]`を参照し、対応するFinderを探します。Finderが見つかったら、`finder.find_spec()`を呼び出します。

`sys.path_hooks[]`は対話環境から確認できます。

```shell
$ python
>>> import sys
>>> sys.path_hooks
[<class 'zipimport.zipimporter'>, <function FileFinder.path_hook.<locals>.path_hook_for_FileFinder at 0x7f3806ece160>]
```

2つ目の`path_hook_for_FileFinder()`は`FileFinder`クラスのインスタンスを返す関数(クロージャ)で、`FileFinder`クラスの`path_hook()`関数にあります。

```Lib/importlib/_bootstrap_external.py
class FileFinder:
    # ...
    @classmethod
    def path_hook(cls, *loader_details):
        def path_hook_for_FileFinder(path):
            return cls(path, *loader_details)
        return path_hook_for_FileFinder
```

関数`path_hook()`の呼び出し部は以下のようになります。

```Lib/importlib/_bootstrap_external.py
supported_loaders = [
    # ...
    (SourceFileLoader, ['.py']),
    (SourcelessFileLoader, ['.pyc'])
]
sys.path_hooks.extend([FileFinder.path_hook(*supported_loaders)])
```

この最後の行で、`FileFinder.path_hook()`の戻り値である`path_hook_for_FileFinder()`が`sys.path_hooks[]`に追加されていることがわかります。よって、先程説明した`PathFinder.find_spec()`において各`sys.path[]`のパスを引数として`path_hook_for_FileFinder()`が実行され、その戻り値である`FileFinder`クラスのインスタンスの`find_spec()`が実行されます。

`FileFinder`の実装を見てみます。

```Lib/importlib/_bootstrap_external.py
class FileFinder:
    def __init__(self, path, *loader_details):
        loaders = []
        for loader, suffixes in loader_details:
            loaders.extend((suffix, loader) for suffix in suffixes)
        self._loaders = loaders
        self.path = path or '.'

    def find_spec(self, fullname, target=None):
        cache_module = fullname.rpartition('.')[2]
        contents = _os.listdir(self.path)
        cache = set(contents)
        # モジュールがディレクトリ名である場合(パッケージ)
        if cache_module in cache:
            base_path = _path_join(self.path, cache_module)
            for suffix, loader_class in self._loaders:
                init_filename = '__init__' + suffix
                full_path = _path_join(base_path, init_filename)
                if _path_isfile(full_path):
                    loader = loader_class(fullname, full_path)
                    spec = _bootstrap.ModuleSpec(name, loader, origin=full_path)
                    spec._set_fileattr = True
                    spec.submodule_search_locations = submodule_search_locations
                    return spec
            else:
                # ... 名前空間パッケージのための処理
        # モジュール名が通常のファイルを指す場合
        for suffix, loader_class in self._loaders:
            full_path = _path_join(self.path, tail_module + suffix)
            if cache_module + suffix in cache:
                if _path_isfile(full_path):
                    loader = loader_class(fullname, full_path)
                    spec = _bootstrap.ModuleSpec(name, loader, origin=full_path)
                    spec._set_fileattr = True
                    spec.submodule_search_locations = None
                    return spec
        # 名前空間パッケージのための処理
        return None
```

`FileFinder.find_spec()`では、モジュール名がディレクトリ名やファイル名に一致しているかを調べ、一致している場合は`ModuleSpec`クラスを用いてSpecを作成しています。このとき、`spec.loader`を設定するために`self._loaders`を参照しています。例えば、拡張子`.py`の場合、`SourceFileLoader`クラスのインスタンスがLoaderとして設定されることになります。


## Spec取得後の処理

ここまでで、モジュールに対応する`Spec`及び`Loader`が取得できました。ここで、先に示した`_find_and_load_unlocked()`の続きを見てみます。

```Lib/importlib/_bootstrap.py
def _find_and_load_unlocked(name, import_):
    # ...
    spec = _find_spec(name, path)
    if spec is None:
        raise ModuleNotFoundError(_ERR_MSG.format(name), name=name)
    else:
        module = _load_unlocked(spec)
    # ...
    return module

def _load_unlocked(spec):
    module = module_from_spec(spec)
    # ...
    try:
        sys.modules[spec.name] = module
        try:
            # ...
            spec.loader.exec_module(module)
        except:
            try:
                del sys.modules[spec.name]
            except KeyError:
                pass
            raise
        # exec_module()内で別のモジュールを読み込んだ場合
        # sys.modules[] の最後尾に移動させる
        module = sys.modules.pop(spec.name)
        sys.modules[spec.name] = module
    # ...
    return module

def module_from_spec(spec):
    module = None
    if hasattr(spec.loader, 'create_module'):
        # If create_module() returns `None` then it means default
        # module creation should be used.
        module = spec.loader.create_module(spec)
    # ....
    if module is None:
        module = _new_module(spec.name)
    _init_module_attrs(spec, module)
    return module

def _new_module(name):
    return type(sys)(name)
```

このように、`module_from_spec()`内で`spec.loader.create_module()`という関数を呼び出してモジュールを生成しています。
生成したモジュールは、`sys.modules[]`の最後尾に追加されます。
その後、`spec.loader.exec_module(module)`が呼び出されます。

## `Loader`内の処理

それでは、`SourceFileLoader`クラスの実装を見てみます。

```Lib/importlib/_bootstrap_external.py
class SourceFileLoader:
    def create_module(self, spec):
        return None

    def exec_module(self, module):
        source_path = self.path
        with _io.open_code(source_path) as file:
            source_bytes = file.read()
        code_object = compile(source_bytes, source_path, "exec", dont_inherit=True, optimize=-1)
        exec(code_object, module.__dict__)
```

ここで、`create_module()`は`None`を返します。よって、デフォルトの挙動である`_new_module()`が実行され、`module = type(sys)(name)`によってモジュールが生成されます。
その後、`exec_module()`の呼び出しでは、以下のような処理が行われています。

- ソースコードを読み込む
- `compile()`を用いて`code_object`を生成する
- `exec()`及び`module`を用いてモジュールを実行する

# まとめ

まとめると、通常のテキスト形式のソースファイルに対応するモジュールの読み込み時には以下のような処理が行われていることになります。

- 辞書`sys.modules`に存在する場合、それを使用する。
- `sys.modules`に存在しない場合、`sys.meta_path`を参照する。
- `sys.meta_path`にはFinderオブジェクトが格納されている。各Finderオブジェクトに対し、`finder.find_spec()`を実行する。
- 通常のモジュールでは、`PathFinder`が用いられる。
- `PathFinder.find_spec()`は、`sys.path`を参照して、各パスに対応する`FileFinder`を生成し(このとき`sys.path_hooks[]`に含まれる`Finder`生成関数が順に実行される)、`FileFinder.find_spec()`を`sys.path`の順番で実行する。
- `FileFinder.find_spec()`では、`self.path`に対応するディレクトリ内のファイルを検索し、ファイルが見つかった場合`ModuleSpec`, 見つからなかった場合`None`を返す。その際、`ModuleSpec.loader`として`SourceFileLoader`が渡される。
- `SourceFileLoader.create_module()` でモジュールが生成されるが、これは実際のところ何もしないので、代わりに`type(sys)(name)`を用いてモジュールが生成される。
- 生成されたモジュールが`sys.modules`にキャッシュされる。
- `SourceFileLoader.exec_module()`によってモジュールが実行される。この内部では、ファイルを読み込み、バイトコードにコンパイルし、`exec()`を用いて実行している。

# おわりに

これで、Pythonにが苦手な人がまた一人増えたのではないでしょうか。

[^1]: ディレクトリ`my_module`のみがあり、ファイル`my_module/__init__.py`が存在しない事もあります。その場合は名前空間パッケージと呼ばれます。
[^2]: 名前空間パッケージの場合は、ファイルの実体がない(ディレクトリのみ)ため使用できません。代わりに`__path__`でディレクトリのリストを取得できます。
[^3]: 厳密な定義ではない。