4. Python Objects

Python 对象

在本章中,我们研究 python 对象及其在 CPython 虚拟机中的实现。 了解 python 对象是如何组织的对于深入理解 python 虚拟机的内部结构来说非常重要。 可以在 Include/ 和 Objects/ 目录中找到此处讨论的大多数源代码。 毫不奇怪,用 python 实现对象系统非常复杂,我们尽力避免陷入 C 实现的繁琐细节中。 首先,我们先来看 python 对象系统的主力: PyObject 结构体 。

4.1 PyObject

只要粗略的看一下 CPython 源代码就能注意到 PyObject 结构的普遍性。 实际上,正如我们稍后将会看到的那样,当解释器循环处理求值堆栈上值的时候,所有这些值都被视为PyObject。 如果要使用一个更好的术语来方便讨论,我们不妨称其为所有 python 对象的超类。 值实际上从未被声明为 PyObject,但是可以将指向任何对象的指针强制转换为PyObject。 通俗来讲,任何对象都可以视为 PyObject 结构,因为所有对象的初始段实际上都是 PyObject 结构。

关于 C 结构体

“值永远不会被声明为 PyObject,但是可以将指向任何对象的指针转换为 PyObject” 这种说法依赖于 C 语言及其如何解释内存上存放数据的具体细节。 用于表示 python 对象的 C 结构只是一组字节而已,我们可以选择以任何方式来解释它们。 例如,一个结构体叫做 test ,它可能由5个 short 类型的数字组成,每个值占2个字节,总和最多10个字节。 在C语言中,给定一个对十个字节的引用,我们可以将这十个字节解释为由5个 short 组成的 test 结构,而不管这10个字节是否实际上真的被定义为 test 结构体,只是当你尝试访问该结构的字段时,输出可能是没有意义的乱码。 这意味着在给定 n 个表示 python 对象的数据的 n 个字节(其中 n 大于 PyObject 的大小)的情况下,我们可以将前n个字节解释为一个 PyObject

The PyObject structure is shown in listing 4.0 and is composed of a number of fields that must be filled in order for a value to be treated as an object.

PyObject 结构体的定义如清单4.0所示,它由多个字段组成,这些字段必须被全部填上才能将它视为对象。

代码清单4.0: PyObject 结构体
typedef struct _object {
    _PyObject_HEAD_EXTRA
    Py_ssize_t ob_refcnt;
    struct _typeobject *ob_type;
} PyObject;

The _PyObject_HEAD_EXTRA when present is a C macro that defines fields that point to the previously allocated object and next object forming an implicit doubly linked list of all live objects. The ob_refcnt field is used for memory management and the *ob_type is a pointer to a type object that indicates the type of an object. It is this type that determines what the data represents, what kind of data it contains and the kind of operations that can be performed on that object. Take the snippet in listing 4.1 for example, the name, name, points to a string object and the type of the object is 'str'.

_PyObject_HEAD_EXTRA 是一个C宏,它定义了一个指向之前分配对象的字段和指向下一个对象的字段,这些字段能够形成所有活动对象的隐式双链表。 ob_refcnt 字段用于内存管理,而 *ob_type 是指向类型对象的指针,指示对象的类型。 类型决定了对象代表什么、所包含的数据类型以及可以对它执行的操作类型。 以清单4.1中的代码为例,name 指向一个字符串对象,对象的类型为 str

代码清单4.1
>>> name = 'obi'
>>> type(name)
<class 'str'>

这里存在一个问题,因为(译注:类型对象,比如 str 的)类型字段指向 type 对象,那么 type 对象的 *ob_type 字段指向什么? 实际上 type 对象的 ob_type 实际上指向自身,或者说 type 的类型还是 type

关于引用计数

CPython 使用引用计数进行内存管理。 这是一种相对简单的方法,其中只要在创建对一个对象的新引用时(如清单4.1中将 name 绑定到对象的情况),对象的引用计数就会增加。 反之亦然,每当对一个对象的引用消失(例如,使用名称上的del 删除该引用)时,引用计数就会减少。 当对象的引用计数变为零时,虚拟机可以将其释放。 在虚拟机中,Py_INCREFPy_DECREF 用于增加和减少对象的引用计数,它们出现在我们讨论过的许多代码片段中。

虚拟机中的类型是使用 Objects/Object.h 头文件中定义的 _typeobject 结构实现的。 这是一个 C 结构体,其中包含了用于大多数函数或每种类型输入的函数集合的字段。 接下来我们看一下这个数据结构。

4.2 类型对象的字段

Include/Object.h 中定义的 _typeobject 结构是所有 python 类型的基本结构。 它定义了许多字段,这些字段大多是指向实现给定类型的某些函数的 C 函数的指针。 为方便起见,清单4.2中列出了_typeobject 结构体的定义。

代码清单4.2: _typeobject 结构体
typedef 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;
    printfunc tp_print;
    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;

    /* these must be last and never explicitly initialized */
    Py_ssize_t tp_allocs;
    Py_ssize_t tp_frees;
    Py_ssize_t tp_maxalloc;
    struct _typeobject *tp_prev;
    struct _typeobject *tp_next;
} PyTypeObject;

