参考文档:
关于描述器相关定义及使用说明参考: python描述器使用指南
结论
先上结论:
当访问对象的某个属性时, 优先级先后顺序为:
定义了__get__和__set__方法的描述器 > 实例字典 > 只定义了__get__方法的描述器 > 类字典
property是一个定义了__get__, __set__, __delete__的描述器!即使只声明了fget,tp_descr_get与tp_descr_set也都不为NULL。
python代码验证
定义了__get__和__set__方法的描述器与实例字典
class NameDescr:
"""
name描述器
"""
def __get__(self, instance, owner):
return "Descr"
def __set__(self, instance, value):
pass
class Cat(object):
name = NameDescr()
cat = Cat()
print(cat.name) # Descr
cat.__dict__['name'] = "Tom"
print(cat.name) # Descr 定义了__get__和__set__方法的描述器优先级大于实例字典
实例字典与只定义了__get__方法的描述器
class NameDescr:
"""
name描述器
"""
def __get__(self, instance, owner):
return "Descr"
class Cat(object):
name = NameDescr()
cat = Cat()
print(cat.name) # Descr
cat.__dict__['name'] = "Tom"
print(cat.name) # Tom 实例字典的优先级大于未定义__set__方法的描述器
实例字典与类字典
class Cat(object):
name = "Tom"
def set_name(self, name):
self.name = name
cat = Cat()
print(cat.name) # Tom
cat.set_name("Not Tom")
print(cat.name) # Not Tom 实例字典大于类字典
获取属性
参考译|Python 幕后(7):属性机制内容,获取对象一个属性的基础流程为
对应的字节码: LOAD_ATTR
zh@ubuntu:~$ echo 'obj.attr' | python3.8 -m dis
1 0 LOAD_NAME 0 (obj)
2 LOAD_ATTR 1 (attr)
4 POP_TOP
6 LOAD_CONST 0 (None)
8 RETURN_VALUE
对应字节码的处理:
case TARGET(LOAD_ATTR): {
PyObject *name = GETITEM(names, oparg);
PyObject *owner = TOP();
PyObject *res = PyObject_GetAttr(owner, name);
Py_DECREF(owner);
SET_TOP(res);
if (res == NULL)
goto error;
DISPATCH();
}
可以看到, 实际处理的函数是PyObject_GetAttr函数, 查看该函数的流程
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'",
name->ob_type->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;
}
- tp_getattro
// 作用: 指向get-attribute函数的可选指针。通常把此字段设置为PyObject_GenericGetAttr(),它实现了查找对象属性的正常方式。
// 定义: typedef PyObject *(*getattrofunc)(PyObject *, PyObject *);
// 接受类对象、str对象两个参数
- tp_getattr
// 作用: 指向get-attribute-string函数的可选指针
// 定义: typedef PyObject *(*getattrfunc)(PyObject *, char *);
// 与tp_getattro不同的是,第二个参数接受的是c的char *字符串。此字段已弃用。当它被定义时,它应该指向一个与tp_getattro函数作用相同的函数,但使用C字符串而不是Python字符串对象来给出属性名。
一般情况下, tp_get_attr是NULL
tp_getattro,会调用到PyObject_GenericGetAttr
PyObject *
PyObject_GenericGetAttr(PyObject *obj, PyObject *name)
{
return _PyObject_GenericGetAttrWithDict(obj, name, NULL, 0);
}
_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);
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'",
name->ob_type->tp_name);
return NULL;
}
Py_INCREF(name);
if (tp->tp_dict == NULL) {
if (PyType_Ready(tp) < 0)
goto done;
}
/* 先从实例对象的类及__mro__父类中, 寻找属性 */
/* 为什么先从类中寻找: 因为描述符直接定义在类对象中的, 为了保证描述符的优先级要比实例字典中获取的优先级要高
* 所以先从类中查找, 如果找到了描述符, 就直接通过描述符取值, 不从实例字典中获取了. */
descr = _PyType_Lookup(tp, name);
f = NULL;
if (descr != NULL) {
Py_INCREF(descr);
// 如果在类及__mro__父类中找到了属性, 查看是否定义了__get__方法, 定义了__get__方法的是描述器
f = descr->ob_type->tp_descr_get;
// Py_TYPE(descr)->tp_descr_set != NULL, 如果tp_descr_set也设置了, 直接调用描述器__get__方法, 否则优先从实例字典中查
if (f != NULL && PyDescr_IsData(descr)) {
res = f(descr, obj, (PyObject *)obj->ob_type);
if (res == NULL && suppress &&
PyErr_ExceptionMatches(PyExc_AttributeError)) {
PyErr_Clear();
}
goto done;
}
}
if (dict == NULL) {
/* Inline _PyObject_GetDictPtr */
/* PyObject_GenericGetAttr调用时, dict传的是NULL: _PyObject_GenericGetAttrWithDict(obj, name, NULL, 0);
* 这里去找实例字典 */
dictoffset = tp->tp_dictoffset;
if (dictoffset != 0) {
if (dictoffset < 0) {
Py_ssize_t tsize;
size_t size;
tsize = ((PyVarObject *)obj)->ob_size;
if (tsize < 0)
tsize = -tsize;
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) {
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;
}
}
}
}
// 实例字典中没查到属性, 且在类及__mro__中查到了定义了__get__方法的描述器, 调用描述器__get__方法获取属性值
if (f != NULL) {
res = f(descr, obj, (PyObject *)Py_TYPE(obj));
if (res == NULL && suppress &&
PyErr_ExceptionMatches(PyExc_AttributeError)) {
PyErr_Clear();
}
goto done;
}
if (descr != NULL) {
// 走到这里, 说明从类字典中查到的不是描述器, 而是是一个普通对象, 直接返回该对象
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;
}
python类定义中__getattribute__和__getattr__函数的含义以及要注意的点
__getattr__
当默认属性访问因引发 AttributeError 而失败时被调用,此方法应当返回(找到的)属性值或是引发一个 AttributeError 异常。如果属性是通过正常机制找到的,__getattr__() 就不会被调用。
__getattribute__
此方法会无条件地被调用以实现对类实例属性的访问。如果类还定义了 __getattr__(),则后者不会被调用,除非 __getattribute__() 显式地调用它或是引发了 AttributeError。此方法应当返回(找到的)属性值或是引发一个 AttributeError 异常。为了避免此方法中的无限递归,其实现应该总是调用具有相同名称的基类方法来访问它所需要的任何属性,例如 object.__getattribute__(self, name)。
需要注意的点:
- 重写
__getattribute__()会覆盖类对象的tp_getattro方法,改变默认获取对象属性的流程,阻止描述器的自动调用
class Cat(object):
def get_name(self):
return "Tom"
name = property(fget=get_name)
cat = Cat()
print(cat.name) # Tom
Cat.__getattribute__ = lambda self, item: "Not Tom"
print(cat.name) # Not Tom 定义__getattribute__方法会覆盖python的默认属性获取机制