CPythonが属性名を解決する過程をgdbで確認する

こちらの投稿では，Pythonがあるオブジェクトに与えられた属性名をどのように解決するのか，CPythonの実装レベルで見てきます。バイトコードをCのレベルで確認するための事前準備は[こちらの投稿](https://qiita.com/satote2/items/de3f65353459d5e1ec14)を参考にしてください。

#バイトコードへの変換
属性名の参照は"a.b"とすることで実行されます。このため，まずは"a.b"をバイトコードに変換します。

```py
  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マクロによってスタックにオブジェクトを積みます。

```c
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がスタックに積んだオブジェクトを取得します。この２つのオブジェクトを2859行目のPyObject_GetAttr関数に渡して，その結果をスタックにセットします。

```c:python/ceval.c
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は関数は下記のように，第１引数として渡されたオブジェクト（"a.b"の"a"）型情報を取得後，その型が持つ関数ポインタtp_getattroを実行します。

```c:Objects/object.c
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行目)。

```c:Objects/object.c
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を用いてこの関数をステップ実行すると処理の詳細を把握することができます。