PyObject_VAR_HEAD 字段是上一节中讨论的 PyObject 字段的扩展;该扩展为具有长度概念的对象添加了一个 ob_size 字段。 python C API 文档中提供了此类型对象结构中每个字段的详细说明。需要注意的重要一点是,结构中每个字段都实现了部分类型行为。这些字段中大多数是可以被称为对象接口或协议的部分,因为它们对应于可以在 python 对象上调用的函数,但是其实际实现方式取决于类型。例如,tp_hash 字段是给定类型的哈希函数的引用,如果类型实例不能被哈希,则该字段可以不带值。在该类型的实例上调用 hash 方法时,将调用 tp_hash 字段中的函数。类型对象包括字段 tp_methods,它引用该类型特有的方法。 tp_new 是对用来创建该类型实例的函数的引用。其中一些字段(例如 tp_init)是可选的,并非每种类型都需要运行初始化函数,尤其是当该类型是不可变的(例如元组)但其他字段(例如 tp_new)是必需的。

这些字段中还有用于实现其他 python 协议的字段,例如:

  1. Number protocol: 实现此协议的类型将具有 PyNumberMethods *tp_as_number 字段的实现。 该字段是对一组实现数字操作的函数的引用,这意味着该类型将支持在tp_as_number 集合中包含实现的算术。 例如,非数字的 set 类型在此字段中有一个条目,因为它支持一些算术运算,如 -<= 等。

  2. Sequence protocol: 实现此协议的类型将在 PySequenceMethods *tp_as_sequence字段具有一个值。 这意味着类型将支持部分或全部序列操作,例如 lenin 等。

  3. Mapping protocol: 实现该协议的类型会在 PyMappingMethods *tp_as_mapping 字段具有一个值。 这将允许使用字典下标语法来设置和访问 键-值 映射,从而将此类实例视为python 字典。

  4. Iterator protocol: 实现此协议的类型将在 getiterfunc tp_iter 以及 iternextfunc tp_iternext 字段具有一个值,从而使该类型的实例能够像 python 迭代器一样被使用。

  5. Buffer protocol: 实现此协议的类型将在 PyBufferProcs *tp_as_buffer 字段具有一个值。 这将允许访问该类型的实例作为输入/输出缓冲区。

在本章中,我们将更详细地研究构成类型对象的各个字段,但现在,我们要探讨一些不同的类型对象,作为有关如何在实际类型对象中填充这些字段的具体案例研究。

4.3 类型对象案例研究

tuple 类型

我们来仔细看一下元组类型,以了解如何填充类型对象的字段。 我们之所以选择它,是因为考虑到它实现的代码量较小,大约是一千多行 C 语言(包括文档字符串)代码。 tuple 的实现如清单4.3所示。

代码清单4.3: tuple 类型定义
PyTypeObject PyTuple_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "tuple",
    sizeof(PyTupleObject) - sizeof(PyObject *),
    sizeof(PyObject *),
    (destructor)tupledealloc,                   /* tp_dealloc */
    0,                                          /* tp_print */
    0,                                          /* tp_getattr */
    0,                                          /* tp_setattr */
    0,                                          /* tp_reserved */
    (reprfunc)tuplerepr,                        /* tp_repr */
    0,                                          /* tp_as_number */
    &tuple_as_sequence,                         /* tp_as_sequence */
    &tuple_as_mapping,                          /* tp_as_mapping */
    (hashfunc)tuplehash,                        /* tp_hash */
    0,                                          /* tp_call */
    0,                                          /* tp_str */
    PyObject_GenericGetAttr,                    /* tp_getattro */
    0,                                          /* tp_setattro */
    0,                                          /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC |
        Py_TPFLAGS_BASETYPE | Py_TPFLAGS_TUPLE_SUBCLASS, /* tp_flags */
    tuple_new__doc__,                           /* tp_doc */
    (traverseproc)tupletraverse,                /* tp_traverse */
    0,                                          /* tp_clear */
    tuplerichcompare,                           /* tp_richcompare */
    0,                                          /* tp_weaklistoffset */
    tuple_iter,                                 /* tp_iter */
    0,                                          /* tp_iternext */
    tuple_methods,                              /* tp_methods */
    0,                                          /* tp_members */
    0,                                          /* 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 */
    tuple_new,                                  /* tp_new */
    PyObject_GC_Del,                            /* tp_free */
};

我们了看看这些字段:

  1. PyObject_VAR_HEAD 已使用类型对象 PyType_Type 作为类型进行初始化。 回想一下,type 对象的类型是 type。 查看 PyType_Type 类型对象可发现 PyType_Type 的类型为自身。

  2. tp_name 初始化为类型的名称: tuple

  3. tp_basicsizetp_itemsize 引用元组对象和元组对象中包含的项目的大小,并相应地进行填充。

  4. tupledealloc 是一种内存管理功能,用于在销毁元组对象时处理内存的重新分配。

  5. tuplerepr 是以元组实例作为参数调用 repr 函数时调用的函数。

  6. tuple_as_sequence 是元组实现的序列方法的集合。 比如元组支持的 len 等序列方法。

  7. tuple_as_mapping 是元组支持的一组映射方法,在这种情况下,键只能是整数索引。

  8. tuplehash 是在计算元组对象的哈希值时被调用的函数。 当元组用作字典键或成组使用时会起作用。

  9. PyObject_GenericGetAttr 是引用元组对象的属性时调用的通用函数。 我们将在后续部分中介绍属性引用。

  10. tuple_doc 是元组对象的文档字符串。

  11. tupletraverse 是用于元组对象垃圾回收的遍历函数。垃圾收集器使用此功能来帮助检测引用周期。

  12. tuple_iter是在 tuple 对象上调用 iter 函数时将被调用的方法。 在这种情况下,将返回完全不同的 tuple_iterator 类型,因此 tp_iternext 方法没有实现。

  13. tuple_methods 是元组类型的实际方法。

  14. tuple_new 是用于创建新的元组类型实例的函数。

  15. PyObject_GC_Del 是引用内存管理函数的另一个字段。

