Python源码剖析-PyStringObject对象和STR(上)

PyStringObject 研究分析

引言

在所有的动态语言(解释器)中,字符串对象是被频繁使用的,在Python 字符串对象中,大家都知道其强大的动态拼接重组的能力,无论是使用 '+' 还是使用 'join',甚至 'x'*int 都能生成字符串对象,当然还支持序列索引切片等,那么Python 底层到底做了那些优化,请看本章关于Str对象的源码层分析?
在本章中,我们将介绍关于PyStringObject 的一些机制和原理:

  1. 不可改变对象机制
  2. intern 机制
  3. 缓冲机制
  4. PyStringObject C对象和C方法
  • 使用dis生成字节码
    [图片上传失败...(image-8be25-1549099814362)]
    和GCC,8086,ARM等汇编代码一样,为了方便二进制字节码向高一级的低级语言(汇编)汇编码转化,如MOV, SUB 指令,Python 也有一百多个“汇编指令”,在Int对象分析中已经介绍了 CALL_FUNCTION 这条指令,它在Python 虚拟机中就是一个 宏定义:
    #define CALL_FUNCTION 131 /* #args + (#kwargs<<8) */
    简单理解为在PYC文件中, 会扫描到 0x83 代表将要执行call指令类似于汇编中的CALL address, 表示调用这段地址,此时EIP->address,CPU寄存器将会执行在这段字节码之后的指令字节,调整栈帧结构,压入RET地址等等操作,详细的栈帧调用过程可以在网上参考,或者做一次缓冲区溢出的小实验加深理解。

继续回到上述的字节码,对于 a = str('Python'),通过对'Python' CALL_FUNCTION 操作,将结果 STORE_NAME 到 a 变量,此时在Python虚拟机中, a 将是一个 PyStringObject, 在int章节中谈到PyIntObject,其实它们都是 PyTypeObject 对象,我们可以在命令行中做如下判断:
assert type(int) == type(str)
不抛出异常。


不可改变对象机制

Python在heap中分配的对象分成两类:可变对象和不可变对象。所谓可变对象是指,对象的内容可变,而不可变对象是指对象内容不可变。

  • 不可变(immutable):int、字符串(string)、float、(数值型number)、元组(tuple),字符串(str)

  • 可变(mutable):字典型(dictionary)、列表型(list)

不可变类型特点看下面的例子:
[图片上传失败...(image-823679-1549099814362)]

  • 优点是,这样可以减少重复的值对内存空间的占用
  • 缺点呢,如例1所示,我要修改这个变量绑定的值,如果内存中没用存在该值的内存块,那么必须重新开辟一块内存,把新地址与变量名绑定。而不是修改变量原来指向的内存块的值,这回给执行效率带来一定的降低。

创建PyStringObject 对象的两种方法

