Python源码解析-Objects类型

本文基于Python3.10.4。

简介

在python中,有两种类型可以保存bytes(字节)类型的数据。分别是bytes与bytearray。其中bytearray支持修改任意位置的值,而bytes和tuple一样,是不可变的,无法更改其中的值。

bytes类型:

>>> a = bytes(b'123456789')
>>> type(a)
<class 'bytes'>
>>> a[0] = 97
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: 'bytes' object does not support item assignment

bytearray类型:

>>> b = bytearray(b'123456789')
>>> type(b)
<class 'bytearray'>
>>> b
bytearray(b'123456789')
>>> b[0]=97
>>> b
bytearray(b'a23456789')

PyBytesObject类型是python源码中bytes类型的实现,PyByteArrayObject类型是bytearray类型的实现,这两者实际使用差不多,只是bytes类型无法编辑,这里只针对PyBytesObject类型进行分析。

类型定义

[Include/cpython/bytesobject.h]
typedef struct {
    PyObject_VAR_HEAD
    Py_hash_t ob_shash;
    char ob_sval[1];

    /* Invariants:
     *     ob_sval contains space for 'ob_size+1' elements.
     *     ob_sval[ob_size] == 0.
     *     ob_shash is the hash of the byte string or -1 if not computed yet.
     */
} PyBytesObject;

PyBytesObject类型同其它所有类型一样,都基于PyObject对象。

PyObject_VAR_HEAD:python 可变对象的公共部分

ob_shash:对象中保存的字节数组的哈希值,没计算之前为-1,计算一次之后将会保存哈希值,以后的哈希计算将直接返回ob_shash的值。这也是python源码中提高性能的一个小缓存机制。

ob_sval:保存数据的真实位置,通过注释可以看到,它的空间是ob_size+1的长度,并且最后一个位置为0。这其实是C语言的规定,字符串的最后一个字符为\0。(ob_size的定义在PyObject_VAR_HEAD中,是python可变对象用来记录对象长度的值)

类型对象

python中每一种类型的行为,都由相应的类型对象决定。要了解PyBytesObject对象具有哪些操作,就需要通过它的类型对象来了解。

[Objects/bytesobject.c]
PyTypeObject PyBytes_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "bytes",
    PyBytesObject_SIZE,
    sizeof(char),
    0,                                          /* tp_dealloc */
    0,                                          /* tp_vectorcall_offset */
    0,                                          /* tp_getattr */
    0,                                          /* tp_setattr */
    0,                                          /* tp_as_async */
    (reprfunc)bytes_repr,                       /* tp_repr */
    &bytes_as_number,                           /* tp_as_number */
    &bytes_as_sequence,                         /* tp_as_sequence */
    &bytes_as_mapping,                          /* tp_as_mapping */
    (hashfunc)bytes_hash,                       /* tp_hash */
    0,                                          /* tp_call */
    bytes_str,                                  /* tp_str */
    PyObject_GenericGetAttr,                    /* tp_getattro */
    0,                                          /* tp_setattro */
    &bytes_as_buffer,                           /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE |
        Py_TPFLAGS_BYTES_SUBCLASS |
        _Py_TPFLAGS_MATCH_SELF,               /* tp_flags */
    bytes_doc,                                  /* tp_doc */
    0,                                          /* tp_traverse */
    0,                                          /* tp_clear */
    (richcmpfunc)bytes_richcompare,             /* tp_richcompare */
    0,                                          /* tp_weaklistoffset */
    bytes_iter,                                 /* tp_iter */
    0,                                          /* tp_iternext */
    bytes_methods,                              /* tp_methods */
    0,                                          /* tp_members */
    0,                                          /* tp_getset */
    &PyBaseObject_Type,                         /* tp_base */
    0,                                          /* tp_dict */
    0,                                          /* tp_descr_get */
    0,                                          /* tp_descr_set */
    0,                                          /* tp_dictoffset */
    0,                                          /* tp_init */
    0,                                          /* tp_alloc */
    bytes_new,                                  /* tp_new */
    PyObject_Del,                               /* tp_free */
};

当我看到tp_as_number的值不为0时,我其实有点懵的,刚看源码的可能不太了解这tp_as_xxx这个参数的含义。

tp_as_number: 可以将一个对象能被视为数值对象,进行的操作。

tp_as_sequence: 可以将一个对象能被视为序列对象,进行的操作。

tp_as_mapping:可以将一个对象能被视为关联对象,进行的操作。

那么一个bytes字节类型的对象,如何能够视为数值对象来进行操作呢?我们进一步查看bytes_as_number的定义。

static PyNumberMethods bytes_as_number = {
    0,              /*nb_add*/
    0,              /*nb_subtract*/
    0,              /*nb_multiply*/
    bytes_mod,      /*nb_remainder*/
};
static PyObject *
bytes_mod(PyObject *self, PyObject *arg)
{
    if (!PyBytes_Check(self)) {
        Py_RETURN_NOTIMPLEMENTED;
    }
    return _PyBytes_FormatEx(PyBytes_AS_STRING(self), PyBytes_GET_SIZE(self),
                             arg, 0);
}

看到这个bytes_mod还是没怎么明白,修为不到家,只能进一步去查看bytes_mod的实现了。直到看到FormatEx才明白,它并没有将bytes对象视为数值对象来进行运算,只是实现了一个格式化的方法。

>>> print(b'I am %s' % b"bytes")
b'I am bytes'

对象操作

bytes作为一个序列对象,我们重点再来了解一下,它支持哪一些序列操作。