其余具有 0 值的字段保留为空,因为元组的功能不需要它们。 以 tp_init 字段为例,元组是不可变的类型,因此一旦创建它就不能更改,因此除了 tp_new 引用的函数中发生的事情外,不需要任何初始化,因此该字段保留为空。

type 类型

我们要看的另一种类型是 type 类型。 所有内置类型以及用户定义的普通类型的元类型(用户可以定义一个新的元类型)。注意在初始化 PyVarObject_HEAD_INIT 中的元组对象时如何使用此类型。 在讨论类型时,重要的是区分以 type 为类型的对象和以用户定义类型为类型的对象。 在处理对象中的属性引用时,这非常重要。

object 类型

另一个重要的类型是 object 类型,它与 type 类型非常相似。 object 类型是所有用户定义类型的根类型,并提供一些默认值,用于填充用户定义类型的类型字段。 这是由于以下事实:与以 type 作为其类型的类型相比,用户定义的类型的行为方式有所不同。 正如我们将在后续部分中看到的那样,诸如 object 类型提供的属性解析算法之类的函数与 type 类型所提供的函数有很大不同。

4.4 创建类型实例

假设我们已经对类型的基本知识有了一些了解,我们就可以进入类型的最基本功能之一,即创建类型实例的能力。 为了完全理解创建新类型实例的过程,重要的是要记住,正如我们区分内置类型和用户定义类型一样,两者的内部结构也很有可能也会有所不同。 tp_new 字段是python 中新类型实例的判断依据。 tp_new 字段的文档对填充它的函数进行了足够详细的描述:

一个指向示例创建函数的指针

函数签名为:

PyObject *tp_new(PyTypeObject *subtype, PyObject *args, PyObject *kwds);

subtype参数为被创建对象的类型;argskwds 参数代表传给它的位置(positional)参数和关键词(keyword)参数. 注意,subtype 不是必须等同于调用 tp_new 的类型;它可能是它的子类型。

tp_new 函数应该调用 subtype->tp_alloc(subtype, nitems) 为对象分配空间,然后仅执行一些必要的初始化。 可以安全地忽略或重复进行的初始化应放在 tp_init 中。 一个好的经验法则是,对于不可变类型,所有初始化都应在 tp_new 中进行,而对于可变类型,大多数初始化应推迟到 tp_init 中进行。

Inheritance:

该字段由子类型继承,除非它的字段不是由 tp_baseNULL&PyBaseObject_Type 的静态类型继承。

我们下面还将使用 tuple 作为示例。元组类型的 tp_new 字段引用清单4.4中所示的 tuple_new 方法,该方法处理新元组对象的创建。 如果要创建一个新的元组对象,请取消引用并调用此函数。

代码清单4.4: tuple_new
static PyObject *
tuple_new_impl(PyTypeObject *type, PyObject *iterable)
/*[clinic end generated code: output=4546d9f0d469bce7 input=86963bcde633b5a2]*/
{
    if (type != &PyTuple_Type)
        return tuple_subtype_new(type, iterable);

    if (iterable == NULL)
        return PyTuple_New(0);
    else
        return PySequence_Tuple(iterable);
}

忽略清单4.4中创建元组的第一个和第三个条件,我们看一下第二个条件, if (iterable == NULL) return PyTuple_New(0) 顺着调用链向下来研究他的工作原理, 忽略 PyTuple_New 函数中的优化,创建新元组对象对应的代码是 op = PyObject_GC_NewVar(PyTupleObject, &PyTuple_Type, size); 这个调用,该调用为堆上的 PyTuple_Object 结构实例分配内存。 这就是内置类型和用户定义类型的内部表示之间的一个明显区别:内置类型的实例(例如元组)为了提高效率实际上是C结构体。 那么元组对象的 C 结构体看起来像什么呢? 可以在 Include/tupleobject.h 中找到它作为 PyTupleObject 的 typedef,为方便起见,在清单4.5中显示。

代码清单4.5: PyTuple_Object 定义
typedef struct {
    PyObject_VAR_HEAD
    PyObject *ob_item[1];

    /* ob_item contains space for 'ob_size' elements.
     * Items must normally not be NULL, except during construction when
     * the tuple is not yet visible outside the function that builds it.
     */
} PyTupleObject;

PyTupleObject 定义为具有 PyObject_VAR_HEADPyObject 指针数组 ob_items 的结构体。 与使用 python 来表达的实例相反,这是非常高效的实现。

回想一下,对象是方法和数据的集合。 在这种情况下,PyTupleObject 提供了空间来保存每个元组对象包含的实际数据,因此我们可以在堆上分配 PyTupleObject 的多个实例,但是这些实例都将引用单个 PyTuple_Type 类型对象,它提供可以对数据进行操作的方法。

现在来考虑一个用户定义的类:

代码清单4.6: 用户定义类
class Test:
    pass

