# Python 的属性是如何工作的?

当我们获取或设置 Python 对象的属性时会发生什么?为了对更好理解属性是如何工作的,我们来研究一下属性是如何实现的。

本篇使用的 CPython 的 3.9.12 版本,可能与前面的版本会有变化

Python 对象是至少有两个成员的 C 结构体的实例:

  • 引用计数(a reference count)
  • 指向对象类型的指针(a pointer to the object’s type)

每个对象都必须有一个类型,因为该类型将决定对象的行为,当然,类型也是一个 Python 对象,也就是 PyTypeObject 结构的一个实例

// include/cpython/object.h
// PyTypeObject is a typedef for "struct _typeobject"
struct _typeobject {
    PyObject_VAR_HEAD
    const char *tp_name; /* For printing, in format "<module>.<name>" */
    Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */
    /* Methods to implement standard operations */
    destructor tp_dealloc;
    Py_ssize_t tp_vectorcall_offset;
    getattrfunc tp_getattr;
    setattrfunc tp_setattr;
    PyAsyncMethods *tp_as_async; /* formerly known as tp_compare (Python 2)
                                    or tp_reserved (Python 3) */
    reprfunc tp_repr;
    /* Method suites for standard classes */
    PyNumberMethods *tp_as_number;
    PySequenceMethods *tp_as_sequence;
    PyMappingMethods *tp_as_mapping;
    /* More standard operations (here for binary compatibility) */
    hashfunc tp_hash;
    ternaryfunc tp_call;
    reprfunc tp_str;
    getattrofunc tp_getattro;
    setattrofunc tp_setattro;
    /* Functions to access object as input/output buffer */
    PyBufferProcs *tp_as_buffer;
    /* Flags to define presence of optional/expanded features */
    unsigned long tp_flags;
    const char *tp_doc; /* Documentation string */
    /* Assigned meaning in release 2.0 */
    /* call function for all accessible objects */
    traverseproc tp_traverse;
    /* delete references to contained objects */
    inquiry tp_clear;
    /* Assigned meaning in release 2.1 */
    /* rich comparisons */
    richcmpfunc tp_richcompare;
    /* weak reference enabler */
    Py_ssize_t tp_weaklistoffset;
    /* Iterators */
    getiterfunc tp_iter;
    iternextfunc tp_iternext;
    /* Attribute descriptor and subclassing stuff */
    struct PyMethodDef *tp_methods;
    struct PyMemberDef *tp_members;
    struct PyGetSetDef *tp_getset;
    struct _typeobject *tp_base;
    PyObject *tp_dict;
    descrgetfunc tp_descr_get;
    descrsetfunc tp_descr_set;
    Py_ssize_t tp_dictoffset;
    initproc tp_init;
    allocfunc tp_alloc;
    newfunc tp_new;
    freefunc tp_free; /* Low-level free-memory routine */
    inquiry tp_is_gc; /* For PyObject_IS_GC */
    PyObject *tp_bases;
    PyObject *tp_mro; /* method resolution order */
    PyObject *tp_cache;
    PyObject *tp_subclasses;
    PyObject *tp_weaklist;
    destructor tp_del;
    /* Type attribute cache version tag. Added in version 2.6 */
    unsigned int tp_version_tag;
    destructor tp_finalize;
    vectorcallfunc tp_vectorcall;
};

类型的成员称为槽,每个槽负责对象的特定行为,例如类型的 tp_call,将会指定我们调用该类型的对象时会发生什么。

如何设置类型的槽取决于如何定义类型,在 CPython 中定义类型有两种方法:

  • 静态(statically)
  • 动态(dynamically)

静态定义的类型只是 PyTypeObject 的静态初始化实例,所有内置类型都是静态定义的,比如 float 类型的定义

PyTypeObject PyFloat_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "float",
    sizeof(PyFloatObject),
    0,
    (destructor)float_dealloc,                  /* tp_dealloc */
    0,                                          /* tp_vectorcall_offset */
    0,                                          /* tp_getattr */
    0,                                          /* tp_setattr */
    0,                                          /* tp_as_async */
    (reprfunc)float_repr,                       /* tp_repr */
    &float_as_number,                           /* tp_as_number */
    0,                                          /* tp_as_sequence */
    0,                                          /* tp_as_mapping */
    (hashfunc)float_hash,                       /* tp_hash */
    0,                                          /* tp_call */
    0,                                          /* tp_str */
    PyObject_GenericGetAttr,                    /* tp_getattro */
    0,                                          /* tp_setattro */
    0,                                          /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,   /* tp_flags */
    float_new__doc__,                           /* tp_doc */
    0,                                          /* tp_traverse */
    0,                                          /* tp_clear */
    float_richcompare,                          /* tp_richcompare */
    0,                                          /* tp_weaklistoffset */
    0,                                          /* tp_iter */
    0,                                          /* tp_iternext */
    float_methods,                              /* tp_methods */
    0,                                          /* tp_members */
    float_getset,                               /* tp_getset */
    0,                                          /* tp_base */
    0,                                          /* tp_dict */
    0,                                          /* tp_descr_get */
    0,                                          /* tp_descr_set */
    0,                                          /* tp_dictoffset */
    0,                                          /* tp_init */
    0,                                          /* tp_alloc */
    float_new,                                  /* tp_new */
};

如果要动态分配新类型,我们可以调用元类型,元类型是实例为 type 类型的类型。Python 有一个 type 的内置类,它是所有类型的原型,它被用作创建类的默认元类型。当 CPython 执行 class 语句时,它通常会调用 type () 来创建类。我们也可以通过直接调用 type () 来动态创建一个类

MyClass = type(name, bases, namespace)

