# 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 的内容差不多就这些了,了解一个东西是怎么实现的其实非常有趣,最起码你知道它是怎么跑的🤭