深入解析CPython的`对象模型`:`PyObject`、`PyVarObject`和`TypeObject`的`内存`布局。

CPython 对象模型:PyObject、PyVarObject 和 TypeObject 的内存布局

大家好,今天我们深入探讨 CPython 的对象模型,重点关注 PyObjectPyVarObjectTypeObject 这三个核心结构体的内存布局。 理解这些结构体的内存布局对于深入理解 CPython 的内存管理、类型系统和对象生命周期至关重要。

1. CPython 对象模型的基石:PyObject

PyObject 是 CPython 中所有对象的基类。 它定义了所有 Python 对象共有的行为和属性。 它的定义如下(简化版):

typedef struct _object {
    _PyObject_HEAD_EXTRA
    Py_ssize_t ob_refcnt;
    struct _typeobject *ob_type;
} PyObject;
  • _PyObject_HEAD_EXTRA: 这是一个宏,通常在 debug 构建中使用,用于实现双向链表,用于检测内存泄漏。 在 release 构建中,它通常为空。 我们暂时忽略它。

  • ob_refcnt: 这是一个引用计数器,用于跟踪有多少个指针指向该对象。 当引用计数降至 0 时,对象将被释放。 这是 CPython 内存管理的核心机制。

  • ob_type: 这是一个指向 PyTypeObject 结构体的指针,它定义了对象的类型。 PyTypeObject 包含了关于对象类型的各种信息,例如类型名称、大小、方法等。

内存布局:

PyObject 的内存布局非常简单:

字段 类型 描述
ob_refcnt Py_ssize_t 引用计数器
ob_type PyTypeObject* 指向类型对象的指针

示例:

假设我们创建一个简单的整数对象:

x = 10

在 CPython 内部,会分配一块内存来存储这个整数对象。 这块内存的前面部分就是 PyObject 结构体,包含引用计数和类型信息。 ob_type 指针会指向 int 类型的 PyTypeObject 实例。 实际的整数值 10 存储在 PyObject 之后(具体如何存储取决于类型)。

2. 可变长度对象:PyVarObject

PyVarObjectPyObject 的一个扩展,用于表示可变长度的对象,例如字符串、列表和元组。 它的定义如下(简化版):

typedef struct {
    PyObject ob_base;
    Py_ssize_t ob_size; /* Number of items in variable part */
} PyVarObject;
  • ob_base: 这是一个 PyObject 类型的成员,表示 PyVarObject 继承自 PyObject。 这意味着 PyVarObject 包含了 PyObject 的所有成员(ob_refcntob_type)。

  • ob_size: 这是一个表示对象中项目数量的字段。 例如,对于列表,ob_size 表示列表中元素的数量;对于字符串,ob_size 表示字符串的长度。

内存布局:

PyVarObject 的内存布局如下:

字段 类型 描述
ob_refcnt Py_ssize_t 引用计数器
ob_type PyTypeObject* 指向类型对象的指针
ob_size Py_ssize_t 对象中项目数量(例如,列表的长度,字符串的长度)

示例:

假设我们创建一个字符串对象:

s = "hello"

在 CPython 内部,会分配一块内存来存储这个字符串对象。 这块内存的前面部分是 PyVarObject 结构体,包含引用计数、类型信息和字符串长度。 ob_type 指针会指向 str 类型的 PyTypeObject 实例。 ob_size 将设置为 5 (字符串 "hello" 的长度)。 实际的字符串数据("hello")存储在 PyVarObject 之后。

代码示例:访问 ob_size

虽然我们不能直接在 Python 代码中访问 ob_size 这样的 C 结构体成员,但我们可以通过一些 C 扩展来演示如何访问它。 以下是一个简单的 C 扩展示例:

#include <Python.h>

static PyObject* get_string_size(PyObject *self, PyObject *args) {
    PyObject *obj;

    if (!PyArg_ParseTuple(args, "O", &obj)) {
        return NULL;
    }

    if (!PyUnicode_Check(obj)) {
        PyErr_SetString(PyExc_TypeError, "Argument must be a string");
        return NULL;
    }

    Py_ssize_t size = PyUnicode_GET_LENGTH(obj); // 使用宏来获取字符串长度

    return PyLong_FromSsize_t(size);
}