和想象的一样,Test 类型是 type 的实例。 要创建 Test 类型的实例,要对 Test 类型进行调用:Test()type 类型具有一个函数引用 type_call,它填充了 tp_call 字段,并且在 type 实例上使用调用符号时将取消引用。 清单4.7中显示了 type_call 函数实现的部分代码。

代码清单4.7: type_call 函数实现
    ...
    PyObject *obj = type->tp_new(type, args, kwds);
    obj = _Py_CheckFunctionResult(tstate, (PyObject*)type, obj, NULL);
    if (obj == NULL)
        return NULL;

    /* Ugly exception: when the call was type(something),
       don't call tp_init on the result. */
    if (type == &PyType_Type &&
        PyTuple_Check(args) && PyTuple_GET_SIZE(args) == 1 &&
        (kwds == NULL ||
         (PyDict_Check(kwds) && PyDict_GET_SIZE(kwds) == 0)))
        return obj;

    /* If the returned object is not an instance of type,
       it won't be initialized. */
    if (!PyType_IsSubtype(Py_TYPE(obj), type))
        return obj;

    type = Py_TYPE(obj);
    if (type->tp_init != NULL) {
        int res = type->tp_init(obj, args, kwds);
        if (res < 0) {
            assert(_PyErr_Occurred(tstate));
            Py_DECREF(obj);
            obj = NULL;
        }
        else {
            assert(!_PyErr_Occurred(tstate));
        }
    }
    return obj;
}

清单4.7显示了调用 type 对象的实例时,所发生的一切就是取消了对 tp_new 字段的引用,并调用了所引用的任何函数来获取新的实例。 如果 tp_init 存在,则在新实例上调用它以执行新实例的初始化。 此过程为内置类型提供了解释,因为毕竟它们已经定义了自己的 tp_newtp_init 函数,但是用户定义的类型呢? 大多数情况下,用户不会为新类型定义 __new__ 函数(在定义时,在类创建期间将进入到 tp_new 字段)。 答案也取决于type_new 函数,该函数填充 typetp_new 字段。 在创建用户定义的类型(例如在Test 的情况下)时,type_new 函数检查基类(super types/classes)的存在,如果不存在,则将 PyBaseObject_Type 类型添加为默认基本类型,如清单4.8所示。

代码清单4.8: PyBaseObject_Type 是如何添加到 bases 列表的
    ...
    nbases = PyTuple_GET_SIZE(bases);
    if (nbases == 0) {
        base = &PyBaseObject_Type;
        bases = PyTuple_Pack(1, base);
        if (bases == NULL)
            return NULL;
        nbases = 1;
    }
    ...

默认基本类型也在 Objects/typeobject.c 模块中定义,其中包括各个字段的一些默认值。 这些默认值中包括 tp_newtp_init 字段的值。 这些是解释器为用户定义类调用提供的值。 在用户定义类型实现了自己的方法(例如 __init____new__ 等)的情况下,将调用这些值,而不是 PyBaseObject_Type 类型的值。

可能会注意到,我们没有提到任何对象结构,例如元组对象结构 tupleobject ,那么问题来了:如果没有为用户定义的类定义对象结构,那么对象实例将如何处理?对象属性存放在何处? 这与 tp_dictoffset 字段(一个类型对象中的数字字段)有关。 实例实际上是作为PyObject 创建的,但是当实例类型中的偏移量值不为零时,它将代表实例属性字典与实例(PyObject)本身之间的偏移量,如图4.0所示,因此对于 Person 类型的实例 ,可以通过将此偏移值与 PyObject 内存位置相加来计算属性字典在内存中的位置。

例如,如果实例 PyObject 的值为 0x10,偏移量为16,那么属性字典的位置位于 0x10 + 16。这并不是实例存储其属性的唯一方法,我们会下一节中看到其它的方式。

4.5 对象和它们的属性

类型及其属性(变量和方法)对于面向对象编程来说至关重要。 一般情况下,类型和实例使用 dict 存储它们的属性,而在定义了 __slots__ 的时候情况有所不同。 如上一节所述,可以根据对象的类型在两个位置的其中一个中找到对应的 dict

  1. 对于 type 类型的对象,类型结构体的 tp_dict 字段是指向 dict 的指针,该 dict 包含该类型的值,变量和方法。 或者说,类型对象结构体的 tp_dict 字段是指向类字典的指针。

  2. 对于用户定义类型的实例,该字典(如果存在)位于表示该对象的PyObject 结构之后。 对象类型的 tp_dictoffset 值给出了从对象开始到包含实例属性的实例 dict 的偏移量。

通过一个简单的字典访问来获取属性似乎很简单,但其中其实隐藏了许多看不到的细节。 实际上,与仅检查 type 实例的 tp_dict 值或用户定义类型的实例的 tp_dictoffset 处的 dict 相比,搜索属性要涉及更多的细节。 为了能够对其有一个比较全面的了解,我们必须讨论一下描述符协议,它是 python 属性引用的核心。

Descriptor HowTo Guide 是一篇不错的介绍描述符协议的文章。 简而言之,描述符是实现了描述符协议中的 __get____set____delete__ 特殊方法的对象。 清单4.9显示了 python 中每种方法的签名。

代码清单4.9: 描述符协议中的方法
descr.__get__(self, obj, type=None) --> value
descr.__set__(self, obj, value) --> None
descr.__delete__(self, obj) --> None

