こちらの投稿では,Pythonがあるオブジェクトに与えられた属性名をどのように解決するのか,CPythonの実装レベルで見てきます。バイトコードをCのレベルで確認するための事前準備はこちらの投稿を参考にしてください。
#バイトコードへの変換
属性名の参照は"a.b"とすることで実行されます。このため,まずは"a.b"をバイトコードに変換します。
1 0 LOAD_NAME 0 (a)
2 LOAD_ATTR 1 (b)
4 POP_TOP
6 LOAD_CONST 0 (None)
8 RETURN_VALUE
#gdbによるステップ実行
前回の投稿にあるとおり,バイトコードを処理する際に中心となる関数は_PyEval_EvalFrameDefault関数です。対話モードのプロンプトを表示させた後,"a.b"を入力してどのような動作を行うか確認していきます。
##LOAD_NAME
1つ目のLOAD_NAMEはPython/ceval.cの2345行目にあります。GETITEMマクロで"a"に対するPyObjectを取得します。次に,2355行目のPyDict_GetItem関数でローカルの名前空間(locals変数)からその名前に紐づくオブジェクトを取得します。取得できない場合,2367行目にあるPyDict_GetItemでグローバルの名前空間(f->f_globals)を参照します。そこでも取得できない場合,2370行目にあるPyDict_GetItem関数でビルトイン名前空間(f->f_builtins)にアクセスし,そこに無い場合は,goto文でNameErrorを返す処理に移動します。もしこの処理の過程でオブジェクトが存在した場合は,PUSHマクロによってスタックにオブジェクトを積みます。
TARGET(LOAD_NAME) {
2345 PyObject *name = GETITEM(names, oparg);
...
2354 if (PyDict_CheckExact(locals)) {
2355 v = PyDict_GetItem(locals, name);
...
2366 if (v == NULL) {
2367 v = PyDict_GetItem(f->f_globals, name);
...
2369 if (v == NULL) {
2370 if (PyDict_CheckExact(f->f_builtins)) {
2371 v = PyDict_GetItem(f->f_builtins, name);
2372 if (v == NULL) {
2373 format_exc_check_arg(
2374 PyExc_NameError,
2375 NAME_ERROR_MSG, name);
2376 goto error;
...
2392 PUSH(v);
2393 DISPATCH();
##LOAD_ATTR
次はLOAD_ATTRです。2857行目のGETITEMマクロで"b"に対するPyObjectを取得します。TOP()マクロを利用してLOAD_NAMEがスタックに積んだオブジェクトを取得します。この2つのオブジェクトを2859行目のPyObject_GetAttr関数に渡して,その結果をスタックにセットします。
TARGET(LOAD_ATTR) {
2857 PyObject *name = GETITEM(names, oparg);
2858 PyObject *owner = TOP();
2859 PyObject *res = PyObject_GetAttr(owner, name);
2860 Py_DECREF(owner);
2861 SET_TOP(res);
2862 if (res == NULL)
2863 goto error;
2864 DISPATCH();
}
##PyObject_GetAttr関数の処理
PyObject_GetAttrは関数は下記のように,第1引数として渡されたオブジェクト("a.b"の"a")型情報を取得後,その型が持つ関数ポインタtp_getattroを実行します。
880 PyObject_GetAttr(PyObject *v, PyObject *name)
881 {
882 PyTypeObject *tp = Py_TYPE(v);
...
890 if (tp->tp_getattro != NULL)
891 return (*tp->tp_getattro)(v, name);
...
##_PyObject_GenericAttrWithDict関数
上記の関数ポインタは,PyObject_GenericGetAttr関数をコールします。(gdbで確認)_PyObject_GenericAttrWithDictをコールします。この関数が属性名解決の肝となります。まずは1053行目の_PyType_Lookup関数でそのオブジェクトのクラスが参照したい属性名を保持しているか検索します。このとき継承元となるクラスも検索対象に含まれます。もし見つかった場合は,その属性名の参照先がデータディスクリプタかどうかをチェックします(1059行目)。データディスクリプタであれば,その参照先は__get__特殊メソッドを保持していますので,これをコールしてその結果を返します。参照先がデータディスクリプタでない場合は,そのオブジェクト自身がその属性名を保持しているかを確認します(1065〜1086行目)。保持していれば,その参照先をそのまま返します。保持していない場合は,1098行目以降に処理が移動します。ここでは,オブジェクトのクラスが持っていた参照先をもう一度チェックします。その参照先が非データディスクリプタであれば,__get__特殊メソッドをコールしてその結果を返します。非データディスクリプタでない場合は,その参照先をそのまま返します。また,どこにもその属性が存在しない場合には,AttributeErrorを返します(1109〜1111行目)。
1030 PyObject *
1031 _PyObject_GenericGetAttrWithDict(PyObject *obj, PyObject *name, PyObject *dict)
1032 {
1033 PyTypeObject *tp = Py_TYPE(obj);
...
1053 descr = _PyType_Lookup(tp, name);
...
1056 if (descr != NULL) {
...
1058 f = descr->ob_type->tp_descr_get;
1059 if (f != NULL && PyDescr_IsData(descr)) {
1060 res = f(descr, obj, (PyObject *)obj->ob_type);
1061 goto done;
...
1065 if (dict == NULL) {
1066 /* Inline _PyObject_GetDictPtr */
1067 dictoffset = tp->tp_dictoffset;
...
1083 dictptr = (PyObject **) ((char *)obj + dictoffset);
1084 dict = *dictptr;
...
1086 }
1087 if (dict != NULL) {
...
1089 res = PyDict_GetItem(dict, name);
1090 if (res != NULL) {
...
1093 goto done;
1094 }
...
1098 if (f != NULL) {
1099 res = f(descr, obj, (PyObject *)Py_TYPE(obj));
1100 goto done;
1101 }
1103 if (descr != NULL) {
1104 res = descr;
....
1106 goto done;
...
1109 PyErr_Format(PyExc_AttributeError,
1110 "'%.50s' object has no attribute '%U'",
1111 tp->tp_name, name);
1112 done:
...
1115 return res;
1116 }
#おわりに
上記のように,属性名の解決処理は_PyObject_GenericAttrWithDict関数で実装されています。Pythonの名前解決で不明な点が出てきた場合には,gdbを用いてこの関数をステップ実行すると処理の詳細を把握することができます。