在CPython
中,字符串intern
机制是一种字符串对象缓存机制,用于避免创建多个相同内容的字符串对象,以减少内存使用。具体来说,如果两个字符串对象的内容相同,那么这两个字符串对象实际上会共享同一块内存空间。
CPython
使用了一个哈希表(即interned
字典)来实现字符串对象缓存机制。当一个字符串对象被创建时,CPython
会首先检查该字符串对象是否已经被缓存,如果已经被缓存,则直接返回缓存中的字符串对象的引用;否则,将该字符串对象添加到缓存中,并返回该字符串对象的引用。当一个字符串对象不再被使用时,CPython
会从缓存中删除该字符串对象,以释放其占用的内存空间。
在CPython
中,字符串对象的intern
操作可以使用sys.intern
函数或PyUnicode_InternInPlace
函数来实现。其中,sys.intern
函数是Python层面的函数,它接受一个字符串作为参数,并返回该字符串在字符串缓存中的引用。PyUnicode_InternInPlace
函数是C语言层面的函数,它接受一个字符串对象的指针作为参数,并将该字符串对象进行intern
操作。
void
PyUnicode_InternInPlace(PyObject **p)
{
PyObject *s = *p;
#ifdef Py_DEBUG
assert(s != NULL);
assert(_PyUnicode_CHECK(s));
#else
if (s == NULL || !PyUnicode_Check(s)) {
return;
}
#endif
/* If it's a subclass, we don't really know what putting
it in the interned dict might do. */
if (!PyUnicode_CheckExact(s)) {
return;
}
if (PyUnicode_CHECK_INTERNED(s)) {
return;
}
#ifdef INTERNED_STRINGS
if (interned == NULL) {
interned = PyDict_New();
if (interned == NULL) {
PyErr_Clear(); /* Don't leave an exception */
return;
}
}
PyObject *t;
t = PyDict_SetDefault(interned, s, s);
if (t == NULL) {
PyErr_Clear();
return;
}
if (t != s) {
Py_INCREF(t);
Py_SETREF(*p, t);
return;
}
/* The two references in interned are not counted by refcnt.
The deallocator will take care of this */
Py_SET_REFCNT(s, Py_REFCNT(s) - 2);
_PyUnicode_STATE(s).interned = SSTATE_INTERNED_MORTAL;
#endif
}
该函数是Python解释器内部使用的字符串intern
机制的一部分。字符串intern
机制通过维护一个内部的字符串缓存,以便在程序中出现多个相同内容的字符串时可以复用同一块内存空间,从而减少内存占用。
函数PyUnicode_InternInPlace
的作用是将给定的字符串对象原地(in-place)进行intern
操作,即将其添加到字符串缓存中。如果字符串对象已经被添加到字符串缓存中,则不会重复添加。
接下来,我们对函数的关键代码进行逐一解释:
PyObject *s = *p;
该语句从指针p
中获取字符串对象的引用,并将其存储在s
变量中。
#ifdef Py_DEBUG
assert(s != NULL);
assert(_PyUnicode_CHECK(s));
#else
if (s == NULL || !PyUnicode_Check(s)) {
return;
}
#endif
这段代码是在进行一些前置检查,如果字符串对象NULL
或者不是PyUnicodeObject
类型,则直接返回。
if (!PyUnicode_CheckExact(s)) {
return;
}
这段代码是检查字符串对象是否是PyUnicodeObject
类型的精确子类型。如果不是,则直接返回。
if (PyUnicode_CHECK_INTERNED(s)) {
return;
}
该语句检查字符串对象是否已经被添加到字符串缓存中,如果已经被添加,则直接返回。
if (interned == NULL) {
interned = PyDict_New();
if (interned == NULL) {
PyErr_Clear(); /* Don't leave an exception */
return;
}
}
该语句创建了一个名为interned
的全局变量,用于存储字符串缓存。如果interned
变量为NULL
,则创建一个新的空字典对象,并将其赋值给interned
变量。如果创建字典对象失败,则清除异常状态,并返回。
PyObject *t;
t = PyDict_SetDefault(interned, s, s);
if (t == NULL) {
PyErr_Clear();
return;
}
这段代码是将字符串对象s
添加到interned
字典中,并获取interned
字典中与s
相同键值的对象。如果添加成功,则t
变量将指向s
,否则t
变量为NULL
。如果添加失败,则清除异常状态并返回。
if (t != s) {
Py_INCREF(t);
Py_SETREF(*p, t);
return;
}
如果t
变量不等于s
,则说明字典中已经存在与s
相同内容的字符串对象,直接使用t
代替s
。具体做法是将t
的引用计数加1,将指针p
所指的字符串对象指向t
,然后返回。
/* The two references in interned are not counted by refcnt.
The deallocator will take care of this */
Py_SET_REFCNT(s, Py_REFCNT(s) - 2);
_PyUnicode_STATE(s).interned = SSTATE_INTERNED_MORTAL;
这段代码是将字符串对象s
的引用计数减去2,并设置其为interned
状态。由于在interned
字典中,字符串对象s
本身的引用计数没有被计入,因此需要将其引用计数减去2,以保证在字符串对象不再被使用时能够被正确地释放。同时,将字符串对象s
的状态设置为SSTATE_INTERNED_MORTAL
,表示该字符串对象在interned
字典中被使用时是可变的。
下面是一个简单的示例,用于演示如何使用PyUnicode_InternInPlace
函数将字符串对象进行intern
操作:
import sys
# 创建两个字符串对象
a = "hello"
b = "hello"
# 输出字符串对象的引用地址
print("a: ", hex(id(a)))
print("b: ", hex(id(b)))
# 将字符串对象进行 intern 操作
a = sys.intern(a)
b = sys.intern(b)
# 输出字符串对象的引用地址
print("a: ", hex(id(a)))
print("b: ", hex(id(b)))
该示例首先创建了两个相同内容的字符串对象a
和b
,并输出它们的引用地址。然后,通过sys.getrefcount
函数获取字符串对象的引用计数,并将其作为参数传递给PyUnicode_InternInPlace
函数进行intern
操作。最后,输出字符串对象的引用地址,可以发现a
和b
的引用地址相同,说明它们指向了同一块内存空间,即字符串对象被成功地缓存了。
>>> # 创建两个字符串对象
>>> a = "hello"
>>> b = "hello"
>>>
>>> # 输出字符串对象的引用地址
>>> print("a: ", hex(id(a)))
a: 0x1e22d9824b0
>>> print("b: ", hex(id(b)))
b: 0x1e22d9824b0
>>>
>>> # 将字符串对象进行 intern 操作
>>> a = sys.intern(a)
>>> b = sys.intern(b)
>>>
>>> # 输出字符串对象的引用地址
>>> print("a: ", hex(id(a)))
a: 0x1e22d9824b0
>>> print("b: ", hex(id(b)))
b: 0x1e22d9824b0