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

向pyc写入字符串

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

marshal.c

  1. typedef struct {
  2. FILE *fp;
  3. int error;
  4. int depth;
  5. /* If fp == NULL, the following are valid: */
  6. PyObject *str;
  7. char *ptr;
  8. char *end;
  9. PyObject *strings; /* dict on marshal, list on unmarshal */
  10. int version;
  11. } WFILE;

  

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

我们再次回到PyMarshal_WriteObjectToFile这个函数

marshal.c

  1. void PyMarshal_WriteObjectToFile(PyObject *x, FILE *fp, int version)
  2. {
  3. WFILE wf;
  4. wf.fp = fp;
  5. wf.error = 0;
  6. wf.depth = 0;
  7. wf.strings = (version > 0) ? PyDict_New() : NULL;
  8. wf.version = version;
  9. w_object(x, &wf);
  10. Py_XDECREF(wf.strings);
  11. }

  

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

marshal.c

  1. else if (PyString_Check(v)) {
  2. if (p->strings && PyString_CHECK_INTERNED(v)) {
  3. //<1>获得PyStringObject对象在strings中的序号
  4. PyObject *o = PyDict_GetItem(p->strings, v);
  5. //<2>intern字符串的非首次写入
  6. if (o) {
  7. long w = PyInt_AsLong(o);
  8. w_byte(TYPE_STRINGREF, p);
  9. w_long(w, p);
  10. goto exit;
  11. }
  12. //<3>intern字符串的首次写入
  13. else {
  14. int ok;
  15. o = PyInt_FromSsize_t(PyDict_Size(p->strings));
  16. ok = o &&
  17. PyDict_SetItem(p->strings, v, o) >= 0;
  18. Py_XDECREF(o);
  19. if (!ok) {
  20. p->depth--;
  21. p->error = 1;
  22. return;
  23. }
  24. w_byte(TYPE_INTERNED, p);
  25. }
  26. }
  27. //<4>写入普通string
  28. else {
  29. w_byte(TYPE_STRING, p);
  30. }
  31. n = PyString_GET_SIZE(v);
  32. if (n > INT_MAX) {
  33. /* huge strings are not supported */
  34. p->depth--;
  35. p->error = 1;
  36. return;
  37. }
  38. //<5>写入字符串的长度
  39. w_long((long)n, p);
  40. w_string(PyString_AS_STRING(v), (int)n, p);
  41. }

  

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

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

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

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

  1. >>> a = "HelloWorld"
  2. >>> b = "HelloWorld"
  3. >>> id(a)
  4. 139894353361584
  5. >>> id(b)
  6. 139894353361584

  

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

  1. >>> c = "Hello World"
  2. >>> d = "Hello World"
  3. >>> id(c)
  4. 139894353361536
  5. >>> id(d)
  6. 139894353361728

  

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

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

  1. >>> lst = ["a", "b", "c"]
  2. >>> e = "".join(lst)
  3. >>> f = "".join(lst)
  4. >>> id(e)
  5. 139894353351824
  6. >>> id(f)
  7. 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

  1. typedef WFILE RFILE;
  2.  
  3. PyObject *PyMarshal_ReadObjectFromFile(FILE *fp)
  4. {
  5. RFILE rf;
  6. PyObject *result;
  7. rf.fp = fp;
  8. rf.strings = PyList_New(0);
  9. rf.depth = 0;
  10. rf.ptr = rf.end = NULL;
  11. result = r_object(&rf);
  12. Py_DECREF(rf.strings);
  13. return result;
  14. }

  

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

遗失的PyCodeObject

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

