• Python之code对象与pyc文件(三)


    上一节:Python之code对象与pyc文件(二)

    向pyc写入字符串

    在了解Python如何将字符串写入到pyc文件的机制之前,我们先来了解一下结构体WFILE:

    marshal.c

    typedef struct {
    	FILE *fp;
    	int error;
    	int depth;
    	/* If fp == NULL, the following are valid: */
    	PyObject *str;
    	char *ptr;
    	char *end;
    	PyObject *strings; /* dict on marshal, list on unmarshal */
    	int version;
    } WFILE;
    

      

    WFILE可以看做是一个对FILE *的简单包装,但在WFILE中,出现了一个奇特的strings域,这个域是Python向pyc文件中写入字符串或从中读取字符串的关键所在,当向pyc中写入时,strings会指向一个PyDictObject对象,而从pyc中读出时,strings则会指向一个PyListObject对象

     我们再次回到PyMarshal_WriteObjectToFile这个函数

    marshal.c

    void PyMarshal_WriteObjectToFile(PyObject *x, FILE *fp, int version)
    {
        WFILE wf;
        wf.fp = fp;
        wf.error = 0;
        wf.depth = 0;
        wf.strings = (version > 0) ? PyDict_New() : NULL;
        wf.version = version;
        w_object(x, &wf);
        Py_XDECREF(wf.strings);
    }
    

      

    可以看到在开始写入对象之前,WFILE的strings就已经指向一个PyDictObject对象了

    marshal.c

    else if (PyString_Check(v)) {
    	if (p->strings && PyString_CHECK_INTERNED(v)) {
    		//<1>获得PyStringObject对象在strings中的序号
    		PyObject *o = PyDict_GetItem(p->strings, v);
    		//<2>intern字符串的非首次写入
    		if (o) {
    			long w = PyInt_AsLong(o);
    			w_byte(TYPE_STRINGREF, p);
    			w_long(w, p);
    			goto exit;
    		}
    		//<3>intern字符串的首次写入
    		else {
    			int ok;
    			o = PyInt_FromSsize_t(PyDict_Size(p->strings));
    			ok = o &&
    				 PyDict_SetItem(p->strings, v, o) >= 0;
    			Py_XDECREF(o);
    			if (!ok) {
    				p->depth--;
    				p->error = 1;
    				return;
    			}
    			w_byte(TYPE_INTERNED, p);
    		}
    	}
    	//<4>写入普通string
    	else {
    		w_byte(TYPE_STRING, p);
    	}
    	n = PyString_GET_SIZE(v);
    	if (n > INT_MAX) {
    		/* huge strings are not supported */
    		p->depth--;
    		p->error = 1;
    		return;
    	}
    	//<5>写入字符串的长度
    	w_long((long)n, p);
    	w_string(PyString_AS_STRING(v), (int)n, p);
    }
    

      

    向pyc写入一个字符串时,可能分为3种情况:

    写入一个普通的字符串,先写入字符串的类型标识TYPE_STRING,然后调用w_long写入字符串长度,最后通过w_string写入字符串本身,这一切都在<4>和<5>这里完成的。除了普通字符串外,Python还会碰到在以后加载pyc文件时需要进行intern操作的字符串。对于这种字符串又分为首次写入和非首次写入。

    这里简略介绍一下intern机制,Python有一个字符串缓冲池,当要生成一个字符串时,Python会检查缓冲池是否已有现成的字符串,如果有则返回,如果没有则将字符串存储在缓冲池,但Python并非对所有的字符串都做缓冲检查(即intern机制)

    我们声明a和b两个变量,并赋予相同的字符串,然后查看它们的地址,它们的地址是相同的,这正是因为intern机制起了作用

    >>> a = "HelloWorld"
    >>> b = "HelloWorld"
    >>> id(a)
    139894353361584
    >>> id(b)
    139894353361584
    

      

    我们再来看另外一个例子:

    >>> c = "Hello World"
    >>> d = "Hello World"
    >>> id(c)
    139894353361536
    >>> id(d)
    139894353361728
    

      

    如上,c和d之于a和b,无非就是多了一个空格,但我们发现,c和d的地址明显不一样,为什么会造成这样的差异呢?原因是因为intern机制默认只对简单字符进行处理,简单字符即"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"构成的字符串

    另外,对于计算得出的字符串,也不做intern

    >>> lst = ["a", "b", "c"]
    >>> e = "".join(lst)
    >>> f = "".join(lst)
    >>> id(e)
    139894353351824
    >>> id(f)
    139894353353104
    

      

    现在,我们知道什么是intern机制,再次回到w_object这个方法中的写入字符串处,如果字符串是可以被intern机制处理的字符串,那么分为首次写入和非首次写入。

    之前说过,在写入的时候,WFILE中的strings指向一个PyDictObject对象,这个对象中,实际上维护着(PyStringObject,PyIntObject)这样的映射关系,key为字符串,而value则是这个字符串是第几个被加入到WFILE.strings中的字符串,更确切的说是第几个被写入到pyc文件中的intern字符串

    Python为什么需要这个PyIntObject对象的值呢?假设我们要向pyc文件写入3个字符串:"hello"、"world"、"hello",如果我们没有intern机制,也没有strings这个PyDictObject对象,我们只管埋头往pyc文件里写字符串,那么pyc文件会长什么样子呢?如下:

    pyc文件
    (TYPE_STRING,5,hello)
    (TYPE_STRING,5,world)
    (TYPE_STRING,5,hello)

    上面的pyc文件存储着3个值,这个值里第一个元素是类型,为字符串类型TYPE_STRING,第二个元素为字符串的长度,第三个为字符串本身的值,我们会发现,第2行和第4行重复了,如果源代码存在大量重复的字符串,那么这样的做法无疑会使得整个pyc文件存在大量冗余的信息。Python作为一门优雅的语言,显然不允许这样的操作存在,那么intern机制和strings指向的PyDictObject就派上用场了

    现在我们有了intern机制和strings指向的PyDictObject对象,我们依旧往pyc文件写入"hello"、"world"、"hello"这3个值,由于这3个字符串是可以启用intern机制,所以这里我们也看一下strings这个PyDictObject对象中的结果:

    string
    hello 0
    world 1

    之前我们说过,strings会存储写入pyc文件的字符串是第几个,所以strings存储的内容如上,那么pyc文件存储的内容又是怎样的呢?

    pyc文件
    (TYPE_STRING,5,hello)
    (TYPE_STRING,5,world)
    (TYPE_STRINGREF,0)

    现在这个pyc文件的内容,较之前的pyc内容有一点不一样了,就是第三行所存储的内容,我们来看一下新pyc文件的第三行内容:(TYPE_STRINGREF,0),这里存储的类型不再是TYPE_STRING,也不再存储字符串的长度和字符串本身,TYPE_STRINGREF这个类型代表在解析pyc文件时,对应的值要去strings中查找,而它的索引值即为0。等一下,好像有点不对?在上面我们的介绍中,strings这个PyDictObject对象里何曾有过0这个索引?不要急,且看我后面慢慢道来

    我们都知道,PyDictObject中存储的是(字符串,第几次写入pyc文件)这样的形式,在加载pyc文件时,同样用到WFILE这个结构体,而且同样需要用到strings这个变量,不过这时候的strings不再是PyDictObject,而是PyListObject。strings这个变量非常有意思,在写入对象时,它是PyDictObject,在加载pyc文件读取对象时,它是PyListObject。之前PyDictObject的value,即为整型值,现在作为PyListObject的索引值,而索引值对应的内容即为字符串,这样,当Python加载pyc文件时,读到一个TYPE_STRINGREF类型的元素和一个索引值,就知道要去strings这个PyListObject查找对应索引值所存储的内容

    现在,我们再来看下pyc文件加载的方法

    marshal.c

    typedef WFILE RFILE;
    
    PyObject *PyMarshal_ReadObjectFromFile(FILE *fp)
    {
    	RFILE rf;
    	PyObject *result;
    	rf.fp = fp;
    	rf.strings = PyList_New(0);
    	rf.depth = 0;
    	rf.ptr = rf.end = NULL;
    	result = r_object(&rf);
    	Py_DECREF(rf.strings);
    	return result;
    }
    

      

    我们看到,在加载pyc文件时,rf依然是WFILE对象,且这时候strings不再是PyDictObject,而是PyListObject,而r_object可以视为上面w_object的逆运算

    遗失的PyCodeObject

    Python之code对象与pyc文件(一)这个章节中,我们说过demo.py会生成3个PyCodeObject:

    demo.py

    class A:
        pass
     
     
    def func():
        pass
     
     
    a = A()
    func()
    

      

    而在PyMarshal_WriteObjectToFile这个方法中,我们看到,这个方法只会对一个PyCodeObject对象进行操作,那么另外两个PyCodeObject对象呢?事实上,另外两个PyCodeObject存在于一个PyCodeObject之中,即demo.py本身是一个大的PyCodeObject,而class A和def func这两个PyCodeObject存在于demo.py对应的PyCodeObject里面。我们可以用co_consts来查看另外两个PyCodeObject: 

    >>> source = open("demo.py").read()
    >>> co = compile(source, "demo.py", "exec")
    >>> co.co_consts
    ('A', <code object A at 0x7f966910ae30, file "demo.py", line 1>, <code object func at 0x7f96690aa930, file "demo.py", line 5>, None, ())
    

      

    果然如我们前面所说,co_consts会返回一个元组,元组中包含的就是class A和def func这两个PyCodeObject,code类型中依旧存在很多我们可以探究的对象,如:co_nlocals(Code Block中局部变量的个数,包括其位置参数的个数)、co_varnames(Code Block中的局部变量名集合),感兴趣的同学可以再去多做研究一下,这里不再一个个展示

      

  • 相关阅读:
    wp8使用mvvm模式简单例子(二)---登陆功能,事件触发
    wp8使用mvvm模式简单例子
    win8.1使用WP8SDK出现Windows Phone Emulator无法启动的问题解决方案
    asp.net原理笔记----页面控件类型,页面状况和asp.net编译过程
    asp.net生命周期
    asp.net服务器数据源控件学习笔记
    AJax学习笔记
    asp.net敏感词过滤
    网上书城总结笔记
    在自己的网站上使用RSS订阅功能
  • 原文地址:https://www.cnblogs.com/beiluowuzheng/p/9381618.html
Copyright © 2020-2023  润新知