类型的 tp_new 被调用以创建一个类,也就是__new__方法,该函数分配类型对象和空间并设置它。

所有类型都必须通过调用 PyType_Ready () 函数来初始化,PyType_Ready () 由 type_new () 调用。对于静态定义的类型,必须显式调用 PyType_Ready ()。当 CPython 启动时,它为每个内置类型调用 PyType_Ready ()。

# Attributes and the VM

在 Python 中,可以使用属性做三件事:

  • 获取属性的值:value = obj.attr
  • 设置属性值: obj.attr = value
  • 删除属性: del obj.attr

与对象行为的其它一样,这些操作的作用取决于对象的类型。一个类型有一些负责获取、设置和删除属性的槽,VM 通过调用这些槽来执行 value = obj.attr 和 obj.attr = value 等语句。

那么 VM 是如果做到的,我们通过一下方法观察:

  1. 编写一段获取 / 设置 / 删除属性的代码
  2. 使用内置 dis module,反汇编为 bytecode
  3. 生成的 bytecode 指令的实现 ceval.c

# Getting an attribute

当我们得到一个属性的值时,VM 会做什么,通过字节码观察到,编译器生成 LOAD_ATTR 操作码来加载值

>>> dis.dis('obj.attr')
1           0 LOAD_NAME                0 (obj)
            2 LOAD_ATTR                1 (attr)
            4 RETURN_VALUE

VM 执行这个操作码

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();
}

可以看到 VM 调用了 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'",
                     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;
}

它首先尝试调用 tp_getattro,如果没有实现该方法,它会再尝试调用 tp_getattr,如果也没有实现,则会引发 AttributeError。

type 实现 tp_getattro 或 tp_getattr 或者两者,都支持属性访问,唯一区别在于 tp_getattro 采用 Python 字符串作为属性的名称,而 tp_getattr 采用 C 字符串,不过 tp_getattr 已经被弃用了,转而使用 tp_getattro。

# Setting an attribute

从 VM 的角度来说,设置属性与获取属性没太大区别,编译器生成 STORE_ATTR 操作码,将属性设置为某个值

>>> dis.dis('obj.attr = value')
1           0 LOAD_NAME                0 (value)
            2 LOAD_NAME                1 (obj)
            4 STORE_ATTR               2 (attr)
            6 LOAD_CONST               0 (None)
            8 RETURN_VALUE

STORE_ATTR 操作码

case TARGET(STORE_ATTR): {
    PyObject *name = GETITEM(names, oparg);
    PyObject *owner = TOP();
    PyObject *v = SECOND();
    int err;
    STACK_SHRINK(2);
    err = PyObject_SetAttr(owner, name, v);
    Py_DECREF(v);
    Py_DECREF(owner);
    if (err != 0)
        goto error;
    DISPATCH();
}

可以发现,是通过 PyObject_SetAttr 来完成设置工作

int
PyObject_SetAttr(PyObject *v, PyObject *name, PyObject *value)
{
    PyTypeObject *tp = Py_TYPE(v);
    int err;
    if (!PyUnicode_Check(name)) {
        PyErr_Format(PyExc_TypeError,
                     "attribute name must be string, not '%.200s'",
                     Py_TYPE(name)->tp_name);
        return -1;
    }
    Py_INCREF(name);
    PyUnicode_InternInPlace(&name);
    if (tp->tp_setattro != NULL) {
        err = (*tp->tp_setattro)(v, name, value);
        Py_DECREF(name);
        return err;
    }
    if (tp->tp_setattr != NULL) {
        const char *name_str = PyUnicode_AsUTF8(name);
        if (name_str == NULL) {
            Py_DECREF(name);
            return -1;
        }
        err = (*tp->tp_setattr)(v, (char *)name_str, value);
        Py_DECREF(name);
        return err;
    }
    Py_DECREF(name);
    _PyObject_ASSERT(name, Py_REFCNT(name) >= 1);
    if (tp->tp_getattr == NULL && tp->tp_getattro == NULL)
        PyErr_Format(PyExc_TypeError,
                     "'%.100s' object has no attributes "
                     "(%s .%U)",
                     tp->tp_name,
                     value==NULL ? "del" : "assign to",
                     name);
    else
        PyErr_Format(PyExc_TypeError,
                     "'%.100s' object has only read-only attributes "
                     "(%s .%U)",
                     tp->tp_name,
                     value==NULL ? "del" : "assign to",
                     name);
    return -1;
}

# Deleting an attribute

在 Python 中,有趣的是,类型没有用于删除属性的特殊槽。那么要如何指定删除属性?通过编译器生成 DELETE_ATTR 操作码以删除一个属性

>>> dis.dis('del obj.attr')
1           0 LOAD_NAME                0 (obj)
            2 DELETE_ATTR              1 (attr)
            4 LOAD_CONST               0 (None)
            6 RETURN_VALUE

VM 通过执行这个操作码来实现

case TARGET(DELETE_ATTR): {
    PyObject *name = GETITEM(names, oparg);
    PyObject *owner = POP();
    int err;
    err = PyObject_SetAttr(owner, name, (PyObject *)NULL);
    Py_DECREF(owner);
    if (err != 0)
        goto error;
    DISPATCH();
}

删除属性时,VM 调用相同的 PyObject_SetAttr 函数来设置属性,tp_setattro 槽负责删除属性,但它通过 NULL 来指示删除该属性。

# Generic attribute management

PyObject_GenericGetAttr 和 PyObject_GenericSetAttr 函数实现了属性行为,比如将一个对象的属性设置为某个值时,CPython 会将该值放入对象的 dictionary 中