static PyMethodDef StringSizeMethods[] = {
    {"get_size",  get_string_size, METH_VARARGS, "Get the size of a string."},
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

static struct PyModuleDef stringsizemodule = {
    PyModuleDef_HEAD_INIT,
    "stringsize",   /* name of module */
    NULL,           /* module documentation, may be NULL */
    -1,             /* size of per-interpreter state of the module,
                       or -1 if the module keeps state in global variables. */
    StringSizeMethods
};

PyMODINIT_FUNC
PyInit_stringsize(void)
{
    return PyModule_Create(&stringsizemodule);
}

要编译和使用这个 C 扩展,你需要:

  1. 将上面的代码保存为 stringsize.c
  2. 创建一个 setup.py 文件:
from distutils.core import setup, Extension

module1 = Extension('stringsize',
                    sources = ['stringsize.c'])

setup (name = 'StringSize',
       version = '1.0',
       description = 'This is a demo package',
       ext_modules = [module1])
  1. 使用以下命令编译:
python setup.py build_ext --inplace
  1. 在 Python 中使用:
import stringsize

s = "hello"
size = stringsize.get_size(s)
print(size)  # 输出: 5

这个例子展示了如何通过 C 扩展来访问字符串对象的长度信息。虽然这里我们使用了 PyUnicode_GET_LENGTH 宏,而不是直接访问 ob_size,但原理是相似的。 在内部,PyUnicode_GET_LENGTH 宏通常会访问或计算 ob_size 的值(具体实现取决于 Python 版本和字符串的存储方式)。

3. 类型对象:PyTypeObject

PyTypeObject 是 CPython 类型系统的核心。 它定义了对象的类型,并包含了关于该类型的各种信息,例如类型名称、大小、方法、属性等。 每个类型都有一个唯一的 PyTypeObject 实例。

typedef struct _typeobject {
    PyObject_HEAD
    const char *tp_name; /* For printing, in format "<module>.<name>" */
    Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */

    /* Method suites for standard operations */
    destructor tp_dealloc;
    Py_typedef_slot tp_vectorcall_offset;
    getattrfunc tp_getattr;
    setattrfunc tp_setattr;
    PyAsyncMethods *tp_as_async; /* formerly known as tp_compare */
    reprfunc tp_repr;

    /* Method suites for standard operations */
    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 */

    /* call function for all accessible objects of type */
    traverseproc tp_traverse;

    /* delete references to contained objects */
    inquiry tp_clear;

    /* 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 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;
    PyType_Slot *tp_vectorcall;
} PyTypeObject;
  • PyObject_HEAD: 这是 PyObject 结构体,意味着 PyTypeObject 也继承自 PyObject,拥有引用计数和类型信息。 PyTypeObject 自身的 ob_type 指向 PyType_Type,也就是类型对象的类型。

  • tp_name: 类型的名称,例如 "int", "str", "list"。

  • tp_basicsize: 类型对象的大小,即该类型对象实例所需分配的内存大小。

  • tp_itemsize: 对于可变长度对象,表示每个 item 的大小。例如 list 类型的 tp_itemsize 是列表中每个元素的大小。

  • tp_dealloc: 析构函数,用于释放该类型的对象。

  • tp_as_number, tp_as_sequence, tp_as_mapping: 指向实现了数值、序列和映射协议的函数指针的结构体。

  • tp_methods: 指向一个 PyMethodDef 数组的指针,该数组定义了该类型的对象可用的方法。

  • tp_members: 指向一个 PyMemberDef 数组的指针,该数组定义了该类型的对象可用的成员变量。

  • tp_getset: 指向一个 PyGetSetDef 数组的指针,该数组定义了该类型的对象可用的 get 和 set 属性。

  • tp_base: 指向基类的 PyTypeObject 指针。

  • tp_dict: 一个字典,包含了类型的命名空间。

  • tp_init: 初始化函数,用于初始化新创建的对象。

  • tp_new: 创建新对象的函数。

  • tp_call: 如果对象可以被调用,则指向调用该对象的函数。

  • tp_alloc: 分配内存的函数。

  • tp_free: 释放内存的函数。

  • tp_flags: 标志位,指示类型的各种特性。

  • tp_doc: 该类型的文档字符串。

内存布局:

PyTypeObject 的内存布局非常复杂,因为它包含了大量的函数指针和数据成员。 以下是一个简化的表示:

字段 类型 描述
ob_refcnt Py_ssize_t 引用计数器
ob_type PyTypeObject* 指向类型对象的类型 (通常是 PyType_Type)
tp_name const char* 类型名称
tp_basicsize Py_ssize_t 类型对象的大小
tp_itemsize Py_ssize_t 每个 item 的大小 (可变长度对象)
其他字段 (函数指针, 标志位, 等)

示例:整数类型的 PyTypeObject

整数类型的 PyTypeObject 实例是 PyLong_Type。 我们可以通过 CPython 的源码(Objects/longobject.c)找到它的定义。 它定义了整数类型的大小、方法、以及其他相关的操作。 例如,PyLong_Type.tp_as_number 包含了指向整数的加法、减法、乘法等操作的函数指针。PyLong_Type.tp_methods 包含了诸如 bit_length 等方法的定义。

代码示例:访问 tp_name

同样,我们可以使用 C 扩展来访问 tp_name 字段:

#include <Python.h>

static PyObject* get_type_name(PyObject *self, PyObject *args) {
    PyObject *obj;

    if (!PyArg_ParseTuple(args, "O", &obj)) {
        return NULL;
    }

    PyTypeObject *type = Py_TYPE(obj); // 获取对象的类型对象
    const char *type_name = type->tp_name;

    return PyUnicode_FromString(type_name);
}

static PyMethodDef TypeNameMethods[] = {
    {"get_name",  get_type_name, METH_VARARGS, "Get the name of a type."},
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

static struct PyModuleDef typenamemodule = {
    PyModuleDef_HEAD_INIT,
    "typename",   /* name of module */
    NULL,           /* module documentation, may be NULL */
    -1,             /* size of per-interpreter state of the module,
                       or -1 if the module keeps state in global variables. */
    TypeNameMethods
};

PyMODINIT_FUNC
PyInit_typename(void)
{
    return PyModule_Create(&typenamemodule);
}

编译和使用方法与之前的 stringsize 模块类似。 在 Python 中使用:

import typename

x = 10
type_name = typename.get_name(x)
print(type_name)  # 输出: int

这个例子展示了如何通过 C 扩展来获取对象的类型名称。 Py_TYPE(obj) 宏用于获取对象的 ob_type 指针,然后我们可以访问 tp_name 字段。

4. 内存布局的实际意义

理解 PyObjectPyVarObjectPyTypeObject 的内存布局对于以下方面至关重要:

  • 内存管理: CPython 的垃圾回收机制依赖于 ob_refcnt 引用计数器。 了解对象的内存布局可以帮助我们更好地理解引用计数的工作原理,以及如何避免内存泄漏。

  • 类型系统: PyTypeObject 定义了对象的类型,并包含了关于该类型的各种信息。 了解 PyTypeObject 的结构可以帮助我们更好地理解 CPython 的类型系统,以及如何创建自定义类型。

  • 性能优化: 了解对象的内存布局可以帮助我们编写更高效的代码。 例如,如果我们需要频繁访问对象的某个属性,我们可以通过 C 扩展直接访问该属性的内存地址,从而提高性能。

  • 调试: 在调试 CPython 扩展时,了解对象的内存布局可以帮助我们更好地理解程序的状态,并找到错误的原因。

5. 对象的创建和销毁

Python 对象的创建通常涉及到 tp_alloctp_new 函数。 tp_alloc 负责分配内存,而 tp_new 负责初始化对象。对象的销毁则通过 tp_dealloc 函数完成,该函数会释放对象占用的内存并处理相关的清理工作。当对象的引用计数降为 0 时,tp_dealloc 会被调用。

示例:对象创建流程

  1. 用户代码: x = MyClass()
  2. CPython 解释器: 找到 MyClass 对应的 PyTypeObject
  3. tp_alloc: 调用 MyClass->tp_alloc 分配内存。
  4. tp_new: 调用 MyClass->tp_new 初始化对象。
  5. tp_init: 调用 MyClass->tp_init (如果存在) 执行进一步的初始化。
  6. 返回: 返回指向新创建对象的指针。

示例:对象销毁流程

  1. 引用计数降为 0: x 的引用计数降为 0。
  2. tp_dealloc: 调用 MyClass->tp_dealloc 释放对象。
  3. 清理: tp_dealloc 负责清理对象相关的资源,例如释放持有的其他对象的引用。
  4. 释放内存: tp_dealloc 最终会释放对象占用的内存。

6. 总结:CPython 对象模型的核心

PyObject 是所有 Python 对象的基石,提供引用计数和类型信息。 PyVarObject 用于表示可变长度对象,提供长度信息。 PyTypeObject 定义了对象的类型,包含了类型名称、大小、方法等重要信息。 理解这三个结构体的内存布局对于深入理解 CPython 的内存管理和类型系统至关重要。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注