LoginSignup
29
18

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-07

Pythonその2 Advent Calendar 2020 8日目の記事です。よろしくおねがいします。

記事の趣旨と対象読者

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

import numpy as np

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

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を用いることにより、モジュールを読み込むことができます。

import my_module

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

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を配置することで、サブモジュールとして扱うことができます。

# 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に辞書形式で格納されるため、以下のようにすればコマンドラインから実行したファイル名を取得できます。

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

import文の内部実装

バイトコードの生成

import文はCPythonによってどのように実行されるのでしょうか。
まず、CPythonは入力されたPythonスクリプトを抽象構文木に変換します。その後、抽象構文木を解釈しPythonの内部バイトコードに置き換えます。
Python/compile.c:compiler_import()において、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で宣言された構造体で、構文木を表します。simport文を表す場合、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_tyname及びasnameの組です。すなわち、import name1 as asname1, name2 as asname2, name3という文は

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()を実行します。import_name()では、まず_PyDict_GetItemIdWithError()によってPythonで書かれた関数__import__()を取得しています。次に配列stack[]を用いて__import__()に渡す引数を用意し、_PyObject_FastCall()を用いて__import__()を実行します。

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

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

注意

__import__()の実体はLib/importlib/_bootstrap.pyにありますが、実際にはこのファイルが直接実行されるわけではなく、実際にはCPythonのバイトコードに変換された_frozen_importlib/__import__()が実行されます。そのためLib/importlib/_bootstrap.pyやLib/importlib/_bootstrap_external.pyを変更した場合は、

$ 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を手動で再生成した上で、

$ 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をモジュールpubsub属性として追加し、モジュールpubを返します。
  • globals ... 相対的なモジュール名を指定した場合の起点となるモジュール。
  • locals ... 無視されます。
  • fromlist ... from-import文で使われる、読み込むべき属性のリストです。
  • level ... globalsで指定したモジュールを起点として何個親に戻るか。

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

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のロードは以下のように行われます。

_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を確認してみます。(通常のスクリプトでも同様の結果になります)

>>> 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()

BuiltinImporterfind_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オブジェクトは以下に示すものになります。

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

BuiltinImporterfind_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

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

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

$ 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. 厳密な定義ではない。 

29
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
18