2025年6月4日 星期三 乙巳(蛇)年 三月初八 设为首页 加入收藏
rss
您当前的位置:首页 > 计算机 > 编程开发 > Python

Python 源码剖析 - 字符串对象 PyStringObject(2)

时间:12-14来源:作者:点击数:7

3. Intern 机制

无论是PyString_FromString还是PyString_FromStringAndSize,我们都注意到,当字符数组的长度为0或1时,需要进行了一个特别的动作:PyString_InternInPlace。这就是前面所提到的Intern机制。

PyStringObject对象的Intern机制其目的是对于被Intern之后的字符串,在整个Python运行时,系统中都只有唯一的与该字符串对应的PyStringObject对象。这样当判断两个PyStringObject对象是否相同时,如果它们都被Intern了,那么只需要简单地检查它们对应的PyObject*是否相同即可。这个机制既节省了空间,又简化了对PyStringObject对象的比较,嗯,可谓是一箭双雕哇。

假如在某个时刻,我们创建了一个PyStringObject对象A,其表示的字符串是“Python”,在之后的某一时刻,加入我们想为“Python”再次建立一个PyStringObject对象,通常情况下,Python会为我们重新申请内存,创建一个新的PyStringObject对象B,A与B是完全不同的两个对象,尽管其内部维护的字符数组是完全相同的。

这就带来了一个问题,加入我们在程序中创建了100个“Python”的PyStringObject对象呢?显而易见,这样会大量地浪费珍贵的内存。因此Python对PyStringObject对象引入了Intern机制。在上面的例子中,如果对于A应用了Intern机制,那么之后要创建B的时候,Python会首先在系统中记录的已经被Intern的PyStringObject对象中查找,如果发现该字符数组对应的PyStringObject对象已经存在了,那么就将该对象的引用返回,而不会创建对象B。PyString_InternInPlace正是完成对一个对象的Intern操作:

  • [stringobjec.c]
  • void PyString_InternInPlace(PyObject **p)
  • {
  • register PyStringObject *s = (PyStringObject *)(*p);
  • PyObject *t;
  • if (s == NULL || !PyString_Check(s))
  • Py_FatalError("PyString_InternInPlace: strings only please!");
  • /* If it's a string subclass, we don't really know what putting
  • it in the interned dict might do. */
  • if (!PyString_CheckExact(s))
  • return;
  • if (PyString_CHECK_INTERNED(s))
  • return;
  • if (interned == NULL) {
  • interned = PyDict_New();
  • if (interned == NULL) {
  • PyErr_Clear(); /* Don't leave an exception */
  • return;
  • }
  • }
  • t = PyDict_GetItem(interned, (PyObject *)s);
  • if (t) {
  • Py_INCREF(t);
  • Py_DECREF(*p);
  • *p = t;
  • return;
  • }
  • if (PyDict_SetItem(interned, (PyObject *)s, (PyObject *)s) < 0) {
  • PyErr_Clear();
  • return;
  • }
  • /* The two references in interned are not counted by refcnt.
  • The string deallocator will take care of this */
  • s->ob_refcnt -= 2;
  • PyString_CHECK_INTERNED(s) = SSTATE_INTERNED_MORTAL;
  • }

首先会进行一系列的检查。首先,会检查传入的对象是否是一个PyStringObject对象,Intern机制只能应用在PyStringObject对象上,甚至对于它的派生类对象系统都不会应用Intern机制。然后,会检查传入的PyStringObject对象是否已经被Intern机制处理过了,Python不会对同一个PyStringObject对象进行一次以上的Intern操作。

从代码中我们可以清楚地看到,Intern机制的核心在于interned这个东西,那么这个东西是个什么东西呢?

  • static PyObject *interned;

从stringobject.c中的定义我们完全不知道interned是个什么东西,然而在这里我们看到,interned实际上指向的是PyDict_New创建的一个对象。而PyDict_New实际上创建了一个PyDictObject对象,这个对象我们将在后面详细地剖析。其实,现在,一个PyDictObject对象完全可以看作是C++中的map,即map<PyObject*, PyObject*>。