仅实现 __get__ 方法的对象是非数据描述符(non-data descriptors),因此只能在初始化后从它们中读取,而实现 __get__ __set__ 的对象是数据描述符,这意味着此类描述符对象是可写的。 我们对描述符及其在表示对象属性中的应用感兴趣。 清单4.10中的 TypedAttribute 描述符是一个用于表示对象属性的描述符的示例。

代码清单4.10: 一个简单的用于属性值类型检查的描述符
class TypedAttribute:
    
    def __init__(self, name, type, default=None):
        self.name = "_" + name
        self.type = type
        self.default = default if default else type()

    def __get__(self, instance, cls):
        return getattr(instance, self.name, self.default)

    def __set__(self,instance,value):
        if not isinstance(value,self.type):
            raise TypeError("Must be a %s" % self.type) 
        setattr(instance,self.name,value)
    
    def __delete__(self,instance):
        raise AttributeError("Can't delete attribute")

TypedAttribute 描述符类对用于表示的类的任何属性强制执行类型检查。 重要的是要注意,只有在类级别而不是实例级别中定义描述符时(比如在清单4.11中所示的类中定义 __init__ 方法下定义描述符属性),描述符才在这种情况下有效。

代码清单4.11: 使用 TypedAttribute 描述符在检查实例属性的类型
class Account:
    name = TypedAttribute("name",str) 
    balance = TypedAttribute("balance",int, 42)
    
    def name_balance_str(self):
        return str(self.name) + str(self.balance)

>> acct = Account()
>> acct.name = "obi"
>> acct.balance = 1234
>> acct.balance
1234
>> acct.name 
obi
# trying to assign a string to number fails
>> acct.balance = '1234'
TypeError: Must be a <type 'int'>

如果仔细考虑一下,似乎的确只有在类型级别定义此类描述符才有意义,因为如果在实例级别定义,则对该属性的任何赋值都将覆盖该描述符。 必须阅读 python 虚拟机的源代码,以了解 Python 的整体描述符的方式。 描述符提供了 Python 中的属性,静态方法,类方法和许多其他功能的机制。 为了具体说明描述符的重要性,请考虑用于从用户定义类型的实例 b 解析属性的算法,如清单4.12所示。

代码清单4.12: 用于搜索用户定义类型属性的算法
1. 在 type(b).__dict__ 中搜索属性名,如果找到并且它是一个数据描述符,
则调用描述符的 __get__ 方法,然后将结果返回。如果没有找到则在 type(b) 
的 *mro* 的所有类中以相同方式继续搜索。
2. 在 b.__dict__ 中搜索属性名,如果找到则将其返回
3. 如果 1 中的名字是一个非数据描述符,返回 __get__ 的调用结果。
4. 如果名字没有没找到,将 raise 一个 AttributeError。
或者如果用户定义了 __getattr__ 的话,调用并返回结果。

清单4.12中列出的算法说明了在属性引用期间,会首先检查描述符对象,以及 TypedAttribute 描述符如何表示对象的属性,每当引用诸如 b.name 之类的属性时,都会在Account 类型对象中搜索该属性,在这种情况下,将找到TypedAttribute 描述符,并且 __get__ 方法被相应地调用。 TypedAttribute 示例说明了一个描述符,但是这个例子过于理想。 为了真正了解描述符对于语言核心的重要性,我们看一些其它例子来说明如何应用描述符。

请注意,清单4.12中的属性引用算法与引用类型为 type 的属性时使用的算法不同。 清单4.13显示了这种算法。

代码清单4.13: 用于搜索 type 类型属性的算法
1. 在 type(type).__dict__ 中搜索属性名,如果找到了它并且是一个数据描述符,将返回
描述符 __get__ 方法调用的结果。如果名字没有被找到,且将在 type(type) 的 *mro* 的
所有类中以相同方式搜索。
2. 在 type.__dict__ 中搜索,如果找到,并且是一个描述符,调用 __get__ 的结果,返回。
如果是一个普通的属性,则直接返回该属性。
3. 如果 1 中找到的是一个非数据描述符,则返回 __get__ 的调用结果。
4. 如果 1 中找到的并非一个描述符,则返回这个值。

在 VM 内部使用描述符进行属性引用的示例

描述符在 Python 中的属性引用中起着非常重要的作用。 考虑本章前面讨论的类型数据结构。 任何希望被视为描述符的类型实例都可以填充类型数据结构中的 tp_descr_gettp_descr_set 字段。 函数对象是一个很好的展示其工作原理的例子。

给定类型定义,如清单4.11中的 Account ,请考虑当我们从中引用方法 name_balance_str 时,如 Account.name_balance_str,以及如清单4.14中所示从实例引用相同的方法时会发生什么。

代码清单4.14: bound function 与 unbound method 的说明。
>> a = Account()
>> a.name_balance_str
<bound method Account.name_balance_str of <__main__.Account object at 
0x102a0ae10>>

>> Account.name_balance_str
<function Account.name_balance_str at 0x102a2b840>

查看清单4.14中的代码段,尽管我们似乎引用了相同的属性,但返回的实际对象的值和类型不同。 从 Account 类型引用时,返回的值是函数类型,但是从 Account 类型的实例引用时,返回的结果是绑定方法(bound method)类型。 这完全是可能的,因为函数其实也是描述符。 清单4.15显示了函数对象类型的定义