在 int 那一章节已经粗略的介绍过PyTypeObject, 它是一个 C中的结构体,其中封装了 HEAD 信息(包括了引用计数等信息),以及所支持的大量方法,实例的初始化 init函数信息, new初始化一个对象函数信息都包括在其中。
我们先来看看以下两个方法:

  • 方法一:PyString_FromString
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) {
        //判断长度溢出错误
    }
    if (size == 0 && (op = nullstring) != NULL) {
        /*python运行时nullstring专门指向空的字符数组,因为在生成空字符串的时候对其经行了intern操作,使得nullstring指向了这个intern-dict中的对象*/
        //处理 null string
    }
    if (size == 1 && (op = characters[*str & UCHAR_MAX]) != NULL) {
        //处理单字符,判断是否在缓冲池中
    }

    /* 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 = 0; //设置intern flag标志为:未intern
    Py_MEMCPY(op->ob_sval, str, size+1);
    ....
    //创建新的PyStringObject对象并初始化
    if (size == 0) {
        PyObject *t = (PyObject *)op;
        PyString_InternInPlace(&t);
        op = (PyStringObject *)t;
        nullstring = op;    //空字节数组指向nullstring
        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);
    }
    //Intern机制,加入缓冲池操作等。
}

在该C函数中,可以直观的看到上面注视的五个先后步骤:

  1. 判断长度溢出错误
  2. 处理 null string
  3. 处理单字符,判断是否在缓冲池中
  4. 创建新的PyStringObject对象并初始化
  5. Intern机制,加入缓冲池操作

  • 方法二:PyString_FromStringAndSize
    主要来看其创建PyStringObject 和 上述方法有何不同。因为1,2,3和 PyString_FromString是一样的。
 PyObject *
PyString_FromStringAndSize(const char *str, Py_ssize_t size){
    ......
    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;
    if (str != NULL)
        Py_MEMCPY(op->ob_sval, str, size);
    op->ob_sval[size] = '\0';
    /* 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 && str != NULL) {
        PyObject *t = (PyObject *)op;
        PyString_InternInPlace(&t);
        op = (PyStringObject *)t;
        characters[*str & UCHAR_MAX] = op;
        Py_INCREF(op);
    }
    return (PyObject *) op;
}

何其相似,只是这里的一句:
op->ob_sval[size] = '\0';
让我们看到如下的细节:
在C中,字符串数组其实是以 \0 结尾做字符串结束标识的,对于PyString_FromString传入的 char 指针字符数组也必须为 '\0' 结尾,上述的 Py_MEMCPY(op->ob_sval, str, size+1); 中的 size+1 就是为了使数组多一位空间来存储 '\0'的。


缓冲机制

static PyStringObject *characters[0xff + 1];
int(base=16,x='0xff') returns 254
说明characters是一个维护着255个PyStringObject的单字符数组(静态对象)。
在Python初始化完成时, 该数组对象都指向NULL.

无论在PyString_FromString还是PyString_FromStringAndSize中,我们都可以发现两处characters:

  1. 处理单字符,判断是否在缓冲池中。
    if (size == 1 && str != NULL &&
        (op = characters[*str & UCHAR_MAX]) != NULL)
    {
        PyString_InternInPlace(&t)
        Py_INCREF(op);
        return (PyObject *)op;
    }
  1. 当size等于1的时候,先intern再加入缓冲池中。
    } else if (size == 1 && str != NULL) {
        PyObject *t = (PyObject *)op;
        PyString_InternInPlace(&t);
        op = (PyStringObject *)t;
        characters[*str & UCHAR_MAX] = op;
        Py_INCREF(op);
    }

什么是intern机制?

上述介绍了那么多,大量的操作和intern相关,那么intern操作是什么呢? 它有什么作用?
通过观察上述源码可知:当size为0 或者 为1 的时候, 字符都会经过intern(PyString_InternInPlace)操作 。

  • 观察内置intern函数
    [图片上传失败...(image-5f03ad-1549099814362)]
    可以发现经过intern处理过后对象指向同一片内存地址。

在创建字符串的时候, python首先会在intern维护的PyStringObject中进行查找,如果发现存在就会将该对象的引用返回,否则就会创建,分配内存,写入intern,加入缓冲等操作。由于Python中有大量的字符串操作,所有intern机制带来了极大的性能提升。PyString_InternInPlace正是对对象进行intern操作的原生函数。


  • intern源码机制
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 (interned == NULL) {
        interned = PyDict_New();
    }
    t = PyDict_GetItem(interned, (PyObject *)s);
    //检查创建的字符串是否则intern的字典中
    if (t) {
        Py_INCREF(t);
        Py_DECREF(*p);
        *p = t;
        return;
    }
    //将字符串添加到intern的字典中,key-value pair均为字符串本身
    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;
    //调整s中的intern flag标志
    PyString_CHECK_INTERNED(s) = 1;
    //#define PyString_CHECK_INTERNED(op) (((PyStringObject *)(op))->ob_sstate)
}
  • intern机制小结
  • intern机制核心在于 interned这个对象,其由PyDict_New()创建一个PyDictObject(后期章节将会讲到),可以类比为C++中的 map < PyObject *, PyObject * >
  • interned中的指针不能作为字符创对象的有效引用,也就是上述在对引用计数 s->ob_refcnt -= 2 的原因所在。
  • 在销毁一个在interned中的对象时候,会在interned中删除指向该对象的指针。
    [图片上传失败...(image-114d6b-1549099814362)]

总结

PyStringObject对象在创建的过程中会经历各种机制和条件判断最终分配内存等,这些是Python虚拟机在实例化str对象时候自动进行的操作,这里我们需要注意一下 (void)PyObject_INIT_VAR(op, &PyString_Type, size); 通过PyObject_INIT_VAR 将op 和 PyString_Type经行绑定, 这里的 PyString_Type 和我们上一章节分析的 PyInt_Type 是一个东西(PyTypeObject),我们在下一节中不再讨论 intern 和 缓存机制,来看看一个字符串为什么会具有 切割,number对象的属性等


@敬贤。 知识就是力量!


2018-06-21 23:50:17 星期四

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容