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にが苦手な人がまた一人増えたのではないでしょうか。