>>> class A:
...     pass
...
>>> a = A()
>>> a.__dict__
{}
>>> a.x = "x attr"
>>> a.__dict__
{'x': 'x attr'}

当我们尝试获取属性的值时,CPython 就会从对象的 dictionary 中加载它

>>> a.x
'x attr'

如果对象的 dictionary 中不存在该属性,CPython 将从该类型的 dictionary 中加载它

>>> A.y = "class y attr"
>>> a.y
'class y attr'

如果该类型的 dictionary 也不存在该属性,CPython 将会在该类型父类的 dictionary 中搜索该值

>>> class B(A):
...     pass
...
>>> b = B()
>>> b.y
'class y attr

一个对象的属性有两种情况:

  • 实例变量
  • 类型变量

实例变量存储在对象的字典中,类型变量存储在类型的字典和类型父类的字典中。要将属性设置为某个值,CPython 只需要更新对象的 dictionary。为了获取属性的值,CPython 首先会在对象的 dictionary 中查找它,如果没有找到,就会在类型的 dictionary 和类型父类的 dictionary 中查找它。CPython 在查找值时会迭代这些类型 MRO(Method ResoltionOrder)。

# Descriptors

什么是描述符,描述符是一个 Python 对象,其类型实现了 tp_descr_get 或 tp_descr_set 方法,也就是 Python 中的__get__和__set__方法。如果 PyObject_GerericGetAttr 函数在调用中发现属性值是一个描述符,其类型实现了 tp_descr_get 方法,那么就会调用 tp_descr_get 方法并返回调用结果。tp_descr_get 方法接受三个参数:描述符本身,正在查找其属性的对象和对象的类型。至于返回什么取决于 tp_descr_get 的实现。

Python 中的 property 就是基于 tp_descr_get 和 tp_descr_set 实现的:Property 源码分析

PyObject_GenericSetAttr 函数在查找当前属性值时,如果发现该值的类型实现了 tp_descr_set 方法,就会调用 tp_descr_set 方法,而不仅仅是更新对象的字典。传递给 tp_descr_set 的参数有描述符本身、对象和新属性值。要删除一个属性,PyObject_GenericSetAttr 也会调用 tp_descr_set,但将新属性值设置为 NULL。

Understanding descriptors is a key to a deep understanding of Python because they are the basis for many features including functions, methods, properties, class methods, static methods, and reference to super classes.

在类型中的方法不同于普通函数,比如说当我们调用一个对象的方法时,我们不需要显式的传递它

>>> A.f = lambda self: self
>>> a.f()
<__main__.A object at 0x00000128F0AEB490>

A.f 看起来像属性,它还是一个方法。

>>> a.f
<bound method <lambda> of <__main__.A object at 0x00000128F0AEB490>>

如果我们直接通过类型的字典中查找 f 值,我们将得到原来的函数

>>> A.__dict__['f']
<function <lambda> at 0x00000128F067F160>

CPython 返回的不是字典中存储的值,而是其他值,因为函数是描述符,其函数类型实现了 tp_descr_get 方法,所以,当你调用 PyObject_GenericGetAttr 时,它就会调用 tp_descr_get 方法并返回调用结果。

当我们调用一个方法对象时,实例会被放在参数列表的前面,然后函数调用

/* Bind a function to an object */
static PyObject *
func_descr_get(PyObject *func, PyObject *obj, PyObject *type)
{
    if (obj == Py_None || obj == NULL) {
        Py_INCREF(func);
        return func;
    }
    return PyMethod_New(func, obj); // 变成了 method 对象
}
// 最终调用该方法
static PyObject *
method_vectorcall(PyObject *method, PyObject *const *args,
                  size_t nargsf, PyObject *kwnames)
{
    PyObject *self = PyMethod_GET_SELF(method); // 获取 self
    ......
    newargs[0] = self;
        * undefined behaviour. */
    ......
    // 将 self 作为第一个参数
    result = _PyObject_VectorcallTstate(tstate, func,
                                        newargs, nargs+1, kwnames);
    ......
    return result;
}

需要注意的是,描述符只有在类型变量时才会触发,当被作为实例变量时,它的行为和普通对象相似,放在对象字典中的函数不会成为方法。

>>> a.f
<bound method <lambda> of <__main__.A object at 0x00000128F0AEB490>>
>>> a.f()
<__main__.A object at 0x00000128F0AEB490>
>>> a.g
<function <lambda> at 0x00000128F0B4E670>
>>> a.g()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: <lambda>() missing 1 required positional argument: 'self'

我们可以自定义一个描述符类型,实现__get__、__set__、__delete__魔法方法

>>> class DescrClass:
...     def __get__(self, obj, type=None):
...         print("something happed...")
...         return self
...
>>> A.descr_attr = DescrClass()
>>> A.descr_attr
something happed...
<__main__.DescrClass object at 0x00000128F0536CD0>

如果一个类定义了__get__,CPython 将其 tp_descr_get 设置为调用该方法的函数。如果一个类定义了__set__或者__delete__,CPython 在 tp_descr_set 设置为当前值为 NULL 时会调用__delete__函数,否则调用__set__函数。

If you wonder why anyone would want to define their our descriptors in the first place, check out the excellent Descriptor HowTo Guide by Raymond Hettinger.

# Object’s dictionary and type’s dictionary

对象的字典是存储实例变量的字典。类型的每个对象都有一个指向自己字典的指针。例如函数对象的 func_dict。