static PySequenceMethods bytes_as_sequence = {
    (lenfunc)bytes_length, /*sq_length*/
    (binaryfunc)bytes_concat, /*sq_concat*/
    (ssizeargfunc)bytes_repeat, /*sq_repeat*/
    (ssizeargfunc)bytes_item, /*sq_item*/
    0,                  /*sq_slice*/
    0,                  /*sq_ass_item*/
    0,                  /*sq_ass_slice*/
    (objobjproc)bytes_contains /*sq_contains*/
};

bytes_length:获取对象的长度

bytes_concat:合并两个序列

bytes_repeat:重复序列

bytes_item:通过下标取值

bytes_contains:判断是否存在包含关系

这里查看两个方法的实现,其它有兴趣的可以再从源码中学习。

static Py_ssize_t
bytes_length(PyBytesObject *a)
{
    return Py_SIZE(a);
}

bytes_length简单调用了Py_SIZE,获取到序列的大小返回。

static PyObject *
bytes_concat(PyObject *a, PyObject *b)
{
    Py_buffer va, vb;
    PyObject *result = NULL;

    va.len = -1;
    vb.len = -1;
    if (PyObject_GetBuffer(a, &va, PyBUF_SIMPLE) != 0 ||
        PyObject_GetBuffer(b, &vb, PyBUF_SIMPLE) != 0) {
        PyErr_Format(PyExc_TypeError, "can't concat %.100s to %.100s",
                     Py_TYPE(b)->tp_name, Py_TYPE(a)->tp_name);
        goto done;
    }

    /* Optimize end cases */
    if (va.len == 0 && PyBytes_CheckExact(b)) {
        result = b;
        Py_INCREF(result);
        goto done;
    }
    if (vb.len == 0 && PyBytes_CheckExact(a)) {
        result = a;
        Py_INCREF(result);
        goto done;
    }

    if (va.len > PY_SSIZE_T_MAX - vb.len) {
        PyErr_NoMemory();
        goto done;
    }

    result = PyBytes_FromStringAndSize(NULL, va.len + vb.len);
    if (result != NULL) {
        memcpy(PyBytes_AS_STRING(result), va.buf, va.len);
        memcpy(PyBytes_AS_STRING(result) + va.len, vb.buf, vb.len);
    }

  done:
    if (va.len != -1)
        PyBuffer_Release(&va);
    if (vb.len != -1)
        PyBuffer_Release(&vb);
    return result;
}

bytes_concat就是将两个序列进行拼接,如果其中一个为空,就直接返回另一个。不然的话通过memcpy将两个序列复制到一个新的序列中返回。

缓存池

在python中,针对整数对象中的小整数设计一个小整数缓存池。在运行时,会一次性将所有的小整数都创建好,长期保存在内存中,避免小整数频繁申请空间释放空间造成的性能问题与资源浪费。

同样的,python对于objects对象也设计了缓存池机制,只是做法不同,这里我们从创建objects对象来了解它的缓存池机制是如何实现的。

[Include/bytesobject.h]
PyAPI_FUNC(PyObject *) PyBytes_FromStringAndSize(const char *, Py_ssize_t);
PyAPI_FUNC(PyObject *) PyBytes_FromString(const char *);
PyAPI_FUNC(PyObject *) PyBytes_FromObject(PyObject *);
PyAPI_FUNC(PyObject *) PyBytes_FromFormatV(const char*, va_list)
                                Py_GCC_ATTRIBUTE((format(printf, 1, 0)));
PyAPI_FUNC(PyObject *) PyBytes_FromFormat(const char*, ...)
                                Py_GCC_ATTRIBUTE((format(printf, 1, 2)));

从上面可以看到很多创建bytes对象的方法,这里我们通过PyBytes_FromStringAndSize函数来了解bytes对象的创建以及 python针对bytes做的缓冲池机制。

PyObject *
PyBytes_FromStringAndSize(const char *str, Py_ssize_t size)
{
    PyBytesObject *op;
    if (size < 0) {
        PyErr_SetString(PyExc_SystemError,
            "Negative size passed to PyBytes_FromStringAndSize");
        return NULL;
    }
    if (size == 1 && str != NULL) {
        struct _Py_bytes_state *state = get_bytes_state();
        op = state->characters[*str & UCHAR_MAX];
        if (op != NULL) {
            Py_INCREF(op);
            return (PyObject *)op;
        }
    }
    if (size == 0) {
        return bytes_new_empty();
    }

    op = (PyBytesObject *)_PyBytes_FromSize(size, 0);
    if (op == NULL)
        return NULL;
    if (str == NULL)
        return (PyObject *) op;

    memcpy(op->ob_sval, str, size);
    /* share short strings */
    if (size == 1) {
        struct _Py_bytes_state *state = get_bytes_state();
        Py_INCREF(op);
        state->characters[*str & UCHAR_MAX] = op;
    }
    return (PyObject *) op;
}

简单介绍一下整个函数的处理逻辑。

  1. 判断size是否为负数,作为一个数据正确性校验
  2. 如果数据为单字节对象,判断缓存池中是否已经存在(characters是节点缓存池),如果存在,增加对象引用数,返回。
  3. 如果对象大小为0,返回空对象
  4. 申请内存空间,将数据保存在ob_sval中
  5. 最后,再次判断,如果大小为1,增加引用数,写入缓存池,再返回对象

从上面的逻辑实现,可以了解到python中对于单字节对象的缓存机制与小整数不同。在程序运行之初,不会去主动的添加缓存。只会在创建单字节bytes对象的时候,才会将单字节对象保存在缓存池中。当下次创建单字节对象时,如果缓存池中已经存在,直接返回,避免了重复单字节对象的空间申请与释放。利用空间换时间的方法,来提高python的执行效率。

posted @ 2022-09-02 16:26  红雨520  阅读(108)  评论(0编辑  收藏  举报