demo.py

  1. class A:
  2. pass
  3.  
  4. def func():
  5. pass
  6.  
  7. a = A()
  8. func()

  

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

  1. >>> source = open("demo.py").read()
  2. >>> co = compile(source, "demo.py", "exec")
  3. >>> co.co_consts
  4. ('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中的局部变量名集合),感兴趣的同学可以再去多做研究一下,这里不再一个个展示

  

Python之code对象与pyc文件(三)的更多相关文章

  1. Python之code对象与pyc文件(二)

    上一节:Python之code对象与pyc文件(一) 创建pyc文件的具体过程 前面我们提到,Python在通过import或from xxx import xxx时会对module进行动态加载,如果 ...

  2. Python之code对象与pyc文件(一)

    Python程序的执行过程 我们都知道,C语言在执行之前需要将源代码编译成可执行的二进制文件,也就是将源代码翻译成机器代码,这种二进制文件一旦生成,即可用于执行.但是,Python是否一样呢?或许很多 ...

  3. 《python解释器源码剖析》第8章--python的字节码与pyc文件

    8.0 序 我们日常会写各种各样的python脚本,在运行的时候只需要输入python xxx.py程序就执行了.那么问题就来了,一个py文件是如何被python变成一系列的机器指令并执行的呢? 8. ...

  4. Python逆向(二)—— pyc文件结构分析

    一.前言 上一节我们知道了pyc文件是python在编译过程中出现的主要中间过程文件.pyc文件是二进制的,可以由python虚拟机直接执行的程序.分析pyc文件的文件结构对于实现python编译与反 ...

  5. 关于python包,模块,.pyc文件和文件导入理解

    参考文献 一.包 包是一个文件夹,用来存放模块和子包. 包里一般会有一个__init__.py的文件(也可以没有). 包里会有一个__pycache__文件夹,存放.py文件经解释器解释后的中间字节码 ...

  6. Python编程时.py与.pyc文件的介绍

    Python的程序中,是把原始程序代码放在.py文件里,而Python会在执行.py文件的时候.将.py形式的程序编译成中间式文件(byte-compiled)的.pyc文件,这么做的目的就是为了加快 ...

  7. python运行时禁止生成pyc文件

    方法 在环境变量文件~/.bashrc中添加 export PYTHONDONTWRITEBYTECODE=False source ~/.bashrc加载即可 如何从项目中删除所有.pyc文件 fi ...

  8. .pyc文件的结构体PyCodeObject

    python执行程序时生成的pyc文件里面是,PyCodeObject 的结构体构成,每个命名空间(函数名.import模块等)都会形成一个core block,一个python程序的所有命名空间生成 ...

  9. 删除项目开发中的.pyc文件

    在实际开发中python会自动生成很多pyc文件,但是这些pyc文件是不需要我们追踪的,删除了对项目也没有影响,下面是删除pyc文件的方法. Linux或Mac系统 find /tmp -name & ...

随机推荐

  1. ruby 正则表达式 匹配中文

    1.puts /[一-龥]+/.match("this is 中文")                 =>中文 2.str2="123中文"puts / ...

  2. c#基础值类和引用类型

    //值类型:int double char decimal bool enum struct //引用类型:string 数组 自定义类 集合 object 接口 值传递传递的值得本身 引用传递传递的 ...

  3. C#知识点-StopWatch-计时

    目录 简单介绍 基本用法 结尾 简单介绍 Stopwatch 可以测量一个时间间隔的运行时间,也可以测量多个时间间隔的总运行时间.一般用来测量代码执行所用的时间或者计算性能数据,在优化代码性能上可以使 ...

  4. window下安装php调试工具xdebug

    1.https://xdebug.org/wizard.php在方框中输入本地phpinfo.php中的内容会提示对应要安装的版本, 2.https://xdebug.org/download.php ...

  5. redmine安装详解

    1.Linux:centos6.4(32位)2.Gcc的编译环境.使用make命令编辑.yum install gcc-c++ 3.PCRE PCRE(Perl Compatible Regular ...

  6. xwork-conversion.properties 目前没有解决方案

    它没法变成.xml 这意味着项目里就只能这样

  7. UVALive 4794 Sharing Chocolate(状压,枚举子集)

    n的规模可以状压,f[x][y][S]表示x行,y列,S集合的巧克力能否被切割. 预处理出每个状态S对应的面积和sum(S),对于一个合法的状态一定满足x*y=sum(S),实际上只有两个变量是独立的 ...

  8. HTML5新增的音频标签、视频标签

    我们所说的H5就是我们所说的HTML5中新增的语言标准 一.音频标签 在HTML5当中有一个叫做audio的标签,可以直接引入一段音频资源放到我们的网页当中 格式: <audio autopla ...

  9. JavaScript -- 内置对象数组

    数组 创建数组的基本方式有两种: 1.使用 Array构造函数 语法:new Array() 小括号( )说明: (1)预先知道数组要保存的项目数量 (2)向Array构造函数中传递数组应包含的项 2 ...

  10. ctDNA|endosymbiosis

    5.10叶绿体基因组编码多种蛋白质和RNA 叶绿体和线粒体的共同点:叶绿体和线粒体的大小,功能(编码区)大体一致,但叶绿体拥有更多基因,所以在编码tRNA时,也有内含子作为被剪切片段. 因为在原核生物 ...