[Python源码剖析] Python的字符串对象

如前面提到的,Python的对象可分为可变对象与不可变对象,从另一个角度又可以分为变长对象和定长对象。前一章中的PyIntObject属于不可变对象+定长对象,而本章中的PyStringObject则是不可变对象+变长对象。也就是说,一个Python字符串的长度是非固定的(不同的字符串长度很可能不一样),而当它创建起来之后,就不可以再修改了(因此也可以作为dict的键)。这种不可变性也会使某些字符串操作的效率降低,比如多个字符串的拼接就不得不生成若干的中间对象。

PyStringObject的定义如下

typedef struct {
    PyObject_VAR_HEAD
    long ob_shash;
    int ob_sstate;
    char ob_sval[1];
} PyStringObject;

PyObject_VAR_HEAD宏展开后有一个名为ob_size的变量,它记录了这个字符串的元素个数,而这个字符串的起始位置是由ob_sval指针来指向。这两个指变共同定义了这个字符串,由于有ob_size这个变量,所以可以允许ob_sval到ob_sval+ob_size之间存在'\0',但是这段内存也必须满足ob_sval[ob_size] == '\0'。另外的两个变量,ob_shash缓存了该对象hash值,初始值为-1;ob_sstate标记了该对象是否已经过intern机制的处理,这个后面会提到。

PyStringObject对应的类型对象是PyString_Type,它也是PyTypeObject的一个实例。其中的tp_itemsize记录了元素的单位长度(即所占用的内存大小),这个tp_itemsize和字符串对象中的ob_size共同决定了这个字符串所占用的内存大小。

字符串的创建有两个方式,PyString_FromString以及PyString_FromStringAndSize,前者要求传入一个以'\0'结尾的字符串,后者则还要传入一个字符串的长度,因此可以允许字符串中包括'\0'。两者差不多,主要看PyString_FromString。

PyObject* PyString_FromString(const char* str)
{
    register size_t size;
    register PyStringObject* op;
    size = strlen(str);
    if(size > PY_SSIZE_T_MAX) {
        return NULL;
    }
    if(size == 0 && (op = nullstring) != NULL) {
        return (PyObject*) op;
    }
    if(size == 1 && (op = characters[*str & UCHAR_MAX]) != NULL) {
        return (PyObject*) op;
    }
    op = (PyStringObject*) PyObject_MALLOC(sizeof(PyStringObject) + size);
    PyObject_INIT_VAR(op, &PyString_Type, size);
    op->ob_shash = -1;
    op->sstate = SSTATE_NOT_INTERNED;
    memcpy(op->ob_sval, str, size+1);
    ...
    return (PyObject*) op;
}

首先检查字符串长度,太大的话拒绝创建;nullstring是一个特殊的字符串对象,表示空串。第一次创建空字符串对象时,nullstring为NULL,所以会走正常流程创建一个PyStringObject,并通过intern机制共享出来,未来再有要求创建长度为0的字符串时就直接返回它了。最后面的一段是真正的创建字符串对象,为PyStringObject及其内部的字符串数组申请内存并初始化,包括对ob_shash和sstate的初始化。

实际上,在return之前,函数内部还做一个intern的动作:

PyObject *t = (PyObject*)op;
PyString_InternInPlace(&t);
op = (PyStringObject*)t;

字符串在被intern之后,整个程序运行期间就只有唯一一个PyStringObject与这个字符串对应,不节省了空间,又简化了对PyStringObject的比较。下面就是这个intern具体的工作:

void PyString_InternInPlace(PyObject** p)
{
    register PyStringObject *s = (PyStringObject*)(*p);
    PyObject* t;
    if(!PyString_CheckExact(s)) {
        return;
    }
    if(PyString_CHECK_INTERNED(s)) {
        return;
    }
    if(interned == NULL) {
        interned = PyDict_New();
    }
    t = PyDict_GetItem(interned, (PyObject*)s);
    if(t) {
        Py_INCREF(t);
        Py_DECREF(*p);
        *p = t;
        return;
    }
    PyDict_SetItem(interned, (PyObject*)s, (PyObject*)s);
    s->ob_refcnt -= 2
    PyString_CHECK_INTERNED(s) = SSTATE_INTERNED_MORTAL;
}