现在一切都清楚了,所谓的Intern机制,实际上就是系统中有一个(Key, Value)的映射的集合interned。在这个集合中,记录着被应用了Intern机制的PyStringObject对象。当对一个PyStringObject对象A应用Intern机制时,首先会在Interned中检查是否有满足一下条件的对象B:B中维护的原生字符串与A相同。如果确实存在对象B,那么指向A的PyObject指针将会指向B,而A的引用计数减1,这样,其实A只是一个临时被创建的对象。如果interned中还不存在这样的B,那么就将A记录到interned中。

图2展示了如果Interned中存在这样的对象B,在对A进行Intern操作时, 原本指向A的PyObject指针的变化:

对于被Intern的PyStringObject对象,Python采用了特殊的引用计数机制。在将一个PyStringObject对象A的PyObject指针作为Key和Value添加到interned中时,PyDictObject对象会通过这两个指针对A的引用计数进行两次加1操作。但是Python的设计者规定在interned中A的指针不能被视为对象A的有效引用,因为如果是有效引用的话,那么A的引用计数在Python运行时结束之前永远都不可能为0,因为至少有interned中的两个指针引用了A,那么删除A就永远不可能,这显然是没有道理的。

因此interned中的指针不能作为A的有效引用。这也就是在PyString_InternInPlace最后会将引用计数减2的原因。当A的引用计数在某个时刻减为0之后,系统将会销毁对象A,那么我们可以预期,在销毁A的同时,会在interned中删除指向A的指针,显然,这一点在string_dealloc得到了验证:

  • [stringobject.c]
  • static void string_dealloc(PyObject *op)
  • {
  • switch (PyString_CHECK_INTERNED(op)) {
  • case SSTATE_NOT_INTERNED:
  • break;
  • case SSTATE_INTERNED_MORTAL:
  • /* revive dead object temporarily for DelItem */
  • op->ob_refcnt = 3;
  • if (PyDict_DelItem(interned, op) != 0)
  • Py_FatalError(
  • "deletion of interned string failed");
  • break;
  • case SSTATE_INTERNED_IMMORTAL:
  • Py_FatalError("Immortal interned string died.");
  • default:
  • Py_FatalError("Inconsistent interned string state.");
  • }
  • op->ob_type->tp_free(op);
  • }

前面提到,Python在创建一个字符串时,会首先在interned中检查是否已经有该字符串对应得PyStringObject对象了,如果有,则不用创建新的,这样可以节省内存空间。事到如今,我必须要承认,我说谎了,节省内存空间是没错的,可是Python并不是在创建PyStringObject时就通过interned实现了节省空间的目的。事实上,从 PyString_FromString 中可以看到,无论如何,一个合法的PyString_FromString对象是会被创建的,同样,我们可以注意到,PyString_InternInPlace也只对PyStringObject起作用。

事实正是如此,Python始终会为字符串S创建PyStringObject对象,尽管S在interned中已经有一个与之对应的PyStringObject对象了。而Intern机制是在S被创建后才起作用的,通常Python在运行时创建了一个PyStringObject对象Temp后,基本上都会调用PyString_InternInPlace,Intern机制会减少Temp的引用计数,Temp对象会由于引用计数减为0 而被销毁,它只是作为一个临时对象昙花一现地在内存中闪现,然后湮灭。

那么我们现在有一个疑问了,是否可以直接在C的原生字符串上做Intern的动作,而不需要再创建这样一个临时对象呢?事实上,Python确实提供了一个以char*为参数的Intern机制相关函数,但是你会相当失望,嗯,因为它基本上是换汤不换药的:

  • [stringobject.c]
  • PyObject* PyString_InternFromString(const char *cp)
  • {
  • PyObject *s = PyString_FromString(cp);
  • if (s == NULL)
  • return NULL;
  • PyString_InternInPlace(&s);
  • return s;
  • }

