# Python @property 的源码分析
property 在 Python 中是一种装饰器,可以用来修饰方法
# 作用
我们可以使用 @property 装饰器来创建只读属性,@property 装饰器会将方法转换为相同名称的只读属性,可以与所定义的属性配合使用,以防止属性被修改
property 具体的使用案例可以参考:https://www.runoob.com/python/python-func-property.html,里面有详细介绍
以下的用法是最常用的
class GHost: | |
def __init__(self) -> None: | |
self.__name = "刹耶" | |
@property | |
def name(self): | |
return self.__name | |
@name.setter | |
def name(self, val): | |
self.__name = val | |
ghost = GHost() |
我们可以直接通过调用 ghost.name 获取到名称,而不需要()进行调用,当执行 ghost.name = xxx 时又会触发赋值操作,我们来看看 CPython 是如何做到的
首先 property 是一个内置类,先看看 property 的结构定义
PyTypeObject PyProperty_Type = { | |
PyVarObject_HEAD_INIT(&PyType_Type, 0) | |
"property", /* tp_name */ | |
sizeof(propertyobject), /* tp_basicsize */ | |
0, /* tp_itemsize */ | |
/* methods */ | |
property_dealloc, /* tp_dealloc */ | |
...... // 省略,不需要 | |
PyObject_GenericGetAttr, /* tp_getattro */ | |
0, /* tp_setattro */ | |
0, /* tp_as_buffer */ | |
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | | |
Py_TPFLAGS_BASETYPE, /* tp_flags */ | |
property_init__doc__, /* tp_doc */ | |
property_traverse, /* tp_traverse */ | |
(inquiry)property_clear, /* tp_clear */ | |
0, /* tp_richcompare */ | |
0, /* tp_weaklistoffset */ | |
0, /* tp_iter */ | |
0, /* tp_iternext */ | |
property_methods, /* tp_methods */ | |
property_members, /* tp_members */ | |
property_getsetlist, /* tp_getset */ | |
0, /* tp_base */ | |
0, /* tp_dict */ | |
property_descr_get, /* tp_descr_get */ | |
property_descr_set, /* tp_descr_set */ | |
0, /* tp_dictoffset */ | |
property_init, /* tp_init */ | |
PyType_GenericAlloc, /* tp_alloc */ | |
PyType_GenericNew, /* tp_new */ | |
PyObject_GC_Del, /* tp_free */ | |
}; |
property 定义了 tp_descr_get 与 tp_descr_set,说明它是一个描述器,先来看看初始化时做了什么,调用的是 property_init 方法
static int | |
property_init(PyObject *self, PyObject *args, PyObject *kwargs) | |
{ | |
int return_value = -1; // 返回值 | |
static const char * const _keywords[] = {"fget", "fset", "fdel", "doc", NULL}; // 一些关键字参数 | |
static _PyArg_Parser _parser = {NULL, _keywords, "property", 0}; | |
PyObject *argsbuf[4]; | |
PyObject * const *fastargs; | |
Py_ssize_t nargs = PyTuple_GET_SIZE(args); // 传递了多少个位置参数 | |
// 可以传递位置参数或者关键字参数,然后如果传递了关键字,就将关键字的数量也相加 | |
Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 0; | |
PyObject *fget = NULL; // 获取属性值的方法 | |
PyObject *fset = NULL; // 设置属性值的方法 | |
PyObject *fdel = NULL; // 删除属性值的方法 | |
PyObject *doc = NULL; // 属性描述信息 | |
// 参数校验 | |
fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, 0, 4, 0, argsbuf); | |
if (!fastargs) { | |
goto exit; | |
} | |
if (!noptargs) { | |
goto skip_optional_pos; | |
} | |
// 重点在这边 | |
//noptargs 是传递了位置参数和关键字参数的数量总和 | |
if (fastargs[0]) { | |
// 如果传递了 fget 参数,就赋值给 fget 变量 | |
fget = fastargs[0]; | |
if (!--noptargs) { | |
goto skip_optional_pos; | |
} | |
} | |
if (fastargs[1]) { | |
// 如果传递了 fset 参数,就赋值 fset 变量 | |
fset = fastargs[1]; | |
if (!--noptargs) { | |
goto skip_optional_pos; | |
} | |
} | |
if (fastargs[2]) { | |
// 如果传递了 fdel 参数,就赋值 fdel 变量 | |
fdel = fastargs[2]; | |
if (!--noptargs) { | |
goto skip_optional_pos; | |
} | |
} | |
doc = fastargs[3]; // 属性描述信息 | |
skip_optional_pos: | |
return_value = property_init_impl((propertyobject *)self, fget, fset, fdel, doc); //property 的一些赋值操作 | |
exit: | |
return return_value; | |
} | |
static int | |
property_init_impl(propertyobject *self, PyObject *fget, PyObject *fset, | |
PyObject *fdel, PyObject *doc) | |
/*[clinic end generated code: output=01a960742b692b57 input=dfb5dbbffc6932d5]*/ | |
{ | |
if (fget == Py_None) | |
fget = NULL; | |
if (fset == Py_None) | |
fset = NULL; | |
if (fdel == Py_None) | |
fdel = NULL; | |
Py_XINCREF(fget); // 引用计数 + 1,但可以处理空指针 | |
Py_XINCREF(fset); | |
Py_XINCREF(fdel); | |
Py_XINCREF(doc); | |
Py_XSETREF(self->prop_get, fget); // 通过 Py_XSETREF 宏来改变 self->prop_get 指针指向 fget,下面是一样的 | |
Py_XSETREF(self->prop_set, fset); | |
Py_XSETREF(self->prop_del, fdel); | |
Py_XSETREF(self->prop_doc, doc); | |
self->getter_doc = 0; | |
/* if no docstring given and the getter has one, use that one */ | |
if ((doc == NULL || doc == Py_None) && fget != NULL) { | |
// 如果没有给属性描述信息,就会尝试从 fget 方法从获取 docstring 赋值到 property 中 | |
_Py_IDENTIFIER(__doc__); | |
PyObject *get_doc; | |
int rc = _PyObject_LookupAttrId(fget, &PyId___doc__, &get_doc); // 在 fget 中取出__doc__ | |
if (rc <= 0) { | |
// 没有__doc__就算了,直接返回结束 | |
return rc; | |
} | |
if (Py_IS_TYPE(self, &PyProperty_Type)) { | |
// 如果__doc__有,判断实例的类型是否为 PyProperty_Type,符合就赋值__doc__ | |
Py_XSETREF(self->prop_doc, get_doc); | |
} | |
else { | |
/* If this is a property subclass, put __doc__ | |
in dict of the subclass instance instead, | |
otherwise it gets shadowed by __doc__ in the | |
class's dict. */ | |
int err = _PyObject_SetAttrId((PyObject *)self, &PyId___doc__, get_doc); // 否则我就在实例对象里获取__doc__并赋值 | |
Py_DECREF(get_doc); | |
if (err < 0) | |
return -1; | |
} | |
self->getter_doc = 1; | |
} | |
return 0; | |
} |
整体逻辑比较简单,调用 property 之后,将获取、设置、删除等方法取出并设置到 property 实例对象的属性中,需要注意,当你使用 @property 装饰一个 def name 时,name 将不再是一个 function object,而是变为了 property object,所以当你再使用 @name.setter 来装饰一个 name 方法来处理赋值操作时,其实并没什么神奇的,因为 name 此时是一个 property object,你调用的实际上是 property 的 setter method,可以看一下 property 支持什么方法
static PyMethodDef property_methods[] = { | |
{"getter", property_getter, METH_O, getter_doc}, | |
{"setter", property_setter, METH_O, setter_doc}, | |
{"deleter", property_deleter, METH_O, deleter_doc}, | |
{0} | |
}; |
先说一下,前面讲到了 property 是一个描述器,在我们没有使用 @property 装饰 def name 时,我们需要执行 name () 才能调用到 name 方法(需要加双括号),如果使用 @property 装饰 def name 后,只需要 .name 就可以调用拿到返回值,它是怎么做到的?
首先,name 是定义在我们的类中,所以 name 属性会先通过 MRO 查找,找到之后,判断 name 是否为一个描述器,那么一开始 name 只是一个普通的 function object,如果通过.name 你只能获取到一个 method obejct,也就是 < bound method GHost.name of <main.GHost object at 0x00000252FF6D6CD0>>,而不是 name 的值,但是,我们通过 property 装饰之后,它就变成了一个 property object,因为 property 重载了__get__和__set__方法,所以在我们通过 .name 时,就会调用它的__get__方法,也就是 property_descr_get 方法
static PyObject * | |
property_descr_get(PyObject *self, PyObject *obj, PyObject *type) | |
{ | |
if (obj == NULL || obj == Py_None) { | |
Py_INCREF(self); | |
return self; | |
} | |
propertyobject *gs = (propertyobject *)self; | |
if (gs->prop_get == NULL) { | |
// 如果 fget 为空,抛异常,比如你是这样定义 name = property (fset=set_name), 那么你的 fget 就为空,无法读取属性 | |
PyErr_SetString(PyExc_AttributeError, "unreadable attribute"); | |
return NULL; | |
} | |
return PyObject_CallOneArg(gs->prop_get, obj); // 调用获取属性的方法并返回 | |
} |
可以看到,当你调用 .name 时,实际上在 property 的__get__方法里调用了你指定的获取属性函数并返回的
当你调用 .name = xxx 时,就会调用 property 的__set__方法,也就是 property_descr_set
static int | |
property_descr_set(PyObject *self, PyObject *obj, PyObject *value) | |
{ | |
propertyobject *gs = (propertyobject *)self; | |
PyObject *func, *res; | |
if (value == NULL) | |
// 当你设置的值为 None 时,就会调用你的 del 方法 | |
func = gs->prop_del; | |
else | |
// 当设置的值不为空时,才会调用 set 方法 | |
func = gs->prop_set; | |
if (func == NULL) { | |
// 如果 func 为空,说明你既没有设置 set 也没有设置 del, 抛异常 | |
PyErr_SetString(PyExc_AttributeError, | |
value == NULL ? | |
"can't delete attribute" : | |
"can't set attribute"); | |
return -1; | |
} | |
if (value == NULL) | |
// 调用 del 方法 | |
res = PyObject_CallOneArg(func, obj); | |
else | |
// 调用 set 方法 | |
res = PyObject_CallFunctionObjArgs(func, obj, value, NULL); | |
if (res == NULL) | |
//-1 为调用异常 | |
return -1; | |
Py_DECREF(res); | |
return 0; // 调用成功 | |
} |
可以看到,get 与 set 的实现非常简单,那么来看看,当我们通过 @name.setter 来指定设置操作时,它做了什么事情,首先要记住,在你使用 @name.setter 装饰一个函数时,name 已经变为了 property object,此时调用的是 property 的 setter 方法,对应的是 property_setter 方法
static PyObject * | |
property_getter(PyObject *self, PyObject *getter) | |
{ | |
return property_copy(self, getter, NULL, NULL); | |
} | |
static PyObject * | |
property_setter(PyObject *self, PyObject *setter) | |
{ | |
// 这里它给 fget 和 fdel 传递了 NULL,fset 传递我们装饰的方法对象 | |
// 可以看到 property_getter,本质上做的事情是一样的,它将 fset 和 fdel 传递 NULL,而 fget 传递我们装饰的方法对象 | |
return property_copy(self, NULL, setter, NULL); | |
} | |
static PyObject * | |
property_copy(PyObject *old, PyObject *get, PyObject *set, PyObject *del) | |
{ | |
propertyobject *pold = (propertyobject *)old; | |
PyObject *new, *type, *doc; | |
type = PyObject_Type(old); // 获取 property object 的类型,也就是 ob_type | |
if (type == NULL) | |
return NULL; | |
if (get == NULL || get == Py_None) { | |
// 如果 get 参数为空,并且 property object 的 get 方法在不为空的情况下,就赋值给 get 参数 | |
Py_XDECREF(get); | |
get = pold->prop_get ? pold->prop_get : Py_None; | |
} | |
if (set == NULL || set == Py_None) { | |
// 如果 set 参数为空,并且 property object 的 set 方法在不为空的情况下,就赋值给 set 参数 | |
Py_XDECREF(set); | |
set = pold->prop_set ? pold->prop_set : Py_None; | |
} | |
if (del == NULL || del == Py_None) { | |
// 如果 del 参数为空,并且 property object 的 del 方法在不为空的情况下,就赋值给 del 参数 | |
Py_XDECREF(del); | |
del = pold->prop_del ? pold->prop_del : Py_None; | |
} | |
if (pold->getter_doc && get != Py_None) { | |
/* make _init use __doc__ from getter */ | |
doc = Py_None; | |
} | |
else { | |
doc = pold->prop_doc ? pold->prop_doc : Py_None; | |
} | |
/* | |
可以看到上面做了什么处理,get、set、del 三个都进行了判断,如果各自的参数都为空,就从 property object 中取出对应的 | |
get、set、del 方法,然后通过调用 type 并传递 get、set、del 作为参数,实例化新的对象并返回 | |
*/ | |
new = PyObject_CallFunctionObjArgs(type, get, set, del, doc, NULL); | |
Py_DECREF(type); | |
if (new == NULL) | |
return NULL; | |
return new; | |
} |
可以看到当我们执行 @name.setter 的时候做了什么,首先获取 name 的 type 类型,也就是 property,然后获取 get、set、del 方法,再通过对 property 调用,实例化一个新的对象并返回,你可以理解为原先我们只装饰了一个 function object,如下
class GHost: | |
def __init__(self) -> None: | |
self.__name = "刹耶" | |
@property | |
def name(self): | |
return self.__name |
上面的类中,在 property 初始化的时候,这个 name 方法作为 property 的 fget 参数传递进去,property 对象只有 get 方法不为空,set 和 del 都为空,那么当我们再新增一个设置方法的时候
class GHost: | |
def __init__(self) -> None: | |
self.__name = "刹耶" | |
@property | |
def name(self): | |
return self.__name | |
@name.setter | |
def name(self, val): | |
self.__name = val |
首先判断 get 方法是否为空,由于我们已经定义了 get 方法,所以它会将 get 方法取出,然后通过 property 的 type 类型再 new 一个新的出来,但是这次分别传递了 get 方法和 set 方法,这时,你的 name 对象又被更新了一次
Python property 的内容差不多就这些了,了解一个东西是怎么实现的其实非常有趣,最起码你知道它是怎么跑的🤭