首先要检查传入的对象必须是一个严格的PyStringObject对象,甚至它的派生类都不会做intern。然后,检查这个对象是否已经被intern过了,如果是的话,直接返回,啥都不会干了。再后面就看到,这个intern机制实际就是维护了一个Python dict,它的key和val都是PyObject*类型的。

通过对引用计数的调整也可以看出,这个dict内对字符串对象的引用是不算在引用计数里面的,否则这个对象一旦被intern了,就永远无法释放了。而当一个字符串对象的引用计数降为0时,在string_dealloc销毁这个对象本身的同时,也是清除掉其在interned结构中的指针。

当有一个相同的字符串创建请求时,无论如何会创建出对应的PyStringObject,因为interned这个dict本身就需要以PyStringObject*作为key的。但是如果在intern检查已存在这个对应的PyStringObject,那么会返回intern中的对象,而刚刚创建的那个临时字符串对象就会被瞬间销毁了。

还有一点,被intern的字符串有两种状态:SSTATE_INTERNED_MORTAL, SSTATE_INTERNED_IMMORTAL,区别在于string_dealloc后者是永远不会被销毁的。PyString_InternInPlace只能创建前者,而后则需要调另外的函数接口PyString_InternImmortal,而它实际就是在调用了PyString_InternInPlace之后强制把状态改成了SSTATE_INTERNED_IMMORTAL。

前面在介绍PyString_FromString创建字符串对象时,漏掉了一个地方,就是当size == 1时的情况,此时会用上一个characters的静态数组

static PyStringObject* charaters[UCHAR_MAX+1];

创建之前会先检查要创建的这个字符串对象是否已经存在于characters这个数组中了,如果是的话就直接返回了,不需要再往下执行了。而这个数组是如何创建的呢?实际也在这个PyString_FromString之中,如下

PyStringObject *t = (PyObject*)op;
PyString_InternInPlace(&t);
op = (PyStringObject*)t;
characters[*str&UCHAR_MAX] = op;
Py_INCREF(op);

也就是说,单个字符的PyStringObject在创建后,像其它字符串一样被intern,接下来又被放入了characters这个数组中。就像PyIntObject的小整数对象一样,既在small_ints数据中,也在block_list管理的结构之中。

最后是关于效率的一点提示,由于PyStringObject是不可变对象,因此当有N个字符串连接时,就必须N-1次的内存申请及内存搬运工作,这必然会影响执行效率。官方推荐的方法是先把这些字符串放到一个list或者tuple中,然后使用join操作,这样就只分配一次内存即可。

 

Posted by Chilly_Rain 2013年8月01日 21:54


[Python源码剖析] Python的整数对象

Python的对象由前面提到的知识可以分为定长对象和变长对象,同时从另一个角度上,Python的对象也可以分为可变对象和不可变对象。本章中的PyIntObject就是一个定长对象+不可变对象。

由于Python的程序中一般会对整数对象的使用极重,如果频繁地创造和销毁是极其影响效率的,因此Python的设计者给出了“整数对象池”这个概念,作为整数对象的缓冲池机制。言归正传,下面是PyIntObject的定义

[intobject.h]
typedef struct {
    PyObject_HEAD
    long ob_ival;
} PyIntObject;

而正如第一章所述,PyObject_HEAD中实际是一个类型对象指针和一个引用计数。PyIntObject对应的类型对象就是PyInt_Type,其就是PyTypeObject的一个实例。

这 个类型对象内容太多,我就不copy了,主要的内容包括名称,大小等基本属性以及一些函数集指针。函数集指针又包括了int_dealloc, int_free, int_repr,int_compare, int_as_number等。特别是最后一个,它定义了作为数值对象的所有可选操作,像int_add, int_sub等。

在Python的实现中,对于一些执行频繁的代码,都同时提供了函数和宏两个版本,比如PyInt_AS_LONG(宏), PyInt_AsLong(函数),调用者需要在安全和性能上做出选择。