临时对象照样被创建出来,实际上,仔细一想,就会发现在Python中,必须创建这样一个临时的PyStringObject对象来完成Intern操作。为什么呢?答案就在PyDictObject对象interned中,因为PyDictObject必须以PyObject指针作为键。

关于PyStringObject对象的Intern机制,还有一点需要注意。实际上,被Intern的PyStringObject对象分为两类,一类是SSTATE_INTERNED_IMMORTAL状态的,而另一类是SSTATE_INTERNED_MORTAL状态的,这两种状态的区别在string_dealloc中可以清晰地看到,显然,SSTATE_INTERNED_IMMORTAL状态的PyStringObject对象是永远不会被销毁的,它将与Python run time同年同月同日死。

PyString_InternInPlace只能创建SSTATE_INTERNED_MORTAL状态的PyStringObject对象,如果想创建SSTATE_INTERNED_IMMORTAL状态的对象,必须要通过另外地接口,在调用了PyString_InternInPlace后,强制改变PyStringObject的intern状态。

  • [stringobject.c]
  • void PyString_InternImmortal(PyObject **p)
  • {
  • PyString_InternInPlace(p);
  • if (PyString_CHECK_INTERNED(*p) != SSTATE_INTERNED_IMMORTAL) {
  • PyString_CHECK_INTERNED(*p) = SSTATE_INTERNED_IMMORTAL;
  • Py_INCREF(*p);
  • }
  • }

4. 字符缓冲池

最后需要注意的一点是与PyIntObject中的小整数对象的对象池一样,Python的设计者为PyStringObject中的一个字节的字符对象也设计了这样一个对象池characters。

  • static PyStringObject *characters[UCHAR_MAX + 1];

其中的UCHAR_MAX是在系统头文件中定义的常量,这也是一个平台相关的常量,在Win32平台下:

  • #define UCHAR_MAX 0xff /* maximum unsigned char value */

在Python的整数对象体系中,小整数的缓冲池是在Python runtime初始化时被创建的,而字符串对象体系中的字符缓冲池则是以静态变量的形式存在着的。在Python runtime初始化完成之后,缓冲池中的所有PyStringObject指针都为空。

在创建一个PyStringObject时,无论是调用PyString_FromString还是PyString_FromStringAndSize,在创建的字符串实际上是一个字符时,会进行如下的操作:

  • [stringobject.c]
  • PyObject* PyString_FromStringAndSize(const char *str, int size)
  • {
  • 。。。。。。
  • else if (size == 1 && str != NULL)
  • {
  • PyObject *t = (PyObject *)op;
  • PyString_InternInPlace(&t);
  • op = (PyStringObject *)t;
  • characters[*str & UCHAR_MAX] = op;
  • Py_INCREF(op);
  • }
  • return (PyObject *) op;
  • }

先对所创建的字符串(字符)对象进行Intern操作,再将Intern的结果缓存到字符缓冲池characters中。图3演示了缓存一个字符对象的过程。

3条带有标号的曲线既代表指针,又代表进行操作的顺序:

1) 创建PyStringObject对象”P”

2) 对对象”P”进行Intern操作

3) 将对象”P”缓存至字符缓冲池中

同样,在创建PyStringObject时,会首先检查所要创建的是否是一个字符对象,然后检查字符缓冲池中是否已经有了这个字符的字符对象的缓冲,如果有,则直接返回这个缓冲的对象即可:

  • [stringobject.c]
  • PyObject* PyString_FromStringAndSize(const char *str, int size)
  • {
  • register PyStringObject *op;
  • ……
  • if (size == 1 && str != NULL &&
  • (op = characters[*str & UCHAR_MAX]) != NULL)
  • {
  • #ifdef COUNT_ALLOCS
  • one_strings++;
  • #endif
  • Py_INCREF(op);
  • return (PyObject *)op;
  • }
  • ……
  • }
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门
本栏推荐