goでreflectを使ってunexported fieldの値を見る
テストなどのhelperを作るときに、特定のstructのunexported fieldにアクセスしたくなることがある。 その方法のメモ(あとでまじめに書くかもしれない書かないかもしれない)。
with exported field
その前にreflect経由でのアクセスで考えてみる。以下の様なstructでとりあえずexported fieldにアクセスしてみる。
type S struct { Exported int unexported int }
reflect.Value
のFieldByName()
が使える。今回はintでやったが、通常利用するときは任意のstructということを考えてinterface{}
として取り出す。
s := &S{Exported: 20, unexported: 10} rv := reflect.ValueOf(s).Elem() fmt.Println(rv.FieldByName("Exported").Interface())
これは上手く取り出せる。
20
with unexported field
同様のことをunexported fieldに対してやってみる。
s := &S{Exported: 20, unexported: 10} rv := reflect.ValueOf(s).Elem() fmt.Println(rv.FieldByName("unexported").Interface())
どうもpanicになるようだ。
panic: reflect.Value.Interface: cannot return value obtained from unexported field or method
unexported fieldなどへのアクセスは禁止されている。
with hack
いろいろ頑張ったところ、以下の様な感じでNewAt()
を経由することで取り出せる。
s := &S{Exported: 20, unexported: 10} rv := reflect.ValueOf(s).Elem() rf := rv.FieldByName("unexported") rf = reflect.NewAt(rf.Type(), unsafe.Pointer(rf.UnsafeAddr())).Elem() fmt.Println(rf.Interface())
今度は大丈夫。
10
このhackが動く理由を調べるために、もう少し詳しく中を覗いてみることにする。
詳細
まず元々のpanicがどこで発生しているかと言うと、reflectパッケージのこの辺り。
value.go
func (v Value) Interface() (i interface{}) { return valueInterface(v, true) } func valueInterface(v Value, safe bool) interface{} { if v.flag == 0 { panic(&ValueError{"reflect.Value.Interface", Invalid}) } if safe && v.flag&flagRO != 0 { // Do not allow access to unexported values via Interface, // because they might be pointers that should not be // writable or methods or function that should not be callable. panic("reflect.Value.Interface: cannot return value obtained from unexported field or method") } if v.flag&flagMethod != 0 { v = makeMethodValue("Interface", v) } // ...
Value.Interface()
は valueInterface()
をsafe=true
で実行していた。
そんなわけで、条件を迂回するには safe=false
で実行するか v.flag
がReadOnlyになっていなければ良い。
ここでflagの定義はこう。ちょっと分かりづらいので、aからeまでの番号を振って見た。
type flag uintptr const ( flagKindWidth = 5 // there are 27 kinds flagKindMask flag = 1<<flagKindWidth - 1 flagStickyRO flag = 1 << 5 // a flagEmbedRO flag = 1 << 6 // b flagIndir flag = 1 << 7 // c flagAddr flag = 1 << 8 // d flagMethod flag = 1 << 9 // e flagMethodShift = 10 flagRO flag = flagStickyRO | flagEmbedRO )
reflect.Valueの中を以下の様な形で無理やり覗いてみると、以下の様に変わっていた。たしかにNewAt()
を経由することでflagStickyRO
が取り除かれている。
fmt.Printf("%b\n", reflect.ValueOf(rf).FieldByName("flag").Uint()) // 元の値 // dcba***** // 110000010 // NewAt後の値 // dcba***** // 110100010
NewAt()
で新たなstructでwrapすることになる。そしてこのときに使われるreflect.Ptr
というconstからflagが計算される。このPtr
から作られたflagは先程の通りroではないから、Interface()
が使える様になる。というわけ。
// NewAt returns a Value representing a pointer to a value of the // specified type, using p as that pointer. func NewAt(typ Type, p unsafe.Pointer) Value { fl := flag(Ptr) t := typ.(*rtype) return Value{t.ptrTo(), p, fl} }
gist
参考
PEP593のAnnotatedでwrapされた型ヒントからmetadataを取り出す
Annotated
PEP593で型ヒントにmetadataを付けられる様になった。
例えば、関数の型ヒントを見てCLIのコマンドを生成するようなフレームワークがあったとして、コマンドラインオプションのヘルプメッセージ用の文字列も保持したい。以下のコード上での --name
オプションに対するヘルプメッセージをどうにか保持できないか悩んだりしたが、PEP593以前はできなかった。
def hello(name: str) -> None: print(f"hello {name}") # このような形で呼び出せる # $ python main.py --name=world" # hello world
PEP593で提案されたAnnotatedを利用すると以下の様に書ける様になる(3.9から)。
from typing_extensions import Annotated class Description: def __init__(self, description: str) -> None: self.description = description def hello(*, name: Annotated[str, Description("the name of person")]) -> None: print(f"hello {name}")
typingモジュールではなくtyping_extensionsモジュールのものを使うことで3.9以前のpythonでも利用できるようになる。
mypyの解釈
mypyによる型チェックのときに、Annotatedが使われた引数がどのように解釈されているのかを見てみる。reveal_typeを使うと、その時に解釈された型がどのようなものであったかを確認できる。
--- 01hello-annotated.py 2020-07-29 04:59:29.000000000 +0900 +++ 02reveal-type.py 2020-07-29 04:59:06.000000000 +0900 @@ -7,4 +7,5 @@ def hello(*, name: Annotated[str, Description("the name of person")]) -> None: + reveal_type(name) print(f"hello {name}")
nameはstrとして認識されていた。と、まぁそんなわけで、型チェック時には無視されるような値を型ヒントに埋め込むことができる。
$ mypy --strict 02*.py 02reveal-type.py:10: note: Revealed type is 'builtins.str'
metadataの抽出
mypyでは無視されるmetadata部分を取り出して利用したい。Annotatedを使って定義された関数からmetadataを取り出そうとしてみる。
pepの文章を見ると、typing.get_type_hints()
がinclude_extrasというオプションを持つらしい。これを利用することでmetadataでwrapされた値が返ってくる(はず)。
from typing_extensions import get_type_hints print(get_type_hints(hello)) # {'name': <class 'str'>, 'return': <class 'NoneType'>} print(get_type_hints(hello, include_extras=True)) # {'name': typing_extensions.Annotated[str, <__main__.Description object at 0x10cd1e730>], 'return': <class 'NoneType'>}
なるほど。これで関数からmetadata付きの辞書が取れる様になった。通常genericsなどの型パラメーターを持った型の値を取り出すにはget_args()
を使う。また、get_originを使うことで、特殊化した型の基底として使われている値を取る事ができる1
from typing_extensions import get_args hints = get_type_hints(hello, include_extras=True) print(get_args(hints["name"])) # (<class 'str'>, <__main__.Description object at 0x106427730>) print(get_origin(hints["name"])) # <class 'typing_extensions.Annotated'>
これで、metadataを取り出してよしなにできるようになった。やりましたね。
3.8以前のpythonでは?
ところが話はもう少し面倒なことになる。
実は、get_args()
などが追加されたのは3.8からで、それ以前のpythonはtypingモジュールがこれらの関数を持っていない。backportとしてtyping-inspectパッケージが用意されているが、まだこちらはAnnotatedに対応していない模様。
import typing_inspect print(typing_inspect.get_args(hints["name"])) # (<class 'str'>,)
仕方がないので、直接実装を覗いて無理やり取り出す。どうやら__metadata__
というフィールドに情報を格納しているらしい。
print(hasattr(hints["name"], "__metadata__")) # True print(hints["name"].__metadata__) # (<__main__.Description object at 0x104b9b730>,)
はい。
gist
-
正しくない表現かもしれない。
Dict[str,str]
に対してget_origin()
を使うとdict
が返るというような意味合いで使いたかった。↩