# bytes

# str&bytes

image

# 内部结构

  • 注意,CPython会为待存储的字节序列额外分配一字节空间用于在末尾处保存\0​,以便兼容C字符串。
typedef struct {
    PyObject_VAR_HEAD 		// 24 字节
    Py_hash_t ob_shash;		// 8 字节
    char ob_sval[1];		// n+1 字节

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

imageimage​​

# 对象行为

PyTypeObject PyBytes_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "bytes",
    PyBytesObject_SIZE,
    sizeof(char),
    // ...
    &bytes_as_number,                           /* tp_as_number */
    &bytes_as_sequence,                         /* tp_as_sequence */
    &bytes_as_mapping,                          /* tp_as_mapping */
    (hashfunc)bytes_hash,                       /* tp_hash */
    // ...
};

# 数值型

static PyNumberMethods bytes_as_number = {
    0,              /*nb_add*/
    0,              /*nb_subtract*/
    0,              /*nb_multiply*/
    bytes_mod,      /*nb_remainder*/	// 根据源码可以看出bytes只是利用%运算实现字符串格式化
					// 例如  b'msg: a=%d b=%d' % (1, 2)
}

# bytes_mod

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

# 序列型

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

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

# bytes_concat

  • 注意:如果是a+b+c​这样的形式,实际上执行的是a+b=r​和 r+c​两步,即a,b​实际上需要被,待合并的bytes对象越多,数据重复拷贝的现象也就越严重。
  • 因此大多类似的情况都会提供一个join方法来高效合并多个序列化对象;join会先遍历待合并对象计算总长度,然后根据总长度来创建目标对象,最后再遍历待合并对象注意拷贝数据,以此解决重复拷贝的陷阱。

image

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对象(也可称为字符对象)的创建效率,Python内部维护了一个字符缓冲池,长度为256。

  • Python内部创建单字节bytes对象时,会先检查对象是否已在缓冲池中,如果在则直接返回。

  • 因此需要注意下列代码的结果

    # 场景一
    >>> a1 = b'a'
    >>> a2 = b'a'
    >>> a1 is a2
    True
    
    # 场景二
    >>> ab1 = b'ab'
    >>> ab2 = b'ab'
    >>> ab1 is ab2
    False
    
    # 场景3 py文件中
    # 此部分原因与代码作用域和虚拟机有关系
    ab1 = b'ab'
    ab2 = b'ab'
    print(ab1 is ab2) # True
    

static PyBytesObject *characters[UCHAR_MAX + 1]; // 256个
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 &&
        (op = characters[*str & UCHAR_MAX]) != NULL) // 检查是否已在缓冲池中
    {
#ifdef COUNT_ALLOCS
        one_strings++;
#endif
        Py_INCREF(op);
        return (PyObject *)op;
    }

    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) {
        characters[*str & UCHAR_MAX] = op;
        Py_INCREF(op);
    }
    return (PyObject *) op;
}

# 缓存hash值

计算hash值是一个比较耗时的工作,而且bytes是一个不可更改对象-其hash值从创建那一刻就已经确定,因此CPython会将其hash值存储在对象内,而不是每次取的时候都重新计算,以空间换时间。

类似会缓存hash值的还有str。

下图中ob_hash字段即位缓存的hash值。

image

估计不少不可变对象都会做这种优化。