创建一个PyIntObject对象可有三种途径,对应三种不同的输入参数类型,如下,但是实际上这里应用了Adpator Pattern的思想,后两个实际最终都只是通过在接口上做了转换后调用了PyInt_FromLong。

PyObject* PyInt_FromLong(long ival)
PyObject* PyInt_FromString(char* s, char** pend, int base)
PyObject* PyInt_FromUnicode(Py_UNICODE* s, int length, int base)

程序在运行期间,Python的整数对象并不是孤立存在地,而是形成了一个整数对象系统。先来看小整数对象。

由 于Python对象在运行期间都在存活在系统堆上的,因此如果没有优化机制,而是对这些频繁使用的小整数对象进行malloc和free,那么不仅降低运 行效率,也会造成内存碎片。因此小整数对象使用了对象池技术,同时由于整数对象是不可变对象,所以对象池中的对象都是被任意共享的。

那么多小的整数才算是小整数呢?默认范围是[-5, 257),当然也可以改,但是就得自己重新编译一遍源码了。

#define NSMALLPOSINTS 257
#define NSMALLNEGINTS 5
#if NSMALLPOSINTS + NSMALLNEGINTS > 0
    static PyIntObject* small_ints[NSMALLPOSINTS + NSMALLNEGINTS];
#endif

对于大整数对象了,Python运行环境提供一块内存,这些内存由大整数轮流使用,从而避免了不断malloc的工作。而 这块内存也是有自己的结构的,它是由一个称为block_list的指针维护的一个单向链表,其中每一个结点称为一个PyIntBlock,而每个 block中又维护着N_INTOBJECTS个PyIntObject对象,即objects数组。

struct _intblock {
    struct _intblock *next;
    PyIntObject objects[N_INOBJECTS];
};
typedef struct _intblock PyIntBlock;

static PyIntBlock* block_list = NULL;
static PyIntObject* free_list = NULL;

在Python运行的某个时刻,通常有一些block中的某些object正在被使用,而其它的则处于空闲状态,为了把这些空闲object组织起来,就有了free_list,它作为链表的表头,把所有空闲内存串成另一个单链表。

PyIntObject的创建过程中,上面的结构是如何起作用的呢?

首先,程序会检查是否有小整数对象池,如果有的话再检查要创建的这个PyIntObject是不是在小整数范围内,如果是的话,直接返回;如果上面的条件无法满足,那么就会求助于block_list和free_list,寻找一块可用的PyIntObject对象的内存。

if(free_list == NULL) {
    if((free_list = fill_free_list()) == NULL)
        return NULL;
}

v = free_list;
free_list = (PyIntObject*)v->ob_type;
PyObject_INIT(v, &PyInt_Type);
v->ob_ival = ival;
return (PyObject*) v;

当第一次调用PyInt_FromLong时,free_list是空(另外,当没有空闲block时它也是空),那么就会调用fill_free_list函数来创建新的内存。

// fill_free_list
PyIntObject *p, *q;
p = (PyIntObject*) PyMem_MALLOC(sizeof(PyIntBlock));
((PyIntBlock*)p)->next = block_list;
block_list = (PyIntBlock*) p;

p = &((PyIntBlock*)p)->objects[0];
q = p + N_INTOBJECTS;
while(--q > p)
    q->ob_type = (struct _typeobject*)(q-1);
q->ob_type = NULL;
return p + N_INTOBJECTS -1;

实际工作就是加一块block,放在block_list的头部,所以block_list总是会指向最新创建的PyIntBlock对象;然后对于其内部的所有PyIntObject,利用ob_type从后到前串起来, 最后返回最后一个object的位置作为free_list的赋值。

此时free_list只管理了一个block下的所有空闲 objects,block之间的objects如何关联呢?这发生在一个PyIntObject的引用计数减少到0的时候:在int_dealloc内,它会将自己的ob_type指针原free_list,再将free_list指向这个刚刚销毁的object。从这里也能看出,如果一块内存被申请 用作PyIntObject,那么它就永远不会归还给OS,而是一直保留着,因此PyIntObject占用的内存大小与同时共存的整数个数最大值有关。

