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のみを考えます。
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に構文木の情報が入力されています。
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;
};
typedef PyObject * identifier;
s->v.Import.namesが構造体alias_tyのリストになっており、alias_tyはname及び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を用いて実行します。
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__()は以下のように定義されています。
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は考えない)。
test = __import__("test.test2", sys.modules["__main__"], None, None, 0)
__import__の処理の中身
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回目の呼び出しではこのキャッシュから取得されるだけです。
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__を持つ)。最後に親パッケージにパッケージ名の属性を追加し、モジュールを返します。
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を参照します。
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()
BuiltinImporterのfind_spec()は以下のようになっています。
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"}
BuiltinImporterのfind_specによってつくられたspecに対してcreate_module()を実行すると、_imp.create_builtin()が実行されます。
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}
};
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__と同じ |
実装
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()にあります。
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);
}
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 */
};
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の中身はシンプルです。
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[]は対話環境から確認できます。
$ 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()関数にあります。
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()の呼び出し部は以下のようになります。
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の実装を見てみます。
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()の続きを見てみます。
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クラスの実装を見てみます。
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にが苦手な人がまた一人増えたのではないでしょうか。