reflect経由でgoのmethodを呼んだり存在を確認したりする
encoding/jsonパッケージの範囲を越えてJSONと戯れようとする。動的な何かが必要になる。そしてけっこうすぐにreflectパッケージに触れることになる。触りたくないけれど触る必要がある汚泥のような存在。それがreflect。
重い腰をあげてreflectと少し仲良くなるためにした試行錯誤のメモ。
概ね以下のことのやリ方についての文章になっている。
- 定義されているメソッドの数を確認
- メソッドの存在確認
- メソッドを呼び出す
定義されているメソッドの数を確認
定義されているメソッドの確認は型に対して行う。型に対してということは reflect.Value
ではなく reflect.Type
を使うということ。
package main import ( "fmt" "reflect" ) type Foo struct{} func (f *Foo) String() string { return "Foo" } func main() { { v := Foo{} rt := reflect.TypeOf(v) fmt.Println(rt.NumMethod(), rt.Kind(), rt.Name(), rt.String()) // 0 struct Foo main.Foo } { v := &Foo{} // pointer rt := reflect.TypeOf(v) fmt.Println(rt.NumMethod(), rt.Kind(), rt.Name(), rt.String()) // 1 ptr *main.Foo } { v := Foo{} // Valueからpointerの型を作る rv := reflect.ValueOf(v) rt := reflect.PtrTo(rv.Type()) fmt.Println(rt.NumMethod(), rt.Kind(), rt.Name(), rt.String()) // 1 ptr *main.Foo } }
ついでに組み込みの型かどうかを調べる
reflect.Type.PkgPathを見るのが正解なのかもしれない。
type Type interface { ... // PkgPath returns a defined type's package path, that is, the import path // that uniquely identifies the package, such as "encoding/base64". // If the type was predeclared (string, error) or not defined (*T, struct{}, // []int, or A where A is an alias for a non-defined type), the package path // will be the empty string. PkgPath() string ... }
ためしにやってみる。悪くなさそう。
package main import ( "fmt" "reflect" ) type MyInt int func main() { { var z int rt := reflect.TypeOf(z) fmt.Printf("%q %q\n", rt.String(), rt.PkgPath()) // "int" "" } { var z MyInt rt := reflect.TypeOf(z) fmt.Printf("%q %q\n", rt.String(), rt.PkgPath()) // "main.MyInt" "main" } }
詳しい。
メソッドの存在確認
reflect.Type経由で名前で確認する。
package main import ( "fmt" "reflect" ) type Foo struct{} func (f Foo) String() string { return "Foo" } type Bar struct{} func main() { { v := Foo{} rt := reflect.TypeOf(v) // or reflect.ValueOf(v).Type() fmt.Println(rt.MethodByName("String")) // {String func(main.Foo) string <func(main.Foo) string Value> 0} true } { v := Bar{} rt := reflect.TypeOf(v) // or reflect.ValueOf(v).Type() fmt.Println(rt.MethodByName("String")) // { <nil> <invalid Value> 0} false } }
ところでポインターにくっついたメソッドは取り出せない。まぁ型が違うので。
package main import ( "fmt" "reflect" ) type Foo struct{} func (f *Foo) String() string { return "Foo" } func main() { { v := Foo{} rt := reflect.TypeOf(v) // or reflect.ValueOf(v).Type() fmt.Println(rt.MethodByName("String")) // { <nil> <invalid Value> 0} false } { v := &Foo{} rt := reflect.TypeOf(v) // or reflect.ValueOf(v).Type() fmt.Println(rt.MethodByName("String")) // {String func(*main.Foo) string <func(*main.Foo) string Value> 0} true } }
interface経由で確認
上のようにメソッドの存在を確認するのではなく、インターフェイスが実装されているかで確認したほうがきれいな場合もあるかも。
package main import ( "fmt" "reflect" ) type Foo struct { } func (f *Foo) String() string { return "Foo" } func main() { { z := Foo{} var iface fmt.Stringer rt := reflect.TypeOf(z) fmt.Println(rt.Implements(reflect.TypeOf(&iface).Elem())) // false } { z := &Foo{} var iface fmt.Stringer rt := reflect.TypeOf(z) fmt.Println(rt.Implements(reflect.TypeOf(&iface).Elem())) // true } }
インターフェイスのreflect.Type
として取り出す時に、TypeOf()
と Elem()
を組み合わせるのがちょっとトリッキー。
以下のようなコードだとpanicする。
// NG
rt.Implements(reflect.TypeOf(iface))
まぁ確かに nil type (値と型のペアで保持していて型の部分がnilだとnil type)。
panic: reflect: nil type passed to Type.Implements
go-nutsとかはじめて覗いた(気がする)。
メソッドを呼び出す
メソッドを呼び出すのは値のほうなので reflect.Value
引数も戻り値も全部 []reflect.Value
なことに注意。
package main import ( "fmt" "reflect" ) type Foo struct { Name string } func (Foo) String() string { return "Foo" } func (*Foo) Say() string { return "hello" } func main() { { v := Foo{Name: "xxx"} rv := reflect.ValueOf(v) method := rv.MethodByName("String") fmt.Printf("%[1]T %[1]v\n", method.Call(nil)) // []reflect.Value [Foo] fmt.Printf("%[1]T %[1]v\n", method.Call(nil)[0].String()) // string Foo } { v := Foo{Name: "xxx"} rv := reflect.ValueOf(v) // make pointer rptrType := reflect.PtrTo(rv.Type()) rptr := reflect.New(rptrType.Elem()) rptr.Elem().Set(rv) method := rptr.MethodByName("Say") fmt.Printf("%[1]T %[1]v\n", method.Call(nil)) // []reflect.Value [hello] } }
ちなみにpointerを期待した所でpointer以外からメソッドを呼び出すと以下のようなエラーが出る。
panic: reflect: call of reflect.Value.Call on zero Value
はい。
なのでわざわざ値を作ってからその値をセットしなくてはいけない。
// make pointer
rptrType := reflect.PtrTo(rv.Type())
rptr := reflect.New(rptrType.Elem())
rptr.Elem().Set(rv)
Sliceなどに対しては MakeSlice()
という関数なので MakePtr()
だと思うと New()
というのはちょっとハマる。
さいごに
さいごにこのreflectのサンプル集がとても便利です。
:pray: このページだけ覚えて帰ってもらえるとそれだけでこの記事を読むより価値があると思います。
参考文献
pythonのimportシステム周りのことについてのメモ、あるいはimport fooとしたときに何が起きるかについて
あんまり丁寧に文章を書く気力が起きないので個人的なメモ。pythonのimport周りの話。(最後の実行例のgistはこちら)
(追記: 結局それなりに長くなった)
当初のお気持ち
import fooがどうなるかみたいな話、そういえば発表のお題としてはけっこう良かったかもなーとか思ったりした。
— po (@podhmo) 2019年7月24日
(まぁ資料の準備がわりと大変そうではある)
ところでpythonのimportlibまわりは全体を追ってみるとへーと思うことが多かったりするので暇つぶしにはおすすめ。
— po (@podhmo) 2019年7月24日
はじめに
ある程度pythonのドキュメントを丁寧に読めば分かることではあるのだけれど。自分用の情報の整理のためにまとめる。
このあたりを読めば良い。
- https://docs.python.org/ja/3/reference/import.html#the-import-system
- https://docs.python.org/ja/3/library/importlib.html
とくに1つ目のリンクでけっこう詳しく書かれているのだけれど。古い情報と今の情報が混在していて分かりづらい。
ドキュメントがむずかしい
以下の様なオブジェクトがいきなり出てきて何も知識なしに呼んでも正直何を言っているのかわからないと思う。
わからないオブジェクトたち
以下の様な役者がさらっとでてくる。
- finder (path entry finder, meta path finder)
- loader
- importer
- spec
わからない設定値たち
そしていきなり以下の様なsysモジュール以下の設定値のことが出てくる。
- sys.path
- sys.meta_path
- sys.path_hooks
- (sys.path_impoter_cache)
いきなり情報が溢れてわからない。この辺を把握できるような感じの概観を使むような説明を書いてメモしておく。
import fooしたときに起きること
pythonのコード上で以下の様なコードをインタプリタが解釈した時に何が起きるかということがあいまいにわかっていれば良い。なのでそのための説明を雑に加えてみる。
import foo
import fooが解釈されるとき裏側では以下の様な処理が行われている。とても簡略化された形では。
- sys.meta_path に登録されているFinderを順に実行していく
- Finderのfind_spec()を実行する
- Finderのfinder_spec()がNone以外のものだったらそのspecを使って以降の処理を進める
- specからLoaderを取り出す
- Loaderのcreate_module()でモジュールを作る
- Loaderのexec_module()を実行する
概要を擬似的なコードで
雑な疑似コードに落とすとこういう形(ちょっとだけ例外の扱いも追加した。完全に正確なものではない)。
# import foo で行われることを簡略化したコード name = "foo" # sys.modules["foo"] がない場合の処理 for finder in sys.meta_path: spec = finder.find_spec(name) if spec is None: continue # ゴニョゴニョ処理をする loader = spec.loader module = loader.create_module() # moduleがNoneだったらdefaultの方法でmoduleを作るなどゴニョゴニョ処理をする try: loader.exec_module(module) break except ImportError: continue else: raise ModuleNotFoundError(name)
詳しい話はこの辺りにかかれているがたぶん最初に呼んでも分かりづらいかもしれない。
より詳細な疑似コードはこの辺り
ただし実際に行われているコードはこれよりも古い実装への対応なども含めてもう少し複雑。
import周りを理解するのに便利なモジュール群
以下の辺りのモジュールのコードを読むと良い。
- importlib
- importlib.util
- importlib.machinery
- importlib._bootstrap
- impoortlib._bootstrap_external
基本的には裏側の実装が _bootstrap
と _bootstrap_external
に書かれていて、それへの参照を用意するためのmoduleが importlib.machnery
になっている。
finder,loader?
以下の様な対応関係になっている(説明は用語集のものを引用)。
- finder -- インポートされているモジュールの loader の発見を試行するオブジェクトです。
- loader -- モジュールをロードするオブジェクト。 load_module() という名前のメソッドを定義していなければなりません。ローダーは一般的に finder から返されます。
- Importer = Finder + Loader
ドキュメントもabcも古い
幸いにも importlilb.abcにfinder,loaderのbase classが定義されているのでこれを参照すると便利。
と思いきや、ここで定義されているメソッド群は古い仕様を尊重したものなので具合が悪い。これが古い仕様の混在がドキュメントにも存在していてけっこうツライ。
importlib.abc:Finder <- builtins:object [method] find_module(self, fullname, path=None) # deprecated importlib.abc:Loader <- builtins:object [method] create_module(self, spec) [method] load_module(self, fullname) # deprecated [method] module_repr(self, module) # deprecated
実際のfinder,loader
実際には以下の様なオブジェクトを期待している(後で余裕があればpepへのリンクを貼る)。
importlib.abc:Finder2 <- builtins:object [method] find_spec(self, fullname, path=None, target=None) importlib.abc:Loader2 <- builtins:object [method] create_module(self, spec) [method] exec_module(self, module)
だいぶわかりやすくなった。そんなわけで以下の様なイメージで覚えてくれれば良い。
- finder -- find_spec()を実装しているもの
- loader -- create_module()とexec_module()を実装しているもの
新規に使う場合にはこういう捉え方で十分。これだけ覚えると良い。
finderからloaderへの受け渡し
実際先ほどのfinder,loaderの定義だけだとどうやってloaderが取り出されているかわからないと思う(実は用語集に言及があった(あとできづいた))。これはspecの作られ方を見てみると分かりやすい。
# 内部的には importlib._bootstrap.ModuleSpec from importlib.machinery import ModuleSpec spec = ModuleSpec(fullname, loader) # spec.loder でloader が参照。
(後々の操作でloaderがNoneのときにはちょっとした探索がはしったりもするけれどそれは省略)
ここでimporterへの言及を。impoter = finder + loader というのはそういうことで典型的なimporterのfind_spec()は以下の様な実装になる。自分自身をloaderとして渡すということ(特に状態をもたない場合にはclassmethodとして実装されることもある)。
class MyImporter: def find_spec(self, fullname, path=None, target=None): loader = self return ModuleSpec(fullname, loader) # def create_module(self, spec) ... # def exec_module(self, module) ...
実際にimportの機能を提供しているもの
ようやく具体的な話になってきた。これまででloader,finderとspecの説明が続いてきたが各種設定値についての説明がまだ。以下のもののこと。
- (sys.path)
- sys.meta_path
- sys.path_hooks
- (sys.path_impoter_cache)
それぞれの意味について説明する前に現在のpythonの環境で設定されている値について見てみよう。
$ python -q >>> import sys >>> sys.meta_path [ <class '_frozen_importlib.BuiltinImporter'>, <class '_frozen_importlib.FrozenImporter'>, <class '_frozen_importlib_external.PathFinder'> ] >>> sys.path_hooks [ <class 'zipimport.zipimporter'>, <function FileFinder.path_hook.<locals>.path_hook_for_FileFinder at 0x7f22f52e9598> ]
なにやらいろいろ登録されている。
通常使われるloader
そしてテキトウに re モジュールなどをimportして使われているloaderを調べる。
moduleの__loader__
に利用したloaderが入る
>>> import re >>> re.__loader__ <_frozen_importlib_external.SourceFileLoader object at 0x7f22f5155b38>
importlib.util.find_spec()でspecを取り出せる。
>>> import importlib.util as u >>> u.find_spec("re") ModuleSpec(name='re', loader=<_frozen_importlib_external.SourceFileLoader object at 0x7f22f5155b38>, origin='/usr/lib/python3.7/re.py')
通常はSourceFileLoaderが使われると思う(ということが分かった)。
色々なloader,finder
その他には以下の様なクラスが用意されている。それこそimportlib.machineryを見れば良い。いままでの説明があればなんとなく予想がつくと思う。
- BuiltinImporter
- FrozenImporter
- WindowsRegistryFinder
- PathFinder
- FileFinder
- SourceFileLoader
- SourcelessFileLoader
- ExtensionFileLoader
(その他LazyLoader が importlib.utilに存在するがdocumentに書いてある通り使いかたに注意が必要)
通常使われているfinder
さて、loaderが分かったが、finderがわからない。またそれぞれとsys.meta_path, sys.path_hooksの対応がわからない。
重要なのはこの2つ。
- PathFinder
- FileFinder
それだけじゃどの設定値にどうやって設定されているのかわからないじゃん。と思ってしまう。実は以下の様な関係になっている。
めんどくさいので関係をコードで表す。
# import fooされたときに name = "foo" # module name for finder in sys.meta_path: # ここで使われるfinderがPathFinder spec = finder.find_spec(name) # PathFinderのfind_spec()の中でFileLoaderが使われる
そして、PathFinderのfind_spec()の中で、sys.path_hooksに格納されているfinderを見る。
実際のコードはこんな感じ。
class PathFinder: """Meta path finder for sys.path and package __path__ attributes.""" @classmethod def _path_hooks(cls, path): """Search sys.path_hooks for a finder for 'path'.""" # ... for hook in sys.path_hooks: try: return hook(path) except ImportError: continue else: return None
大抵の場合には、path_hooksで利用されるfinderはFileFinder。ただしsys.path_hooks に登録されているのは、これを生成するクロージャ関数になっている。
補足するとsys.pathを気にするのはFileFinderの実装。これで全体が分かる様になってきたと思う。FileFinderはファイルの拡張子とLoaderの対応を取るのでこれを元にファイルシステムを探索してファイルを読み込みむという流れになる(.pyと.pycあるいは.soとかの拡張子に対応したloaderが選択される)。
利用されるfinderのまとめ
つまり
- PathFinder が sys.meta_path に (python本体のモジュールシステム)
- FileFinder が sys.path_hooks に (こちらはPathFinderが利用する設定)
ということになる(正確にはsys.path_hooksに登録されるのはfinderのfactory)。
用語整理
そろそろドキュメント本体を読むのも手軽になってきたと思うので用語の整理。これは[用語集をそのまま持ってきても良い気がする
loader
モジュールをロードするオブジェクト。 load_module() という名前のメソッドを定義していなければなりません。ローダーは一般的に finder から返されます。詳細は PEP 302 を、 abstract base class については importlib.abc.Loader を参照してください。
finder
(ファインダ) インポートされているモジュールの loader の発見を試行するオブジェクトです。 Python 3.3 以降では 2 種類のファインダがあります。 sys.meta_path で使用される meta path finder と、 sys.path_hooks で使用される path entry finder です。
そして2つのfinderと言っていたものの名前もしっかりと用語集に存在していて以下のようなもの
- meta path finder -- PathFinder がそのひとつ
- path entry finder -- FileFinder がそのひとつ
meta path finder
sys.meta_path を検索して得られた finder. meta path finder は path entry finder と関係はありますが、別物です。 meta path finder が実装するメソッドについては importlib.abc.MetaPathFinder を参照してください。
path entry finder
sys.path_hooks にある callable (path entry hook) が返した finder です。与えられた path entry にあるモジュールを見つける方法を知っています。 パスエントリーファインダが実装するメソッドについては importlib.abc.PathEntryFinder を参照してください。
importシステムの拡張
何かしら実例もないと面白くないだろうということでちょっとしたimportシステムの拡張を作ってみる。
import foo
で何が起きるか?
何を起こしても良いけれど、今回は存在しないファイルを指定してimportしても必ずdummyのmoduleが見つかってimportされるような拡張を考えてみる。
作るものは?
今回作るものは存在しないmoduleをimportした時に、dummy moduleが返され、そのdummy moduleはhello()という関数を1つ持っているというもの。
そしてhello()の戻り値はmodule名つきの文字列。以下の様なイメージ。
# xxx.py, yyy.py は存在しない import xxx import yyy xxx.hello() # => hello from xxx yyy.hello() # => hello from yyy
はい。
importer? loader? finder?
手軽な実装なのでimporterで良いはず。
実装
簡単なものなので実装は手軽に終わる。はい。
import textwrap import sys from importlib.machinery import ModuleSpec class DummyImporter: # importer = finder + loader @classmethod def find_spec(cls, fullname, path=None, target=None): loader = cls return ModuleSpec(fullname, loader) @classmethod def create_module(cls, spec): return None @classmethod def exec_module(cls, module): code = textwrap.dedent( """ def hello(): return f"hello from {__name__}" """ ) exec(code, module.__dict__)
finder, loaderを自作する時に注意点が幾つかある。以下のようなもの(詳しくはこの辺りにも書いてある)。
loader.exec_module()
- python moduleの場合には
module.__dict__
でexecするべき - 上手く動かない例外はぜんぶImportErrorにするべき
loader.create_module()
- Noneを返せば良い感じにmoduleを作ってくれる
- (内部的には types.ModuleType)
finder.find_spec()
- importerとして扱うのならfind_spec()で返すloaderにselfを渡せば良い
はい。
sys.path_hooks, sys.meta_pathへの登録
作ったimporterを有効にするためにhookに登録する。今回の場合はsys.meta_pathの方が手軽ですね。。
sys.meta_path.append(DummyImporter)
実行してみる。
# xxx.py, yyy.py は存在しない import xxx import yyy xxx.hello() # => hello from xxx yyy.hello() # => hello from yyy
良さそう。手元で実行したい場合のgist
sys.path_hooksを使いたい場合
sys.path_hooksを使いたい場合には以下の様なコードが必要になる。
def make_finder(path): # print("@@", path) return DummyImporter # 蛇足に理由を書いた sys.path.append(".") sys.path_hooks.insert(0, make_finder)
はい。
その他便利な説明の記事について
ちなみにimportシステムの拡張について、この記事の内容が全体を把握するのに手軽で便利だった。
pyfoの内部の話について書かれた内容っぽい。
clojure -> pythonの変換をするというのはちょっと面白い。
https://t.co/iFcMaGMote
— po (@podhmo) 2019年7月24日
そういえばこれがわりとpythonのmoduleのloader,finderの実装を把握するのに良さそうな記事だった
ClojureLoaderというオブジェクトを追加して一部のコードをclojure -> pythonに変換して importみたいな感じ
— po (@podhmo) 2019年7月24日
コメントの部分のsys.path[0]を参照するのがよくわからないっっていうのには同意。
— po (@podhmo) 2019年7月24日
蛇足 find_spec()などについて
find_spec()などのbaseクラスで定義されていないメソッドについて、typeshedの方の型定義の方を覗いてみるのも良いかもしれない(例えばMetaPathFinderのあたり)。以下の様なコメントが書かれていてかなしい。
class MetaPathFinder(Finder): def find_module(self, fullname: str, path: Optional[Sequence[_Path]]) -> Optional[Loader]: # Not defined on the actual class, but expected to exist. def find_spec( self, fullname: str, path: Optional[Sequence[_Path]], target: Optional[types.ModuleType] = ... ) -> Optional[ModuleSpec]: ...
これはなぜかと言うと既存のコードを補助するために find_spec()
の存在の有無で分岐しているため。abcのbase classでfind_spec()
を定義してしまうとその分岐が動かなくなってしまうという判断から(レガシーとの付き合いという感じ)。
/usr/lib/python3.7/importlib/_bootstap_external.py
class PathFinder: """Meta path finder for sys.path and package __path__ attributes.""" # ... @classmethod def _get_spec(cls, fullname, path, target=None): """Find the loader or namespace_path for this module/package name.""" # ... if hasattr(finder, 'find_spec'): spec = finder.find_spec(fullname, target) else: spec = cls._legacy_get_spec(fullname, finder)
defaultの実装だけを気にするなら、実行しようとしてみてNotImplementedを拾って古い方の処理に分岐というやり方もありかもだけど(2.xを特別扱いしたくないですよね的な気持ち)。そうしてしまうと古い方に対応した外部のコードでNotImplementedを拾えない場合がありそうで怖いと言うような話(と理解している)。
蛇足 FileFinderがどうやって利用するloaderを選択しているか
実際初期化時に呼ばれるコードは以下のようなもの。そんなわけで.pyと.pycや拡張モジュールの読み込みで使われるloaderが変わる。
/usr/lib/python3.7/importlib/_bootstap_external.py
def _install(_bootstrap_module): """Install the path-based import components.""" _setup(_bootstrap_module) supported_loaders = _get_supported_file_loaders() sys.path_hooks.extend([FileFinder.path_hook(*supported_loaders)]) sys.meta_path.append(PathFinder) def _get_supported_file_loaders(): """Returns a list of file-based module loaders. Each item is a tuple (loader, suffixes). """ # '.abi3.so', '.so' とか(環境によって異なる) extensions = ExtensionFileLoader, _imp.extension_suffixes() # .py source = SourceFileLoader, SOURCE_SUFFIXES # .pyc bytecode = SourcelessFileLoader, BYTECODE_SUFFIXES return [extensions, source, bytecode]
蛇足 FileLoaderのsys.pathを見る部分について
ここでファイルの存在を確認していた。つまりsys.path_hooksに追加するときにはここに注意。
/usr/lib/python3.7/importlib/_bootstap_external.py
class FileFinder: """File-based finder.""" # ... def find_spec(self, fullname, target=None): """Try to find a spec for the specified module.""" # ... # Check for a file w/ a proper suffix exists. for suffix, loader_class in self._loaders: full_path = _path_join(self.path, tail_module + suffix) _bootstrap._verbose_message('trying {}', full_path, verbosity=2) if cache_module + suffix in cache: if _path_isfile(full_path): return self._get_spec(loader_class, fullname, full_path, None, target) def _path_isfile(path): """Replacement for os.path.isfile.""" return _path_is_mode_type(path, 0o100000) def _path_is_mode_type(path, mode): """Test whether the path is the specified mode type.""" try: stat_info = _path_stat(path) except OSError: return False return (stat_info.st_mode & 0o170000) == mode
参考文献
- http://www.robots.ox.ac.uk/~bradley/blog/2017/12/loader-finder-python.html
- https://docs.python.org/ja/3/reference/import.html#finders-and-loaders
- https://docs.python.org/ja/3/library/importlib.html
- https://docs.python.org/ja/3/glossary.html
- https://github.com/python/typeshed/tree/master/stdlib/3/importlib
- https://www.python.org/dev/peps/pep-0302/
- https://www.python.org/dev/peps/pep-0451/