如果要删除的对象是一个整数的派生类对象,那么int_dealloc也不会做任何动作,只是调用其类型对象中的tp_free。

最后要看的是小整数对象的创建时机:它们是在python初始化的时候,调用_PyInt_Init时被调用的。small_ints中保存的object 也是被block_list和free_list来管理的,只不过它们是永生不灭的,因此small_ints中总会贡献一次引用计数。

Posted by Chilly_Rain 2013年7月30日 21:54


[Python源码剖析] Python对象初探

在Python的世界里,一切都是对象。面向对象概念中的实例是对象,类也是对象。特别地,像Python中的int,string,dict等则称为内建类型对象。当然,我们也可以自定义对象。

Python中的对象在C级别就是一块内存而已,而且通常不能被静态初始化,也不能在栈上生存,唯一的例外就是类型对象。而当一个对象被创建之后,它在内存中的大小就不变了,所以像list等可变长的对象都需要在其内部包含一个指针变量。

所有对象的基石是PyObject结构体,而其中主要就是宏PyObject_HEAD,里面主要是两个字段

int ob_refcnt;         // 引用记数
struct _typeobject *ob_type;  // 类型对象的指针

实际上,所有的对象的内存排布上都必须以PyObject_HEAD开头,然后才是具体类型相关的变量(或者说是扩展),比如PyIntObject的扩展变量是long ob_ival。这样的目的就是用一个PyObject*指针就可以引用任意一个对象。

对应于变长对象,还有一个PyVarObject结构体,其主要的内容是宏PyObject_VAR_HEAD开头,其相对于PyObject_HEAD就是多出一个变量int ob_size,即变长对象中的item数量。

后面来说类型对象,正如前面所述,它就是面向对象概念中的“类”。类型对象以PyTypeObject结构体来定义,它以PyObject_VAR_HEAD开头,后面包括类型的名称,与分配空间大小有关的变量,该类型可以支持的各种函数集合的指针等。具体的一个类型,比如PyInt_Type就是PyTypeObject结构体的一个实例而已。

对象的创建有两种途径:范型API,称为AOL;与类型相关的API,称为COL。不多说了。

PyIntObject被创建的过程:实际上Python中的int对应于PyInt_Type,而其父类object的类型对象则是PyBaseObject_Type。首先,PyInt_Type中的tp_new会被调用,如果为空的话,会向上找其父类,即PyBaseObject_Type的tp_new来申请内存,申请过程会用到PyInt_Type中的tp_basicsize等信息。申请内存完成后,继而会调用PyInt_Type中的tp_init来做初始化。对应到C++中,tp_new就是new操作符,而tp_init则可视为构造函数。

对象被创建后,其行为其实大多是由其类型对象中所定义的各种函数集决定的,例如,在PyTypeObject中有tp_as_number, tp_as_sequence, tp_as_mapping三个函数族,分别对应于对象被用作数字,序列和映射时该如何表现。例如,重写一个__getitem__函数实际就相当于指定了PyTypeObject中tp_as_mapping.mp_subscript操作。

一个有趣的现象是PyTypeObject也是以PyObject_VAR_HEAD开头的,所以它也需要指定一个类型对象作为它的对象,亦即“类型的类型”,这就是PyType_Type,它也被称为metaclass。

Python对象的多态性是通过其类型对象指针ob_type动态决定的,例如object->ob_type->tp_print(object)。

PyObject中的都会一个引用计数,即ob_refcnt变量。C源码中主要是通过Py_INCREF和Py_DECREF两个宏来增加和减少一个对象的引用计数的。当引用计数变为0时,Py_DECRF会调用该对象的析构函数来尝试释放其占有的内存(tp_dealloc)。当然,类型对象是超越引用计数规则的,它永远不会被析构。而即使一个对象调用了析构函数,也不一定会释放内存,因为Python大量使用了内存对象池技术,避免反复对OS申请内存而造成性能问题。

Posted by Chilly_Rain 2013年7月24日 20:58