typedef struct {
    // ......
    PyObject *func_dict;        /* The __dict__ attribute, 
    // ......
} PyFunctionObject;

要告诉 CPython 某个对象的哪个成员是指向该对象的字典的指针,该对象的类型使用 tp_dictoffset 来指定该成员的偏移量。

PyTypeObject PyFunction_Type = {
    // ......
    offsetof(PyFunctionObject, func_dict), // tp_dictoffset
    // ......
};

tp_dictoffset 的正值指定从对象的结构开始的偏移量。负值指定从结构的末尾开始的偏移量。零偏移量表示该类型的对象没有字典。例如整数对象

>>> (12).__dict__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'int' object has no attribute '__dict__'

通过__dictoffset__属性可以看到 int 类型的 tp_dictoffset 设置为 0 了

>>> int.__dictoffset__
0

当我们知道描述符是什么以及属性存储在哪里,就可以看到 PyObject_GenericGetAttr 和 PyObject_GenericSetAttr 函数的作用了

# PyObject_GenericSetAttr

PyObject_GenericSetAttr 将属性设置为特定的值,但实际上真正实现的是另一个函数

int
PyObject_GenericSetAttr(PyObject *obj, PyObject *name, PyObject *value)
{
    return _PyObject_GenericSetAttrWithDict(obj, name, value, NULL);
}
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'",
                     Py_TYPE(name)->tp_name);
        return NULL;
    }
    Py_INCREF(name);
    if (tp->tp_dict == NULL) {
        if (PyType_Ready(tp) < 0)
            goto done;
    }
    descr = _PyType_Lookup(tp, name);
    f = NULL;
    if (descr != NULL) {
        Py_INCREF(descr);
        f = Py_TYPE(descr)->tp_descr_get;
        if (f != NULL && PyDescr_IsData(descr)) {
            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) {
        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) {
        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;
}
PyObject *
PyObject_GenericGetAttr(PyObject *obj, PyObject *name)
{
    return _PyObject_GenericGetAttrWithDict(obj, name, NULL, 0);
}
int
_PyObject_GenericSetAttrWithDict(PyObject *obj, PyObject *name,
                                 PyObject *value, PyObject *dict)
{
    PyTypeObject *tp = Py_TYPE(obj);
    PyObject *descr;
    descrsetfunc f;
    PyObject **dictptr;
    int res = -1;
    if (!PyUnicode_Check(name)){
        PyErr_Format(PyExc_TypeError,
                     "attribute name must be string, not '%.200s'",
                     Py_TYPE(name)->tp_name);
        return -1;
    }
    if (tp->tp_dict == NULL && PyType_Ready(tp) < 0)
        return -1;
    Py_INCREF(name);
    descr = _PyType_Lookup(tp, name);
    if (descr != NULL) {
        Py_INCREF(descr);
        f = Py_TYPE(descr)->tp_descr_set;
        if (f != NULL) {
            res = f(descr, obj, value);
            goto done;
        }
    }
    /* XXX [Steve Dower] These are really noisy - worth it? */
    /*if (PyType_Check(obj) || PyModule_Check(obj)) {
        if (value && PySys_Audit("object.__setattr__", "OOO", obj, name, value) < 0)
            return -1;
        if (!value && PySys_Audit("object.__delattr__", "OO", obj, name) < 0)
            return -1;
    }*/
    if (dict == NULL) {
        dictptr = _PyObject_GetDictPtr(obj);
        if (dictptr == NULL) {
            if (descr == NULL) {
                PyErr_Format(PyExc_AttributeError,
                             "'%.100s' object has no attribute '%U'",
                             tp->tp_name, name);
            }
            else {
                PyErr_Format(PyExc_AttributeError,
                             "'%.50s' object attribute '%U' is read-only",
                             tp->tp_name, name);
            }
            goto done;
        }
        res = _PyObjectDict_SetItem(tp, dictptr, name, value);
    }
    else {
        Py_INCREF(dict);
        if (value == NULL)
            res = PyDict_DelItem(dict, name);
        else
            res = PyDict_SetItem(dict, name, value);
        Py_DECREF(dict);
    }
    if (res < 0 && PyErr_ExceptionMatches(PyExc_KeyError))
        PyErr_SetObject(PyExc_AttributeError, name);
  done:
    Py_XDECREF(descr);
    Py_DECREF(name);
    return res;
}

它做几件事情:

  1. 在类型变量中通过 MRO 搜索属性值
  2. 如果值类型实现了 tp_descr_set,将会调用 tp_descr_set
  3. 否则,使用新值更到对象的字典

# PyObject_GenericGetAttr

获取属性值比设置复杂一些,实际真正调用的也是另一个函数

