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;
}
简单介绍一下整个函数的处理逻辑。
- 判断size是否为负数,作为一个数据正确性校验
- 如果数据为单字节对象,判断缓存池中是否已经存在(characters是节点缓存池),如果存在,增加对象引用数,返回。
- 如果对象大小为0,返回空对象
- 申请内存空间,将数据保存在ob_sval中
- 最后,再次判断,如果大小为1,增加引用数,写入缓存池,再返回对象
从上面的逻辑实现,可以了解到python中对于单字节对象的缓存机制与小整数不同。在程序运行之初,不会去主动的添加缓存。只会在创建单字节bytes对象的时候,才会将单字节对象保存在缓存池中。当下次创建单字节对象时,如果缓存池中已经存在,直接返回,避免了重复单字节对象的空间申请与释放。利用空间换时间的方法,来提高python的执行效率。