代码清单4.15: 函数对象类型定义
PyTypeObject PyFunction_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "function",
    sizeof(PyFunctionObject),
    0,
    (destructor)func_dealloc,                   /* tp_dealloc */
    0,                                          /* tp_print */
    0,                                          /* tp_getattr */
    0,                                          /* tp_setattr */
    0,                                          /* tp_reserved */
    (reprfunc)func_repr,                        /* tp_repr */
    0,                                          /* tp_as_number */
    0,                                          /* tp_as_sequence */
    0,                                          /* tp_as_mapping */
    0,                                          /* tp_hash */
    function_call,                              /* tp_call */
    0,                                          /* tp_str */
    0,                                          /* tp_getattro */
    0,                                          /* tp_setattro */
    0,                                          /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,    /* tp_flags */
    func_new__doc__,                            /* tp_doc */
    (traverseproc)func_traverse,                /* tp_traverse */
    0,                                          /* tp_clear */
    0,                                          /* tp_richcompare */
    offsetof(PyFunctionObject, func_weakreflist), /* tp_weaklistoffset */
    0,                                          /* tp_iter */
    0,                                          /* tp_iternext */
    0,                                          /* tp_methods */
    func_memberlist,                            /* tp_members */
    func_getsetlist,                            /* tp_getset */
    0,                                          /* tp_base */
    0,                                          /* tp_dict */
    func_descr_get,                             /* tp_descr_get */
    0,                                          /* tp_descr_set */
    offsetof(PyFunctionObject, func_dict),      /* tp_dictoffset */
    0,                                          /* tp_init */
    0,                                          /* tp_alloc */
    func_new,                                   /* tp_new */
};

函数对象使用 func_descr_get 函数填充 tp_descr_get 字段,因此函数类型的实例是非数据描述符。 清单4.16显示了 funct_descr_get 方法的实现。

代码清单4.16: funct_descr_get 函数定义
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);
}

如上一节所述,可以在类型属性解析或实例属性解析期间调用 func_descr_get。 从类型调用时,对 func_descr_get 的调用类似于 local_get(attribute, (PyObject )NULL,(PyObject )type) 这种,而从用户定义类型的实例的属性引用进行调用时,调用签名为 f(descr, obj, (PyObject *)Py_TYPE(obj))。 仔细阅读清单4.16中func_descr_get 的实现,我们看到如果实例为 NULL,则函数本身将返回,而当将实例传递给调用时,则会使用该函数和实例创建一个新的方法对象。 总结了 python 如何使用描述符为同一函数引用返回不同类型。

当在类中定义方法时,我们将 self 参数用作任何实例方法的第一个参数,因为实际上,实例方法将实例(按惯例称为self)作为第一个参数。 诸如 b.name_balance_str() 之类的调用实际上与 type(b).name_balance_str(b) 相同。 之所以能够调用 b.name_balance_str(),是因为b.name_balance_str 返回的值是一个方法对象,该对象是name_balance_str 的一个薄封装,实例已绑定到该方法实例。 因此,当我们进行诸如 b.name_balance_str() 的调用时,该方法将绑定实例用作包装函数的参数,从而向我们隐藏了此详细信息。

在另一个重要的描述符实例中,请考虑清单4.17中的代码片段,该代码片段显示了从内置类型的实例和用户定义的类型的实例访问 __dict__ 属性的结果。

代码清单4.17: 在内置类型与用户定义类型的实例中访问 __dict__
class A: 
    pass

>>> A.__dict__
mappingproxy({'__module__': '__main__', '__doc__': None, '__weakref__': <att\
ribute '__weakref__' of 'A' objects>, '__dict__': <attribute '__dict__' of 'A' objec\
ts>})
>>> i = A()
>>> i.__dict__
{}
>>> A.__dict__['name'] = 1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'mappingproxy' object does not support item assignment
>>> i.__dict__['name'] = 2
>>> i.__dict__
{'name': 2}
>>> 

从清单4.17可以看出,当引用 __dict__ 属性时,两个对象都没有返回普通字典类型。 当 type 实例返回支持所有常用字典功能的普通字典映射时,类型对象似乎返回了我们甚至无法分配给它的映射代理。 因此,似乎这些对象的属性引用方式有所不同。 从后面的几节中回顾描述的属性搜索算法。 第一步是在对象类型的 __dict__ 中搜索属性,因此我们继续对清单4.18中的两个对象执行此操作。

代码清单4.18: 检查对象类型的 __dict__
>>> type(type.__dict__['__dict__']) # type of A is type
<class 'getset_descriptor'>
 type(A.__dict__['__dict__'])
<class 'getset_descriptor'>

我们看到 __dict__ 属性由两个对象的数据描述符表示,所以这就是我们可以得到不同的对象类型的原因。 我们想找出在此描述符的幕后情况,就像在函数和绑定方法的情况下一样。 一个很好的起点是 Objects /typeobject.c 模块和 type 类型对象的定义。 一个有趣的字段是 tpgetset 字段,其中包含一个 C 结构数组(PyGetSetDef值),如清单4.19所示。 这是将其描述符对象插入到类型的 __dict__ 属性中的值的集合,这是类型对象的 tp_dict 字段指向的映射。