PyObject *
PyObject_GenericGetAttr(PyObject *obj, PyObject *name)
{
    return _PyObject_GenericGetAttrWithDict(obj, name, NULL, 0);
}
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'",
                     Py_TYPE(name)->tp_name);
        return NULL;
    }
    Py_INCREF(name);
    if (tp->tp_dict == NULL) {
        if (PyType_Ready(tp) < 0)
            goto done;
    }
    descr = _PyType_Lookup(tp, name);
    f = NULL;
    if (descr != NULL) {
        Py_INCREF(descr);
        f = Py_TYPE(descr)->tp_descr_get;
        if (f != NULL && PyDescr_IsData(descr)) {
            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) {
        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) {
        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;
}

该算法的主要逻辑:

  1. 在类型变量通过遍历 MRO 查找属性
  2. 如果值是一个描述符,通过调用__get__并返回调用结果,否则保存该值,继续往下走
  3. 定位对象的字典。如果字典包含该值,则返回
  4. 如果步骤 2 中的值是一个描述符,其类型实现了 tp_descr_get,调用 tp_descr_get 并返回调用结果
  5. 返回步骤 2 中的值。该值可以为 NULL

由于一个属性既可以是实例变量,也可以是类型变量,所以 CPython 必须决定哪个属性优先于另一个属性,该算法实际实现了一个优先顺序:

  1. 类型数据描述符
  2. 实例变量
  3. 类型非数据描述符和其它类型变量

那么为什么 CPython 会让数据描述符优先于实例变量,而非数据描述符不优先?,这个话题单独开另一篇文章来唠唠🐱‍👤

# Metatype attribute management

基本上,类型的属性就像普通对象的属性一样工作。当我们将一个类型的属性设置为某个值时,CPython 会将该值放入类型的 dictionary 中

>>> B.x = "class x attribute"
>>> B.__dict__
mappingproxy({'__module__': '__main__', '__doc__': None, 'x': 'class x attribute'})

当我们得到属性的值时,CPython 从类型的 dictionary 中查找它

>>> B.x
'class x attribute'

那如果类型的 dictionary 不包含该属性,CPython 将从 metatype 的 dictionary 中查找它

>>> B.__class__
<class 'type'>
>>> B.__class__ is object.__class__
True

最后,如果 metatype 的 dictionary 中也找不到该属性,CPython 将在 metatype 父类的 dictionary 中查找该值。

type 实现了自己的 tp_getattro 和 tp_setattro

# type_setattro

static int
type_setattro(PyTypeObject *type, PyObject *name, PyObject *value)
{
    int res;
    if (!(type->tp_flags & Py_TPFLAGS_HEAPTYPE)) {
        PyErr_Format(
            PyExc_TypeError,
            "can't set attributes of built-in/extension type '%s'",
            type->tp_name);
        return -1;
    }
    if (PyUnicode_Check(name)) {
        if (PyUnicode_CheckExact(name)) {
            if (PyUnicode_READY(name) == -1)
                return -1;
            Py_INCREF(name);
        }
        else {
            name = _PyUnicode_Copy(name);
            if (name == NULL)
                return -1;
        }
#ifdef INTERN_NAME_STRINGS
        if (!PyUnicode_CHECK_INTERNED(name)) {
            PyUnicode_InternInPlace(&name);
            if (!PyUnicode_CHECK_INTERNED(name)) {
                PyErr_SetString(PyExc_MemoryError,
                                "Out of memory interning an attribute name");
                Py_DECREF(name);
                return -1;
            }
        }
#endif
    }
    else {
        /* Will fail in _PyObject_GenericSetAttrWithDict. */
        Py_INCREF(name);
    }
    res = _PyObject_GenericSetAttrWithDict((PyObject *)type, name, value, NULL);
    if (res == 0) {
        /* Clear the VALID_VERSION flag of 'type' and all its
           subclasses.  This could possibly be unified with the
           update_subclasses() recursion in update_slot(), but carefully:
           they each have their own conditions on which to stop
           recursing into subclasses. */
        PyType_Modified(type);
        if (is_dunder_name(name)) {
            res = update_slot(type, name);
        }
        assert(_PyType_CheckConsistency(type));
    }
    Py_DECREF(name);
    return res;
}

该函数通过调用_PyObject_GenericSetAttrWithDict 来设置属性值,但它也执行了其他操作。首先,它确保类型不是静态定义的类型,因为这些类型被设计为 immutable。

>>> int.x = 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't set attributes of built-in/extension type 'int'

除此之外,它还会检测属性是否为一个特殊方法,如果属性是一个特殊方法,它将更新与该特殊方法对应的 slot

# type_getattro

/* This is similar to PyObject_GenericGetAttr(),
   but uses _PyType_Lookup() instead of just looking in type->tp_dict. */
static PyObject *
type_getattro(PyTypeObject *type, PyObject *name)
{
    PyTypeObject *metatype = Py_TYPE(type);
    PyObject *meta_attribute, *attribute;
    descrgetfunc meta_get;
    PyObject* res;
    if (!PyUnicode_Check(name)) {
        PyErr_Format(PyExc_TypeError,
                     "attribute name must be string, not '%.200s'",
                     Py_TYPE(name)->tp_name);
        return NULL;
    }
    /* Initialize this type (we'll assume the metatype is initialized) */
    if (type->tp_dict == NULL) {
        if (PyType_Ready(type) < 0)
            return NULL;
    }
    /* No readable descriptor found yet */
    meta_get = NULL;
    /* Look for the attribute in the metatype */
    meta_attribute = _PyType_Lookup(metatype, name);
    if (meta_attribute != NULL) {
        Py_INCREF(meta_attribute);
        meta_get = Py_TYPE(meta_attribute)->tp_descr_get;
        if (meta_get != NULL && PyDescr_IsData(meta_attribute)) {
            /* Data descriptors implement tp_descr_set to intercept
             * writes. Assume the attribute is not overridden in
             * type's tp_dict (and bases): call the descriptor now.
             */
            res = meta_get(meta_attribute, (PyObject *)type,
                           (PyObject *)metatype);
            Py_DECREF(meta_attribute);
            return res;
        }
    }
    /* No data descriptor found on metatype. Look in tp_dict of this
     * type and its bases */
    attribute = _PyType_Lookup(type, name);
    if (attribute != NULL) {
        /* Implement descriptor functionality, if any */
        Py_INCREF(attribute);
        descrgetfunc local_get = Py_TYPE(attribute)->tp_descr_get;
        Py_XDECREF(meta_attribute);
        if (local_get != NULL) {
            /* NULL 2nd argument indicates the descriptor was
             * found on the target object itself (or a base)  */
            res = local_get(attribute, (PyObject *)NULL,
                            (PyObject *)type);
            Py_DECREF(attribute);
            return res;
        }
        return attribute;
    }
    /* No attribute found in local __dict__ (or bases): use the
     * descriptor from the metatype, if any */
    if (meta_get != NULL) {
        PyObject *res;
        res = meta_get(meta_attribute, (PyObject *)type,
                       (PyObject *)metatype);
        Py_DECREF(meta_attribute);
        return res;
    }
    /* If an ordinary attribute was found on the metatype, return it now */
    if (meta_attribute != NULL) {
        return meta_attribute;
    }
    /* Give up */
    PyErr_Format(PyExc_AttributeError,
                 "type object '%.50s' has no attribute '%U'",
                 type->tp_name, name);
    return NULL;
}

有三个重要的区别:

  • 它通过 tp_dict 获取类型的字典。泛型实现将尝试使用 metatype 的 tp_dictoffset 来定位它
  • 它不仅在类型的字典中搜索类型变量,而且还在类型父类的字典中搜索类型变量。泛型实现将处理像普通对象一样没有继承概念的类型
  • 它支持描述符

因此有以下优先顺序:

  1. 元类型数据描述符
  2. 类型描述符和其它类型变量
  3. 元类型非数据描述符和其它元类型变量

type 实现了 tp_getattro 和 tp_setattro,因为 type 是所有内置类型的 metatype,默认情况下是所有类的 metatype,所以大多数类型的属性都是根据这个实现工作的。

类本身默认情况下使用通用实现,如果我们想更改类实例的属性行为或类的属性行为,我们需要定义一个新的类或者一个使用自定义实现的 metatype。

# Custom attribute management

类的 tp_getattro 和 tp_setattro 是由创建新类的 type_new 函数里面设置的。泛型实现是其默认选择。类可以通过定义 getattributegetattrsetattr delattr 魔法方法来定义属性的访问、赋值和删除。

当一个类定义了__setattr__或__delattr__时,它的 tp_setattro slot 会被设置为 slot_tp_setattro 函数。当一个类定义了__getattribute__或__getattr__时,它的 tp_getattro slot 会被设置为 slot_tp_getattr_hook 函数。

slot_tp_setattro 函数只调用__delattr__(instance, attr_name) 或__setattr__(instance, attr_name, value),这取决于 value 是否为 NULL

static int
slot_tp_setattro(PyObject *self, PyObject *name, PyObject *value)
{
    PyObject *stack[3];
    PyObject *res;
    _Py_IDENTIFIER(__delattr__);
    _Py_IDENTIFIER(__setattr__);
    stack[0] = self;
    stack[1] = name;
    if (value == NULL) {
        res = vectorcall_method(&PyId___delattr__, stack, 2);
    }
    else {
        stack[2] = value;
        res = vectorcall_method(&PyId___setattr__, stack, 3);
    }
    if (res == NULL)
        return -1;
    Py_DECREF(res);
    return 0;
}

__getattribute__和__getattr__魔法方法提供了一种定制的属性访问方法。两者都以一个实例和一个属性名作为参数,并返回属性值,区别在于何时调用它们。

__getattr__与__getattribute__或泛型函数一起使用。当__getattribute__或泛型函数引发 AttributeError 时调用它。实现逻辑在 slot_tp_getattr_hook 函数

static PyObject *
slot_tp_getattr_hook(PyObject *self, PyObject *name)
{
    PyTypeObject *tp = Py_TYPE(self);
    PyObject *getattr, *getattribute, *res;
    _Py_IDENTIFIER(__getattr__);
    /* speed hack: we could use lookup_maybe, but that would resolve the
       method fully for each attribute lookup for classes with
       __getattr__, even when the attribute is present. So we use
       _PyType_Lookup and create the method only when needed, with
       call_attribute. */
    getattr = _PyType_LookupId(tp, &PyId___getattr__);
    if (getattr == NULL) {
        /* No __getattr__ hook: use a simpler dispatcher */
        tp->tp_getattro = slot_tp_getattro;
        return slot_tp_getattro(self, name);
    }
    Py_INCREF(getattr);
    /* speed hack: we could use lookup_maybe, but that would resolve the
       method fully for each attribute lookup for classes with
       __getattr__, even when self has the default __getattribute__
       method. So we use _PyType_Lookup and create the method only when
       needed, with call_attribute. */
    getattribute = _PyType_LookupId(tp, &PyId___getattribute__);
    if (getattribute == NULL ||
        (Py_IS_TYPE(getattribute, &PyWrapperDescr_Type) &&
         ((PyWrapperDescrObject *)getattribute)->d_wrapped ==
         (void *)PyObject_GenericGetAttr))
        res = PyObject_GenericGetAttr(self, name);
    else {
        Py_INCREF(getattribute);
        res = call_attribute(self, getattribute, name);
        Py_DECREF(getattribute);
    }
    if (res == NULL && PyErr_ExceptionMatches(PyExc_AttributeError)) {
        PyErr_Clear();
        res = call_attribute(self, getattr, name);
    }
    Py_DECREF(getattr);
    return res;
}

代码做了几件事:

  1. 如果类没有定义__getattr__,首先设置它的 tp_getattro 为 slot_tp_getattro 函数,然后调用该函数并返回调用结果
  2. 如果类定义了__getattribute__,就调用该函数并返回调用结果
  3. 如果来自上一步的调用引发了 AttributeError,就调用__getattr__
  4. 返回最后一次调用的结果

slot_tp_getattr 函数是当类定义了__getattribute__而不是__getattr__时,CPython 使用 tp_getattro slot 的实现。这个函数只调用__getattribute__

/* There are two slot dispatch functions for tp_getattro.
   - slot_tp_getattro() is used when __getattribute__ is overridden
     but no __getattr__ hook is present;
   - slot_tp_getattr_hook() is used when a __getattr__ hook is present.
   The code in update_one_slot() always installs slot_tp_getattr_hook(); this
   detects the absence of __getattr__ and then installs the simpler slot if necessary. 
*/
static PyObject *
slot_tp_getattro(PyObject *self, PyObject *name)
{
    PyObject *stack[2] = {self, name};
    return vectorcall_method(&PyId___getattribute__, stack, 2);
}

# Loading methods

当我们获取或设置 Python 对象的属性时会发生什么,来探讨下 Python 属性比较重要的方面。

我们可以看到函数对象是一个描述符,当我们将它绑定到一个实例对象时,它返回一个 bound method

>>> a.f
<bound method <lambda> of <__main__.A object at 0x00000128F0AEB490>>

当编译器看到带有像 object.method (args1, arg2, argN) 这样的位置参数的方法调用时,它不会产生加载该方法的 LOAD_ATTR 和调用该方法的 CALL_FUNCTION 操作码,相反,它会生成一对 LOAD_METHOD 和 CALL_METHOD 操作码:

>>> dis.dis("object.method()")
1           0 LOAD_NAME                0 (object)
            2 LOAD_METHOD              1 (method)
            4 CALL_METHOD              0
            6 RETURN_VALUE

当 VM 看到 LOAD_METHOD 操作码时,它调用_PyObject_GetMethod 函数来搜索属性值。此函数工作方式与泛型函数类似,区别在于它会检查该值是否是未绑定的方法,即返回绑定到实例的类似方法的对象的描述符。在这种情况下,它不调用描述符类型的 tp_descr_get 方法,而是返回描述符本身。例如属性值是一个函数,_PyObject_GetMethod 返回该函数。函数类型和其它描述符类型的对象作为未绑定的方法,在它的 tp_flag 中指定了 Py_TPFLAGS_METHOD_DESCRIPTOR flag,所以很容易识别出来。

/* Objects behave like an unbound method */
#define Py_TPFLAGS_METHOD_DESCRIPTOR (1UL << 17)
int
_PyObject_GetMethod(PyObject *obj, PyObject *name, PyObject **method)
{
    // ......
    int meth_found = 0;
    // ......
    if (Py_TYPE(obj)->tp_getattro != PyObject_GenericGetAttr
            || !PyUnicode_Check(name)) {
        *method = PyObject_GetAttr(obj, name);
        return 0;
    }
    // ......
    descr = _PyType_Lookup(tp, name);
    if (descr != NULL) {
        Py_INCREF(descr);
        if (_PyType_HasFeature(Py_TYPE(descr), Py_TPFLAGS_METHOD_DESCRIPTOR)) {
            meth_found = 1;
        }
    }
    // ......
    if (meth_found) {
        *method = descr;
        return 1;
    }
    // ......
}

需要注意,_PyObject_GetMethod 只有在对象的类型使用 tp_getattro 的通用实现时才能正常工作。否则它只调用自定义实现,不执行任何检查。

如果_PyObject_GetMethod 找到一个未绑定的方法,则必须使用前置于参数列表的实例调用该方法。如果它发现其它不需要绑定到实例的可调用函数,参数列表必须保持不变。因此,在 VM 执行 LOAD_METHOD 之后,堆栈上的值可以以两种方式中的一种进行排列:

  • 未绑定的方法和包括实例在内的参数列表:(method | self | arg1 | … | argN)
  • 其它可调用参数和没有实例的参数列表: (NULL | method| arg1 | … | argN)

CALL_METHOD 操作码的存在是为了在每种情况下适当地调用该方法

# Listing attributes of an object

Python 提供内置的 dir 函数,可用于查看对象具有哪些属性。但它是如何找到这些属性的?它通过调用对象类型的__dir__魔法方法来实现。类型很少会定义自己的__dir__,但所有类型都有它,这是因为对象类型定义了__dir__,而所有其它类型都继承了该对象。对象提供的实现列出了存储在对象的字典、类型的字典和类型父类的字典中的所有属性。这时因为 type 提供了自己的__dir__实现。此实现返回存储在类型的字典和类型父类的字典中的属性。但是它们忽略了存储在 metatype 的字典和 metatype 父类的字典中的属性。文档中解释道

Because dir() is supplied primarily as a convenience for use at an interactive prompt, it tries to supply an interesting set of names more than it tries to supply a rigorously or consistently defined set of names, and its detailed behavior may change across releases. For example, metaclass attributes are not in the result list when the argument is a class.

# Where attributes of types come from

>>> dir(object)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
>>> dir(int)
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_integer_ratio', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']

