缓存
一般性的字符串,从而节省字符串处理任务的空间和时间。intern
,并因此而得名 String Interning。Python猫注:String Interning 一般被译为“字符串驻留”或“字符串留用”,在某些语言中可能习惯用 String Pool(字符串常量池)的概念,其实是对同一种机制的不同表述。intern 作为名词时,是“实习生、实习医生”的意思,在此可以理解成“驻留物、驻留值”。
享元设计模式
共享和重用已经定义的对象,从而优化内存占用。is
运算符,检查两个对象是否引用了同一个内存对象。is
运算符将得出True
,否则为False
。>>> 'python' is 'python'
True
PyAPI_FUNC(void) PyUnicode_InternInPlace(PyObject **);
PyUnicode_CHECK_INTERNED
的宏,同样是定义在 unicodeobject.h 中。PyASCIIObject
结构中维护着一个名为interned
的成员变量,它的值表示相应的字符串是否被驻留。#define PyUnicode_CHECK_INTERNED(op) \
(((PyASCIIObject *)(op))->state.interned)
interned
的 Python 字典所存储、访问和管理。 该字典在第一次调用字符串驻留时,被延迟地初始化,并持有全部已驻留字符串对象的引用。PyUnicode_InternInPlace
,它定义在 unicodeobject.c 中,当调用时,它会创建一个准备容纳所有驻留的字符串的字典interned
,然后登记入参中的对象,令其键和值都使用相同的对象引用。void
PyUnicode_InternInPlace(PyObject **p)
{
PyObject *s = *p;
.........
// Lazily build the dictionary to hold interned Strings
if (interned == NULL) {
interned = PyDict_New();
if (interned == NULL) {
PyErr_Clear();
return;
}
}
PyObject *t;
// Make an entry to the interned dictionary for the
// given object
t = PyDict_SetDefault(interned, s, s);
.........
// The two references in interned dict (key and value) are
// not counted by refcnt.
// unicode_dealloc() and _PyUnicode_ClearInterned() take
// care of this.
Py_SET_REFCNT(s, Py_REFCNT(s) - 2);
// Set the state of the string to be INTERNED
_PyUnicode_STATE(s).interned = SSTATE_INTERNED_MORTAL;
}
interned
字典中遍历所有的字符串,调整这些对象的引用计数,并把它们标记为NOT_INTERNED
,使其被垃圾回收。一旦所有的字符串都被标记为NOT_INTERNED
,则interned
字典会被清空并删除。_PyUnicode_ClearInterned
,在 unicodeobject.c 中定义。void
_PyUnicode_ClearInterned(PyThreadState *tstate)
{
.........
// Get all the keys to the interned dictionary
PyObject *keys = PyDict_Keys(interned);
.........
// Interned Unicode strings are not forcibly deallocated;
// rather, we give them their stolen references back
// and then clear and DECREF the interned dict.
for (Py_ssize_t i = 0; i < n; i++) {
PyObject *s = PyList_GET_ITEM(keys, i);
.........
switch (PyUnicode_CHECK_INTERNED(s)) {
case SSTATE_INTERNED_IMMORTAL:
Py_SET_REFCNT(s, Py_REFCNT(s) + 1);
break;
case SSTATE_INTERNED_MORTAL:
// Restore the two references (key and value) ignored
// by PyUnicode_InternInPlace().
Py_SET_REFCNT(s, Py_REFCNT(s) + 2);
break;
case SSTATE_NOT_INTERNED:
/* fall through */
default:
Py_UNREACHABLE();
}
// marking the string to be NOT_INTERNED
_PyUnicode_STATE(s).interned = SSTATE_NOT_INTERNED;
}
// decreasing the reference to the initialized and
// access keys object.
Py_DECREF(keys);
// clearing the dictionary
PyDict_Clear(interned);
// clearing the object interned
Py_CLEAR(interned);
}
PyUnicode_InternInPlace
函数的调用,并查看其附近的代码。下面是在 Python 中关于字符串驻留的一些有趣的发现。PyCode
对象时,解释器将对所有编译期的常量、名称和字面量进行驻留。PyCodeObject *
PyCode_NewWithPosOnlyArgs(int argcount, int posonlyargcount, int kwonlyargcount,
int nlocals, int stacksize, int flags,
PyObject *code, PyObject *consts, PyObject *names,
PyObject *varnames, PyObject *freevars, PyObject *cellvars,
PyObject *filename, PyObject *name, int firstlineno,
PyObject *linetable)
{
........
if (intern_strings(names) < 0) {
return NULL;
}
if (intern_strings(varnames) < 0) {
return NULL;
}
if (intern_strings(freevars) < 0) {
return NULL;
}
if (intern_strings(cellvars) < 0) {
return NULL;
}
if (intern_string_constants(consts, NULL) < 0) {
return NULL;
}
........
}
PyUnicode_InternInPlace
函数被调用处有一条注释,它问道,我们是否真的需要对所有字典中的全部键进行驻留?int
PyDict_SetItemString(PyObject *v, const char *key, PyObject *item)
{
PyObject *kv;
int err;
kv = PyUnicode_FromString(key);
if (kv == NULL)
return -1;
// Invoking String Interning on the key
PyUnicode_InternInPlace(&kv); /* XXX Should we really? */
err = PyDict_SetItem(v, kv, item);
Py_DECREF(kv);
return err;
}
setattr
函数显式地设置,也可以作为类成员的一部分而隐式地设置,或者在其数据类型中预定义。PyObject_SetAttr
的代码片段,该函数定义在文件object.c中,负责为 Python 对象设置新属性。int
PyObject_SetAttr(PyObject *v, PyObject *name, PyObject *value)
{
........
PyUnicode_InternInPlace(&name);
........
}
sys
模块中的intern
函数进行显式地字符串驻留。sys_intern_impl
函数中的字符串驻留过程。static PyObject *
sys_intern_impl(PyObject *module, PyObject *s)
{
........
if (PyUnicode_CheckExact(s)) {
Py_INCREF(s);
PyUnicode_InternInPlace(&s);
return s;
}
........
}
Python猫注:这一条规则值得展开思考,我曾经在上面踩过坑……有两个知识点,我相信 99% 的人都不知道:字符串的 join() 方法是动态创建字符串,因此其创建的字符串不会被驻留;常量折叠机制也发生在编译期,因此有时候容易把它跟字符串驻留搞混淆。推荐阅读《join()方法的神奇用处与Intern机制的软肋》
[a-zA-Z0-9_]*
的常量进行驻留,因为它们非常贴近于 Python 的标识符。Python猫注:关于 Python 中标识符的命名规则,在 Python2 版本只有“字母、数字和下划线”,但在 Python 3.x 版本中,已经支持 Unicode 编码。这部分内容推荐阅读《醒醒!Python已经支持中文变量名啦!》