getattr 是 Python 中的一個内建函數,在官方的 getattr 解釋中是這樣説明的

getattr(object, name[, default]) -> value
Get a named attribute from an object; getattr(x, 'y') is equivalent to x.y.
When a default argument is given, it is returned when the attribute doesn't
exist; without it, an exception is raised in that case.

你可以給定一個 object,并通過 name string 獲取命名屬性,如果沒有設置默認參數,查找屬性不存在,就會引發異常

class A:
    pass
a = A()
item = getattr(a, "name")
Traceback (most recent call last):
  File "E:\test.py", line 7, in <module>
    item = getattr(a, "name")
AttributeError: 'A' object has no attribute 'name'

你可以給定一個默認參數,儅屬性查找不存在時,會返回給定的默認值

class A:
    pass
a = A()
item = getattr(a, "name", "Peter")
print(item)
Peter
Process finished with exit code 0

getattr 方法最主要的作用的實現反射機制,可以動態獲取對象的屬性,還有 setattr、hasattr、delattr 類似的方法,那麽 getattr 是如何實現屬性查找的呢?

# getattr 的背後實現機制

getattr 是 Python 中的内置函數,可以查看源代碼的 Python/bltinmodule.c 文件,在大概第 2703 行有很多方法

static PyMethodDef builtin_methods[] = {
    {"__build_class__", (PyCFunction)(void(*)(void))builtin___build_class__,
     METH_FASTCALL | METH_KEYWORDS, build_class_doc},
    {"__import__",      (PyCFunction)(void(*)(void))builtin___import__, METH_VARARGS | METH_KEYWORDS, import_doc},
    BUILTIN_ABS_METHODDEF
    BUILTIN_ALL_METHODDEF
    BUILTIN_ANY_METHODDEF
    BUILTIN_ASCII_METHODDEF
    BUILTIN_BIN_METHODDEF
    {"breakpoint",      (PyCFunction)(void(*)(void))builtin_breakpoint, METH_FASTCALL | METH_KEYWORDS, breakpoint_doc},
    BUILTIN_CALLABLE_METHODDEF
    BUILTIN_CHR_METHODDEF
    BUILTIN_COMPILE_METHODDEF
    BUILTIN_DELATTR_METHODDEF
    {"dir",             builtin_dir,        METH_VARARGS, dir_doc},
    BUILTIN_DIVMOD_METHODDEF
    BUILTIN_EVAL_METHODDEF
    BUILTIN_EXEC_METHODDEF
    BUILTIN_FORMAT_METHODDEF
    {"getattr",         (PyCFunction)(void(*)(void))builtin_getattr, METH_FASTCALL, getattr_doc},
    BUILTIN_GLOBALS_METHODDEF
    BUILTIN_HASATTR_METHODDEF
    BUILTIN_HASH_METHODDEF
    BUILTIN_HEX_METHODDEF
    BUILTIN_ID_METHODDEF
    BUILTIN_INPUT_METHODDEF
    BUILTIN_ISINSTANCE_METHODDEF
    BUILTIN_ISSUBCLASS_METHODDEF
    {"iter",            (PyCFunction)(void(*)(void))builtin_iter,       METH_FASTCALL, iter_doc},
    BUILTIN_LEN_METHODDEF
    BUILTIN_LOCALS_METHODDEF
    {"max",             (PyCFunction)(void(*)(void))builtin_max,        METH_VARARGS | METH_KEYWORDS, max_doc},
    {"min",             (PyCFunction)(void(*)(void))builtin_min,        METH_VARARGS | METH_KEYWORDS, min_doc},
    {"next",            (PyCFunction)(void(*)(void))builtin_next,       METH_FASTCALL, next_doc},
    BUILTIN_OCT_METHODDEF
    BUILTIN_ORD_METHODDEF
    BUILTIN_POW_METHODDEF
    {"print",           (PyCFunction)(void(*)(void))builtin_print,      METH_FASTCALL | METH_KEYWORDS, print_doc},
    BUILTIN_REPR_METHODDEF
    BUILTIN_ROUND_METHODDEF
    BUILTIN_SETATTR_METHODDEF
    BUILTIN_SORTED_METHODDEF
    BUILTIN_SUM_METHODDEF
    {"vars",            builtin_vars,       METH_VARARGS, vars_doc},
    {NULL,              NULL},
};

