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

Edited on Views times

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

小芳芳 WeChat Pay

WeChat Pay

小芳芳 Alipay

Alipay