Python源码剖析 - Python中的字符串对象
1. 前言
我们已经在 【python中的整数对象】 章节中对定长对象进行了详细的讲解,接下来我们将介绍变长对象,而字符串类型,则是这类对象的典型代表。
这里必须先引入一个概念:
python 中的变长对象分为两类:
- 变长可变对象 - 例如
list
,创建后还能添加、删除元素 - 变长不可变对象 - 例如
string
,tuple
, 创建后,不再支持添加、删除等操作
2. pystringobject初识
pystringobject 是对字符串对象的实现方式。首先它是一个可变长度的对象,这个可变是只指在创建字符串对象的时候,这个长度并不固定。但是一旦创建完毕后,这个长度就固定了,不能再发生变化。
举例来说:
test_str = "hello world" test_url = "https://www.xtuz.net"
显而易见,test_str 的长度和 test_url 的长度并不一样,这个原因就是在字符串对象创建时 pystringobject 并不限定长度,然后创建完毕后,改对象内部维护的字符串对象就不在改变了。
我们从源码中也可以进行佐证:
typedef struct { pyobject_var_head long ob_shash; int ob_sstate; 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 string or -1 if not computed yet. * ob_sstate != 0 iff the string object is in stringobject.c's * 'interned' dictionary; in this case the two references * from 'interned' to this object are *not counted* in ob_refcnt. */ } pystringobject;
pyobject_var_head 中的 ob_size (详见python源码剖析 - 对象初探),记录变长对象的内存大小,ov_sval 作为字符指针指向一段内存,这段内存就是实际字符串。比例 test_str 的 ob_size 则为11。
ob_shash 则是该对象的哈希值,这在 dict 类型中是非常有用的,作为 key 值存在。
ob_sstate 则是表明该对象是否经过 intern 机制处理,简单来说就是即值同样的字符串对象仅仅会保存一份,放在一个字符串储蓄池中,是共用的,当然,肯定不能改变,这也决定了字符串必须是不可变对象。
3. pystringobject创建
从代码上来看,可以有多种创建 pystringobject 的方式:
pyapi_func(pyobject *) pystring_fromstringandsize(const char *, py_ssize_t); pyapi_func(pyobject *) pystring_fromstring(const char *); pyapi_func(pyobject *) pystring_fromformatv(const char*, va_list) py_gcc_attribute((format(printf, 1, 0))); pyapi_func(pyobject *) pystring_fromformat(const char*, ...) py_gcc_attribute((format(printf, 1, 2)));
其中,最常用的则是 pystring_fromstring(const char *);
代码实现如下:
pyobject * pystring_fromstring(const char *str) { register size_t size; register pystringobject *op; assert(str != null); size = strlen(str); if (size > py_ssize_t_max - pystringobject_size) { pyerr_setstring(pyexc_overflowerror, "string is too long for a python string"); return null; } if (size == 0 && (op = nullstring) != null) { #ifdef count_allocs null_strings++; #endif py_incref(op); return (pyobject *)op; } if (size == 1 && (op = characters[*str & uchar_max]) != null) { #ifdef count_allocs one_strings++; #endif py_incref(op); return (pyobject *)op; } /* inline pyobject_newvar */ op = (pystringobject *)pyobject_malloc(pystringobject_size + size); if (op == null) return pyerr_nomemory(); (void)pyobject_init_var(op, &pystring_type, size); op->ob_shash = -1; op->ob_sstate = sstate_not_interned; py_memcpy(op->ob_sval, str, size+1); /* share short strings */ if (size == 0) { pyobject *t = (pyobject *)op; pystring_interninplace(&t); op = (pystringobject *)t; nullstring = op; py_incref(op); } else if (size == 1) { pyobject *t = (pyobject *)op; pystring_interninplace(&t); op = (pystringobject *)t; characters[*str & uchar_max] = op; py_incref(op); } return (pyobject *) op; }
简单来说,主要是三个逻辑:
- 判断字符串是否过长,过长,则返回 null 指针
- 判断是否是空串,空串,则则将引用
- 分配内存,并将字符串复制到 op->ob_sval 中
在完成创建后,内存布局如上所示
4. 字符缓冲池
我们已经在【python中的整数对象】 中阐述了 python 对小整数的优化处理,而字符串的intern机制与此类似,其实就是会为长度为1的的字符创建对象池。
if (size == 1 && (op = characters[*str & uchar_max]) != null) { #ifdef count_allocs one_strings++; #endif py_incref(op); return (pyobject *)op; } /* share short strings */ if (size == 0) { pyobject *t = (pyobject *)op; pystring_interninplace(&t); op = (pystringobject *)t; nullstring = op; py_incref(op); } else if (size == 1) { pyobject *t = (pyobject *)op; pystring_interninplace(&t); op = (pystringobject *)t; characters[*str & uchar_max] = op; py_incref(op); }
每当创建长度为1的字符串的时候,都会把它存到 characters 里面,这样之后创建长度为1的字符时,如果检测到已经在characters里面了,就直接返回这个缓冲的对象,不用进行malloc,这也就是该缓冲池的作用。
5. 字符串对象的intern机制
在 cpython 中字符串的实现原理使用了一种叫做 intern(字符串驻留)的技术来提高字符串效率。
先来看一段代码:
a='www.xtuz.net' b='www.xtuz.net' print(id(a), id(b)) print(a is b)
可以看到如下输出结果
(4420449312, 4420449312) true
a 和 b 虽然值是一样的,但确实是两个不同的字符串对象,假设程序中存在大量值相同的字符串,系统就不得不为每个字符串重复地分配内存空间,显然,对系统来说是一种无谓的资源浪费。为了解决这种问题,python 引入了 intern 机制。
intern 是 python 中的一个内建函数,该函数的作用就是对字符串进行 intern 机制处理,处理后返回字符串对象。我们发现但凡是值相同的字符串经过 intern 机制处理之后,返回的都是同一个字符串对象,这种方式在处理大数据的时候无疑能节省更多的内存空间,系统无需为相同的字符串重复分配内存,对于值相同的字符串共用一个对象即可。
intern 实现 机制的方式非常简单,就是通过维护一个字符串储蓄池,这个池子是一个字典结构,如果字符串已经存在于池子中了就不再去创建新的字符串,直接返回之前创建好的字符串对象,如果之前还没有加入到该池子中,则先构造一个字符串对象,并把这个对象加入到池子中去,方便下一次获取。
6. 更多内容
原文来自兔子先生网站:
查看原文 >>> python源码剖析 - python中的字符串对象
如果你对python语言感兴趣,可以关注我,或者关注我的微信公众号:xtuz666