代码清单4.19: 检查 type 对象中的 __dict__
static PyGetSetDef type_getsets[] = {
    {"__name__", (getter)type_name, (setter)type_set_name, NULL},
    {"__qualname__", (getter)type_qualname, (setter)type_set_qualname, NULL},
    {"__bases__", (getter)type_get_bases, (setter)type_set_bases, NULL},
    {"__module__", (getter)type_module, (setter)type_set_module, NULL},
    {"__abstractmethods__", (getter)type_abstractmethods,
     (setter)type_set_abstractmethods, NULL},
    {"__dict__",  (getter)type_dict,  NULL, NULL},
    {"__doc__", (getter)type_get_doc, (setter)type_set_doc, NULL},
    {"__text_signature__", (getter)type_get_text_signature, NULL, NULL},
    {0}
};

这些值不是唯一将描述符插入类型字典的值,还有其他值,例如 tp_memberstp_methods 值,它们具有创建的描述符并在类型初始化期间插入到 tp_dict 中。 在类型上调用 PyType_Ready 函数时,会将这些值插入dict。 作为 PyType_Ready 函数初始化过程的一部分,将为 type_getsets 中的每个条目创建描述符对象,然后将其添加到 tp_dict 映射中。Objects/typeobject.c 中的 add_getset 函数将对此进行处理。

回到我们的 __dict__ 属性,我们知道在类型初始化之后,__dict__ 属性存在于类型的 tp_dict 字段中,因此让我们看看该描述符的 getter 函数的作用。 getter 函数是清单4.20中所示的type_dict 函数。

代码清单4.20: type 实例的 getter 函数
static PyObject *
type_dict(PyTypeObject *type, void *context)
{
    if (type->tp_dict == NULL) {
        Py_RETURN_NONE;
    }
    return PyDictProxy_New(type->tp_dict);
}

tp_getattro 字段指向该函数,该函数是获取任何对象的属性的第一个调用端口。 对于 type 对象,它指向 type_getattro 函数。 该方法又实现了清单4.13中描述的属性搜索算法。 __dict__ 属性的类型字典中的描述符所调用的函数是清单4.20中给出的 type_dict 函数,它很容易理解。 这里的返回值我们很感兴趣, 它是包含类型属性的实际字典的字典代理, 这解释了查询类型对象的 __dict__ 属性时返回的 mappingproxy 类型。

那么,用户定义类型 A 的实例又如何解析 __dict__ 属性呢? 现在回想一下,A 实际上是 type 类型的对象,因此我们在 Object/typeobject.c 模块中搜寻以了解如何创建新的类型实例。 PyType_Typetp_new 字段包含用于创建新类型对象的 type_new 函数。 仔细阅读函数中的所有类型创建代码,就会偶然发现清单4.21中的代码片段。

代码清单4.21: 为用户定义类型设置 tp_getset 字段
    ...
    if (type->tp_weaklistoffset && type->tp_dictoffset)
        type->tp_getset = subtype_getsets_full;
    else if (type->tp_weaklistoffset && !type->tp_dictoffset)
        type->tp_getset = subtype_getsets_weakref_only;
    else if (!type->tp_weaklistoffset && type->tp_dictoffset)
        type->tp_getset = subtype_getsets_dict_only;
    else
        type->tp_getset = NULL;
    ...

假设第一个条件为 true,则 tp_getset 字段将填充清单4.22中所示的值。

代码清单4.22: type 实例的 getset 值
static PyGetSetDef subtype_getsets_full[] = {
    {"__dict__", subtype_dict, subtype_setdict,
     PyDoc_STR("dictionary for instance variables (if defined)")},
    {"__weakref__", subtype_getweakref, NULL,
     PyDoc_STR("list of weak references to the object (if defined)")},
    {0}
};

调用 (*tp->tp_getattro)(v, name) 时,将调用包含指向PyObject_GenericGetAttr 的指针的 tp_getattro 字段。 该函数负责为用户定义的类型实现属性搜索算法。 对于字典属性,在对象类型的字典中找到描述符,而描述符的 __get__ 函数是为清单4.22中的 __dict__ 属性定义的 subtype_dict 函数。 清单4.23显示了subtype_dict getter 函数。

代码清单4.22: 用户定义类型 __dict__ 属性的 getter 函数
static PyObject *
subtype_dict(PyObject *obj, void *context)
{
    PyTypeObject *base;

    base = get_builtin_base_with_dict(Py_TYPE(obj));
    if (base != NULL) {
        descrgetfunc func;
        PyObject *descr = get_dict_descriptor(base);
        if (descr == NULL) {
            raise_dict_descr_error(obj);
            return NULL;
        }
        func = Py_TYPE(descr)->tp_descr_get;
        if (func == NULL) {
            raise_dict_descr_error(obj);
            return NULL;
        }
        return func(descr, obj, (PyObject *)(Py_TYPE(obj)));
    }
    return PyObject_GenericGetDict(obj, context);
}

当对象实例处于继承层次结构中时,get_builtin_base_with_dict 返回一个值,因此忽略该实例是适当的。 PyObject_GenericGetDict 对象被调用。 清单4.24显示了 PyObject_GenericGetDict 和实际获取实例字典的关联辅助函数。 实际的获取 dict 的函数是_PyObject_GetDictPtr 函数,该函数查询对象的 dictoffset 并将其用于计算实例 dict 的地址。 在此函数返回空值的情况下, PyObject_GenericGetDict 可以继续向调用函数返回新的字典。