而儅調用 getattr 方法時,就會執行 builtin_getattr 函數

/* AC: cannot convert yet, as needs PEP 457 group support in inspect */
static PyObject *
builtin_getattr(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
{
    PyObject *v, *name, *result;
    if (!_PyArg_CheckPositional("getattr", nargs, 2, 3))
        return NULL;
    v = args[0];  // 取出對象,getattr (obj, "xxx"), v 就是 obj 對象
    name = args[1]; // 取出字符串,也就是 xxx
    if (!PyUnicode_Check(name)) {
	// 類型檢查,如果 name string 不是字符串類型,就會抛出 getattr (): attribute name must be string 異常
        PyErr_SetString(PyExc_TypeError,
                        "getattr(): attribute name must be string");
        return NULL;
    }
    if (nargs > 2) {
        // 如果設置了默認參數,就會執行這裏的代碼
        if (_PyObject_LookupAttr(v, name, &result) == 0) {
            PyObject *dflt = args[2];
            Py_INCREF(dflt);
            return dflt;
        }
    }
    else {
        // 沒有設置默認參數
        result = PyObject_GetAttr(v, name);
    }
    return result;
}

可以看到 builtin_getattr 方法還是比較簡單的,一進來先檢查 name string 是否為字符串類型,否則就抛異常

class A:
    pass
a = A()
item = getattr(a, 1, "Peter")
Traceback (most recent call last):
  File "E:\herokuapp\dawei-base\tests\test.py", line 7, in <module>
    item = getattr(a, 1, "Peter")
TypeError: getattr(): attribute name must be string

如果設置了默認參數,也就是 getattr (a, “name”, “Peter”) 這種寫法,就會調用 _PyObject_LookupAttr 方法,分別傳入了 a 對象、“name” 字符串,以及 &result, 儅屬性找到時就會賦值 result,并返回 result,那如果沒有找到屬性,就會通過 args [2] 獲取到給定的默認參數,并將該參數返回

查看_PyObject_LookupAttr 的代碼調用

int
_PyObject_LookupAttr(PyObject *v, PyObject *name, PyObject **result)
{
    PyTypeObject *tp = Py_TYPE(v);
    if (!PyUnicode_Check(name)) {
        PyErr_Format(PyExc_TypeError,
                     "attribute name must be string, not '%.200s'",
                     Py_TYPE(name)->tp_name);
        *result = NULL;
        return -1;
    }
    if (tp->tp_getattro == PyObject_GenericGetAttr) {
	// 在對象沒有重載 tp_getattro 方法時,會調用_PyObject_GenericGetAttrWithDict 方法查找
        *result = _PyObject_GenericGetAttrWithDict(v, name, NULL, 1);
        if (*result != NULL) {
	    // 1 為找到了
            return 1;
        }
        if (PyErr_Occurred()) {
            //-1 為報異常
            return -1;
        }
	// 沒有找到
        return 0;
    }
    if (tp->tp_getattro != NULL) {
	//tp_getattro 被我們重載了,就會調用我們自己的 tp_getattro 方法
        *result = (*tp->tp_getattro)(v, name);
    }
    else if (tp->tp_getattr != NULL) {
	// 如果 tp_getattro 也沒有重載,就會判斷 tp_getattr 是否被重載
        const char *name_str = PyUnicode_AsUTF8(name);
        if (name_str == NULL) {
            *result = NULL;
            return -1;
        }
        *result = (*tp->tp_getattr)(v, (char *)name_str);
    }
    else {
	// 既沒有重載 tp_getattro,也沒有重載 tp_getattr,抛異常
        *result = NULL;
        return 0;
    }
    if (*result != NULL) {
	// 找到屬性了
        return 1;
    }
    if (!PyErr_ExceptionMatches(PyExc_AttributeError)) {
        return -1;
    }
    PyErr_Clear();
    // 沒有找到
    return 0;
}

那在我們沒有重載 tp_getattro 方法時,會執行_PyObject_GenericGetAttrWithDict 方法,代碼還挺長😶

/* Generic GetAttr functions - put these in your tp_[gs]etattro slot. */
PyObject *
_PyObject_GenericGetAttrWithDict(PyObject *obj, PyObject *name,
                                 PyObject *dict, int suppress)
{
    /* Make sure the logic of _PyObject_GetMethod is in sync with
       this method.
       When suppress=1, this function suppress AttributeError.
    */
    PyTypeObject *tp = Py_TYPE(obj); // 獲取 obj 的父類
    PyObject *descr = NULL;
    PyObject *res = NULL;
    descrgetfunc f;
    Py_ssize_t dictoffset;
    PyObject **dictptr;
    if (!PyUnicode_Check(name)){
        PyErr_Format(PyExc_TypeError,
                     "attribute name must be string, not '%.200s'",
                     Py_TYPE(name)->tp_name);
        return NULL;
    }
    Py_INCREF(name); //name string 引用計數 + 1
    if (tp->tp_dict == NULL) {
        if (PyType_Ready(tp) < 0)
            goto done;
    }
    /*
    _PyType_Lookup 代碼調用比較多,就不粘出來了,它裏面做了一些對屬性值的緩存,最主要的是通過 MRO 查找屬性,
    它會通過 a 對象的父類,也就是 A 類,獲取 A 類的 MRO,并通過遍歷 MRO,獲取每個類的__dict__,再通過 hash 查找
    */
    descr = _PyType_Lookup(tp, name);
    f = NULL;
    if (descr != NULL) {
	// 如果找到屬性了,
        Py_INCREF(descr);
	// 獲取__get__方法
        f = Py_TYPE(descr)->tp_descr_get;
	// 判斷是否__get__不爲空并且__set__不爲空
        if (f != NULL && PyDescr_IsData(descr)) {
	    // 調用__get__方法
            res = f(descr, obj, (PyObject *)Py_TYPE(obj));
            if (res == NULL && suppress &&
                    PyErr_ExceptionMatches(PyExc_AttributeError)) {
                PyErr_Clear();
            }
            goto done;
        }
    }
    if (dict == NULL) {
        /* Inline _PyObject_GetDictPtr */
        dictoffset = tp->tp_dictoffset;
        if (dictoffset != 0) {
            if (dictoffset < 0) {
                Py_ssize_t tsize = Py_SIZE(obj);
                if (tsize < 0) {
                    tsize = -tsize;
                }
                size_t size = _PyObject_VAR_SIZE(tp, tsize);
                _PyObject_ASSERT(obj, size <= PY_SSIZE_T_MAX);
                dictoffset += (Py_ssize_t)size;
                _PyObject_ASSERT(obj, dictoffset > 0);
                _PyObject_ASSERT(obj, dictoffset % SIZEOF_VOID_P == 0);
            }
            dictptr = (PyObject **) ((char *)obj + dictoffset);
            dict = *dictptr;
        }
    }
    if (dict != NULL) {
	// 進入該 if statement, 説明__get__或者__set__沒有定義,就在__dict__裏邊找
        Py_INCREF(dict);
        res = PyDict_GetItemWithError(dict, name);
        if (res != NULL) {
            Py_INCREF(res);
            Py_DECREF(dict);
            goto done;
        }
        else {
            Py_DECREF(dict);
            if (PyErr_Occurred()) {
                if (suppress && PyErr_ExceptionMatches(PyExc_AttributeError)) {
                    PyErr_Clear();
                }
                else {
                    goto done;
                }
            }
        }
    }
    if (f != NULL) {
	// 進入該 if statement, 説明__dict__裏也沒有,那就就調用__get__方法
        res = f(descr, obj, (PyObject *)Py_TYPE(obj));
        if (res == NULL && suppress &&
                PyErr_ExceptionMatches(PyExc_AttributeError)) {
            PyErr_Clear();
        }
        goto done;
    }
    if (descr != NULL) {
	// 如果走到這,判斷自己不爲 NULL 就返回本身,意思是__get__和__set__都沒有定義,并且__dict__也沒有找到
        res = descr;
        descr = NULL;
        goto done;
    }
    if (!suppress) {
        PyErr_Format(PyExc_AttributeError,
                     "'%.50s' object has no attribute '%U'",
                     tp->tp_name, name);
    }
  done:
    Py_XDECREF(descr);
    Py_DECREF(name);
    return res;
}

_PyObject_GenericGetAttrWithDict 處理流程

  1. 獲取查找類的父類,并取出 MRO,遍歷 MRO 查找屬性
  2. 如果找到屬性,會判斷該對象是不是一個 Descriptor
  3. 如果對象定義了__get__并且定義了__set__,調用__get__并返回
  4. 如果沒有定義__set__,就在__dict__裏邊找
  5. 如果__dict__也沒有找到,就調用__get__并返回
  6. 如果__get__也沒有定義,就把自身返回

可以看到,光查找屬性就做了不少的處理,那如果在我們重載了 tp_getattro 的情況下,它就會調用如下代碼

if (tp->tp_getattro != NULL) {
    // 定義了__getattribute__方法,調用并傳遞實例對象 v,也就是上面説的 a,還有 name string
    *result = (*tp->tp_getattro)(v, name);
}

在我們為類中定義了__getattribute__方法時,就會在 getattr (a, “name”, “豬豬俠”) 時被調用,並傳遞 a,name 到__getattribute__裏作爲參數

class Name:
    def __get__(self, instance, owner):
        return "Peter"
class A:
    name = Name()
    def __getattribute__(self, item):
        print(self, item)
        return item
a = A()
item = getattr(a, "name", "豬豬俠")
打印結果: <__main__.A object at 0x000001BC1C306FA0> name

對於__getattr__方法,需要滿足兩個條件

  1. 訪問對象屬性
  2. 觸發 AttributeError 異常
class A:
    name = "Peter"
    def __getattr__(self, item):
        print(item)
a = A()
item = getattr(a, "sex")
打印結果: sex

如果經過一系列的找到屬性都沒有找到,并且你給定了默認參數,就會在你沒有找到的情況下,將默認參數返回給你

if (_PyObject_LookupAttr(v, name, &result) == 0) {
    // 0 表示沒有找到
    PyObject *dflt = args[2]; // 取出默認參數
    Py_INCREF(dflt); // 引用計數 + 1
    return dflt; // 返回
}

在我們只給了兩個參數的情況下,如 getattr (a, “name”),此時會調用 PyObject_GetAttr (v, name)

PyObject *
PyObject_GetAttr(PyObject *v, PyObject *name)
{
    PyTypeObject *tp = Py_TYPE(v); // 獲取父類
    if (!PyUnicode_Check(name)) {
	// 類型檢查
        PyErr_Format(PyExc_TypeError,
                     "attribute name must be string, not '%.200s'",
                     Py_TYPE(name)->tp_name);
        return NULL;
    }
    if (tp->tp_getattro != NULL)
        return (*tp->tp_getattro)(v, name);
    if (tp->tp_getattr != NULL) {
        const char *name_str = PyUnicode_AsUTF8(name);
        if (name_str == NULL)
            return NULL;
        return (*tp->tp_getattr)(v, (char *)name_str);
    }
    PyErr_Format(PyExc_AttributeError,
                 "'%.50s' object has no attribute '%U'",
                 tp->tp_name, name);
    return NULL;
}

處理的事情跟_PyObject_LookupAttr 方法挺相似的,在沒有重載 tp_getattro 的情況下都是調用了_PyObject_GenericGetAttrWithDict 方法,只不過_PyObject_LookupAttr 給_PyObject_GenericGetAttrWithDict 的參數 suppress 傳遞了 1,而調用 PyObject_GetAttr 時,傳遞 suppress 為 0

Make sure the logic of _PyObject_GetMethod is in sync with this method.
When suppress=1, this function suppress AttributeError.

儅 suppress 為 0 時,如果找不到屬性,則會抛出異常

if (!suppress) {
    PyErr_Format(PyExc_AttributeError,
                 "'%.50s' object has no attribute '%U'",
                 tp->tp_name, name);
}
Traceback (most recent call last):
  File "E:\test.py", line 12, in <module>
    item = getattr(a, "age")
AttributeError: 'A' object has no attribute 'age'

儅 getattr 設置了默認參數,suppress 就為 1,在沒有找到的情況下,調用_PyObject_GenericGetAttrWithDict 不會引發異常,并在整個查找結果返回為 0 的情況下,會將默認參數取出并作爲結果返回

Edited on Views times

Give me a cup of [coffee]~( ̄▽ ̄)~*

小芳芳 WeChat Pay

WeChat Pay

小芳芳 Alipay

Alipay