modulegraphが上手く動かないのでsys.path_hooksで遊ぶ
modulegraphというパッケージがあり、これでモジュール間の依存関係をグラフにしてみたりできるらしいのだけれど。何だか上手く動かなかった。
残念な気持ちにはなったのだけれど、何か特別なパッケージに頼らずともpythonのimport hookの機能を利用すれば同様のことはそんなに大変じゃなくできるんじゃないかと思ったのでやってみることにした。
import hook
あんまり使われていないけれど。pythonでのimportの方法についてはわりとまじめにPEPが定められている。
これに関してもまじめに読む必要もあんまりなくて、それなりに丁寧なドキュメントが用意されている(和訳もされている)。
重要なのはfinderとloaderという概念があり、finderはをfind_spec()
というメソッドでloaderを探し出すという辺り。そしてhookとしてpathを解釈する際の sys.path_hooks
がある(sys.meta_path
と言うものもあるけれどこれは使わない)。
実際どのようなものが登録されているかというと以下のような2つが登録されていると思う。
import sys for hook in sys.path_hooks: print(hook) # <class 'zipimport.zipimporter'> # <function FileFinder.path_hook.<locals>.path_hook_for_FileFinder at 0x101242a60>
大抵の場合後者のFileFinderの方のものが使われる。ちなみにこれはどこに定義されているかというと importlib/_bootstrap_external.py
というファイルがあるのでそのあたりを見てみると良い(それ意外にもimportlib以下のコードを覗いてみるとけっこう面白いと思う)。
sys.meta_path
いや、やっぱりまじめに説明するには sys.meta_path
にも触れる必要があったので触れる。基本的にはimportの解釈はsys.meta_pathに登録されているfinderオブジェクトに委ねられる。
import sys for finder in sys.meta_path: print(finder) # <class '_frozen_importlib.BuiltinImporter'> # <class '_frozen_importlib.FrozenImporter'> # <class '_frozen_importlib_external.PathFinder'>
通常のimportではPathFinderが使われる。このfinderオブジェクトの find_spec()
がimportの始まり
まじめなimportの詳細
もう少し真面目に書くと以下の様な形になる。
import <foo>
を処理系が読み取る- (bootstrap中の
_find_and_load()
が呼ばれる) - sys.modulesに既にimportされていないか確認する(importされていれば終了)
- sys.meta_pathをiterateして(iterateされるのはfinder)、
finder.find_spec()
を呼び出す - (spec(
importlib._boostrap.ModuleSpec
)はloaderを持っている) - specの持つloaderの
loader.exec_module()
が呼び出される。
すごく雑に書くと以下の様なコード(色々詳細は省いている)。
if ... not in sys.modules: for finder in sys.meta_path: spec = finder.find_spec(...) if spec is not None: return spec.loader.exec_module(...)
さてここで sys.path_hooks
が現れていない。どこで出てくるかというと、sys.meta_path
からiterateされる通常3つのMetaPathFinderの内のPathFinderの find_spec()
で使われる。こういう感じ。
for hook in sys.path_hooks: try: return hook(path) except ImportError: continue else: return None
何はなくとも sys.path_hooks
で登録された関数が importのタイミング
で呼ばれるとさえわかっていれば良い。またImportErrorが見つかったら後続のhookを探すという処理になっている。
テキトウにpath.hooksを追加してみる
テキトウにpath.hooksを追加してみる。先頭にImportErrorを送出するちょっとしたprintを挟んだhookを追加してみよう。その後jsonモジュールをimportしてみる。
def peek(name): print("@", name) raise ImportError(name) import sys sys.path_hooks.insert(0, peek) import json
例えばこういう風な出力が得られるはず。
@ /opt/local/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/json
そんなわけでimport時のhookを手にする事ができる。後はこれが呼ばれた時のstack frameをたどればどのファイルからimportされたかが分かる。そしてその依存関係を可視化すれば元のmodulegraphでやろうとしていたことは達成できる。
実際tracebackを出力してみると以下の様なものになる。
import traceback def peek(name): traceback.print_stack() raise ImportError(name) import sys sys.path_hooks.insert(0, peek) import json
ちょっと長いtracebackだけれどimportlibの部分を除いてあげれば分かりやすい形になる。
File "main.py", line 11, in <module> import json File "<frozen importlib._bootstrap>", line 969, in _find_and_load File "<frozen importlib._bootstrap>", line 958, in _find_and_load_unlocked File "<frozen importlib._bootstrap>", line 673, in _load_unlocked File "<frozen importlib._bootstrap_external>", line 665, in exec_module File "<frozen importlib._bootstrap>", line 222, in _call_with_frames_removed File "/opt/local/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/json/__init__.py", line 106, in <module> from .decoder import JSONDecoder, JSONDecodeError File "<frozen importlib._bootstrap>", line 969, in _find_and_load File "<frozen importlib._bootstrap>", line 954, in _find_and_load_unlocked File "<frozen importlib._bootstrap>", line 896, in _find_spec File "<frozen importlib._bootstrap_external>", line 1139, in find_spec File "<frozen importlib._bootstrap_external>", line 1110, in _get_spec File "<frozen importlib._bootstrap_external>", line 1082, in _path_importer_cache File "<frozen importlib._bootstrap_external>", line 1058, in _path_hooks File "qr_51394rjo.py", line 5, in peek traceback.print_stack()
こう。
File "main.py", line 11, in <module> import json File "/opt/local/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/json/__init__.py", line 106, in <module> from .decoder import JSONDecoder, JSONDecodeError File "qr_51394rjo.py", line 5, in peek traceback.print_stack()
main.pyからimport jsonされたということが分かる。
例えば wsgiref.simple_server の依存関係
上の話を色々まとめると依存関係はそれなりに簡単に取れる(他にも色々説明することがあるので詳細は省略)。 例えばhttp server用にwsgiref.simple_server等があるけれど。
from wsgiref import simple_server
このimportは以下のようなモジュールを読み込んでいる(単に読み込むモジュールを知りたいだけなのであれば python -v
で起動しても良いかもしれない)。
load wsgiref (where=main) load wsgiref.simple_server (where=main) load http (where=wsgiref.simple_server) load enum (where=http) load http.server (where=wsgiref.simple_server) load html (where=http.server) load html.entities (where=html) load http.client (where=http.server) load email (where=http.client) load email.parser (where=http.client) load email.feedparser (where=email.parser) load email.errors (where=email.feedparser) load email.message (where=email.feedparser) load uu (where=email.message) load lib-dynload.binascii.cpython-35m-darwin.so (where=uu) load quopri (where=email.message) load email.utils (where=email.message) load random (where=email.utils) load lib-dynload.math.cpython-35m-darwin.so (where=random) load hashlib (where=random) load lib-dynload._hashlib.cpython-35m-darwin.so (where=hashlib) load lib-dynload._random.cpython-35m-darwin.so (where=random) load socket (where=email.utils) load lib-dynload._socket.cpython-35m-darwin.so (where=socket) load selectors (where=socket) load lib-dynload.select.cpython-35m-darwin.so (where=selectors) load datetime (where=email.utils) load lib-dynload._datetime.cpython-35m-darwin.so (where=datetime) load urllib (where=email.utils) load urllib.parse (where=email.utils) load email._parseaddr (where=email.utils) load calendar (where=email._parseaddr) load locale (where=calendar) load email.charset (where=email.utils) load email.base64mime (where=email.charset) load base64 (where=email.base64mime) load struct (where=base64) load lib-dynload._struct.cpython-35m-darwin.so (where=struct) load email.quoprimime (where=email.charset) load string (where=email.quoprimime) load email.encoders (where=email.charset) load email._policybase (where=email.message) load email.header (where=email._policybase) load email._encoded_words (where=email.message) load email.iterators (where=email.message) load ssl (where=http.client) load ipaddress (where=ssl) load textwrap (where=ssl) load lib-dynload._ssl.cpython-35m-darwin.so (where=ssl) load mimetypes (where=http.server) load shutil (where=http.server) load fnmatch (where=shutil) load tarfile (where=shutil) load copy (where=tarfile) load lib-dynload.grp.cpython-35m-darwin.so (where=tarfile) load bz2 (where=shutil) load _compression (where=bz2) load threading (where=bz2) load traceback (where=threading) load linecache (where=traceback) load tokenize (where=linecache) load token (where=tokenize) load lib-dynload._bz2.cpython-35m-darwin.so (where=bz2) load lzma (where=shutil) load lib-dynload._lzma.cpython-35m-darwin.so (where=lzma) load socketserver (where=http.server) load argparse (where=http.server) load gettext (where=argparse) load wsgiref.handlers (where=wsgiref.simple_server) load wsgiref.util (where=wsgiref.handlers) load wsgiref.headers (where=wsgiref.handlers) load platform (where=wsgiref.simple_server) load subprocess (where=platform) load signal (where=subprocess) load lib-dynload._posixsubprocess.cpython-35m-darwin.so (where=subprocess)
dot形式で出力してgraphvizなどで可視化すると以下の様になる
https://gist.github.com/podhmo/539854527d1f9b190805873111101414#file-01-svg:cite:pined