代码清单4.24: 获取用户定义类型的 dict 属性
PyObject *
PyObject_GenericGetDict(PyObject *obj, void *context)
{
    PyObject *dict, **dictptr = _PyObject_GetDictPtr(obj);
    if (dictptr == NULL) {
        PyErr_SetString(PyExc_AttributeError,
                        "This object has no __dict__");
        return NULL;
    }
    dict = *dictptr;
    if (dict == NULL) {
        PyTypeObject *tp = Py_TYPE(obj);
        if ((tp->tp_flags & Py_TPFLAGS_HEAPTYPE) && CACHED_KEYS(tp)) {
            DK_INCREF(CACHED_KEYS(tp));
            *dictptr = dict = new_dict_with_shared_keys(CACHED_KEYS(tp));
        }
        else {
            *dictptr = dict = PyDict_New();
        }
    }
    Py_XINCREF(dict);
    return dict;
}

PyObject **
_PyObject_GetDictPtr(PyObject *obj)
{
    Py_ssize_t dictoffset;
    PyTypeObject *tp = Py_TYPE(obj);

    dictoffset = tp->tp_dictoffset;
    if (dictoffset == 0)
        return NULL;
    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);

        dictoffset += (long)size;
        assert(dictoffset > 0);
        assert(dictoffset % SIZEOF_VOID_P == 0);
    }
    return (PyObject **) ((char *)obj + dictoffset);
}

该解释简要总结了如何根据类型使用描述符来实现自定义属性访问。 在整个 VM 中,对于使用描述符执行属性访问的其他实例,使用上述相同策略。 描述符在虚拟机中无处不在。__slots__,静态方法和类方法,属性也只是使用描述符实现的语言功能而已。

4.6 方法解析顺序(MRO)

在讨论属性引用时,我们已经提到了mro,但是由于没有进行过多讨论,因此在本节中,我们将对 mro 进行更详细的介绍。 在 python 中,类型可以有多重继承层次结构,因此当一个类型从多个类继承时,需要一种顺序来搜索方法。 如我们在属性参考解析算法中所看到的那样,在搜索其他非方法属性时,也实际上使用了称为方法解析顺序(Method Resolution Order, MRO)的该顺序。 文章 Python 2.3 Method Resolution order,是有关 python 中使用的方法解析算法的出色且易于阅读的文档,这里总结了一些要点。

当一个类型从多个基本类型继承时,Python 使用 C3 算法来构建方法的解析顺序(在此也称为线性化)。 清单4.25显示了一些用于解释该算法的符号。

代码清单4.25
C1 C2 ... CN 代表一系列类,放在一个列表(list)中: [C1, C2, C3 .., CN]

头(head)是第一个元素: head = C1

尾巴(tail)是其余元素: tail = C2 ... CN.

C + (C1 C2 ... CN) = C C1 C2 ... CN 代表列表的加和:
[C] + [C1, C2, ... ,CN]

考虑多重继承层次结构中的类型 C,其中 C 从基本类型 B1B2,...,BN 继承,则 C 的线性化是 C 加上父项的线性化与 B 的列表的合并: L[C(B1 ... BN)] = C + merge(L[B1] ... L[BN], B1 ... BN) 。 没有父对象的对象类型的线性化是微不足道的 L[object] = object。 合并操作是根据以下算法计算的:

取出第一个列表的 head,即 L[B1][0],,如果 head 不在任何其他列表的尾部,则将其添加到 C 的线性化中,然后从合并中的列表中将其删除,否则,请查看下一个列表的 head 并采用它,如果它是一个正常的 head 然后重复该操作,直到所有类都被删除,或者不可能找到正常的 head。 在这种情况下,不可能构造合并,Python2.3 将拒绝创建类 C 并引发异常。

使用此算法无法线性化某些类型层次结构,在这种情况下,VM 会引发错误,并且不会创建此类层次结构。

假设我们有一个如图4.1所示的继承层次结构,则创建 mro 的算法将从层次结构的顶部开始依次为 OABOAB 的线性化是很简单的:

代码清单4.26: 计算 O, A, B 的线性化
    L[O] = O 
    L[A] = A O
    L[B] = B O

X 的线性化可以计算为 L[X] = X + merge(AO, BO, AB)

A 是一个正常的 head,因此将其添加到线性化中,然后剩下的要计算merge(O, BO, B)O 不是正常 head,因为它位于 BO 的尾部,因此我们跳到下一个序列。 B 是一个正常的 head,因此我们将其添加到线性化中,然后剩下的就可以计算归并为 Omerge(O, O)。所得的 X 线性化 L[X] = X A B O

使用上面相同的过程,如清单4.27所示,计算 Y 的线性化:

代码清单4.27: 计算 Y 的线性化
    L[Y] = Y + merge(AO,BO,AB)
         = Y + A + merge(O,BO,B)
         = Y + A + B + merge(O,O)
         = Y A B O

计算 XY 的线性化后,我们现在可以计算 Z 的线性化,如清单4.28所示。

代码清单4.28: 计算 Z 的线性化
    L[Z] = Z + merge(XABO,YABO,XY)
         = Z + X + merge(ABO,YABO,Y)
         = Z + X + Y + merge(ABO,ABO)
         = Z + X + Y + A + merge(BO,BO)
         = Z + X + Y + A + B + merge(O,O)
         = Z X Y A B O

Last updated