对于魔法方法,是由初始化类型的 PyType_Ready 函数自动添加的,那其他属性呢?

指定类型属性的最直接方法是创建一个字典,用属性填充它,并将类型的 tp_dict 设置为该字典。PyType_Ready 函数在运行时创建内置类型的字典,它还负责添加所有属性。

首先,PyType_Ready 确保类型具有字典,然后它向字典添加属性。类型通过指定 tp_method、tp_member 和 tp_getset 来告诉 PyType_Ready 要添加哪些属性。每个 slot 都是一个结构数组,用于描述不同类型的属性。

# tp_method

tp_method 是描述方法的 PyMthodDef 结构数组:

struct PyMethodDef {
    const char  *ml_name;   /* The name of the built-in function/method */
    PyCFunction ml_meth;    /* The C function that implements it */
    int         ml_flags;   /* Combination of METH_xxx flags, which mostly
                               describe the args expected by the C func */
    const char  *ml_doc;    /* The __doc__ attribute, or NULL */
};
typedef struct PyMethodDef PyMethodDef;

ml_meth member 是一个指向实现该方法的 C 函数指针,它的签名可以是许多签名中的一个。ml_flag 字段用于告诉 CPython 如何准确地调用该函数。

对于 tp_method 中的每个结构,PyType_Ready 将一个可调用对象添加到类型的字典中,此对象封装了结构,当我们调用它时,调用由 ml_meth 指向的函数。这基本上就是 C 函数如何称为 Python 类型方法的过程。

