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 處理流程
- 獲取查找類的父類,并取出 MRO,遍歷 MRO 查找屬性
- 如果找到屬性,會判斷該對象是不是一個 Descriptor
- 如果對象定義了__get__并且定義了__set__,調用__get__并返回
- 如果沒有定義__set__,就在__dict__裏邊找
- 如果__dict__也沒有找到,就調用__get__并返回
- 如果__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__方法,需要滿足兩個條件
- 訪問對象屬性
- 觸發 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 的情況下,會將默認參數取出并作爲結果返回