# tp_member

tp_member slot 是 PyMemberDef 结构的数组。每个结构描述一个属性,该属性公开该类型对象的一个 C 成员:

typedef struct PyMemberDef {
    const char *name;
    int type;
    Py_ssize_t offset;
    int flags;
    const char *doc;
} PyMemberDef;

成员由偏移量指定,其类型由类型指定。

对于 tp_member 中的每个结构,PyType_Ready 将一个成员描述符添加到类型的字典中。成员描述符是封装 PyMemberDef 的数据描述法。它的 tp_descr_get 接受一个实例,找到位于偏移位置的实例的成员,将其转换为相应的 Python 对象并返回。它的 tp_descr_set 接受一个实例和一个值,找到位于偏移量的实例的成员,并将其设置为该值的 C 等价物。可以通过 flag 指定成员是否可读可写

例如通过这种机制,type 定义__dictoffset__和其他成员:

static PyMemberDef type_members[] = {
    {"__basicsize__", T_PYSSIZET, offsetof(PyTypeObject,tp_basicsize),READONLY},
    {"__itemsize__", T_PYSSIZET, offsetof(PyTypeObject, tp_itemsize), READONLY},
    {"__flags__", T_ULONG, offsetof(PyTypeObject, tp_flags), READONLY},
    {"__weakrefoffset__", T_PYSSIZET,
     offsetof(PyTypeObject, tp_weaklistoffset), READONLY},
    {"__base__", T_OBJECT, offsetof(PyTypeObject, tp_base), READONLY},
    {"__dictoffset__", T_PYSSIZET,
     offsetof(PyTypeObject, tp_dictoffset), READONLY},
    {"__mro__", T_OBJECT, offsetof(PyTypeObject, tp_mro), READONLY},
    {0}
};

# tp_getset

tp_getset 是 PyGetSetDef 结构的数组,它描述任意的数据描述符,如 property:

typedef struct PyGetSetDef {
    const char *name;
    getter get;
    setter set;
    const char *doc;
    void *closure;
} PyGetSetDef;

对于 tp_getset 中的每个结构,PyType_Ready 将一个 getset 描述符添加到类型的字典中,getset 描述符的 tp_descr_get 指定 get 函数,tp_descr_set 指定 set 函数。

类型使用这种机制定义__dict__属性

static PyGetSetDef func_getsetlist[] = {
    {"__code__", (getter)func_get_code, (setter)func_set_code},
    {"__defaults__", (getter)func_get_defaults,
     (setter)func_set_defaults},
    {"__kwdefaults__", (getter)func_get_kwdefaults,
     (setter)func_set_kwdefaults},
    {"__annotations__", (getter)func_get_annotations,
     (setter)func_set_annotations},
    {"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict},
    {"__name__", (getter)func_get_name, (setter)func_set_name},
    {"__qualname__", (getter)func_get_qualname, (setter)func_set_qualname},
    {NULL} /* Sentinel */
};

__dict__属性不是作为只读成员描述符实现,而是作为 getset 描述符实现,因为它不仅仅返回位于 tp_dictoffset 的字典。例如描述符创建字典(如果它还不存在)

类还通过这种机制获取__dict__属性。创建类的 type_new 函数在调用 PyType_Ready 之前指定 tp_getset。但是有些类没有这个属性,因为它们的实例没有字典,这些是定义了__slots__的类

# __slots__

类的__slots__属性枚举该类可以拥有的属性

>>> class D:
...     __slots__ = ('x', 'y')
...

如果一个类定义了__slots__,那么__dict__属性将不会被添加到类的字典中,并且该类的 tp_dictoffset 被设置为 0。这样做的主要结果是实例没有字典

>>> D.__dictoffset__
0
>>> d = D()
>>> d.__dict__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'D' object has no attribute '__dict__'

但是__slots__中列出的属性可以正常的工作

>>> d.x = 1
>>> d.x
1

会发现很奇怪,定义在__slots__的成员成为了类实例的成员。对于每个成员,成员描述符被添加到类字典中。type_new 函数指定 tp_memeber 执行此操作。

>>> D.x
<member 'x' of 'D' objects>

你会发现 x、y 是一个 member object。实例没有字典,所以__slots__可以节省内存。

Descriptor HowTo Guide

编译器生成 LOAD_ATTR、STORE_ATTR 和 DELETE_ATTR 操作码以获取、设置和删除属性。为了执行这些操作码,VM 调用对象类型的 tp_getattro 和 tp_setattro slot,一个类型可能以任意的方式实现这些 slot,但大多数情况下我们必须处理三个实现:

  • 大多数内置类型和类使用的泛型实现
  • 按类型使用的实现
  • 定义__getattribute__、__getattr__、__setattr__和__delattr__魔法方法的类所使用的实现

简而言之,描述符是控制属性访问、分配和删除的属性。它允许 CPython 实现许多特性,包括方法和属性。

内置类型使用三种机制定义属性:

  • tp_methods
  • tp_members
  • tp_getset

类还是用这些机制来定义一些属性,比如__dict__被定义为一个 getset 描述符,而__slots__中定义的属性被定义为成员描述符。

Edited on Views times

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

小芳芳 WeChat Pay

WeChat Pay

小芳芳 Alipay

Alipay