网站建设与设计致谢,重庆地区专业做网站的公司,搜索引擎优化介绍,网站建设招标要求前言 前面我们主要围绕pyObject和pyTypeObject聊完了python的内建对象部分#xff0c;现在我们将开启新的篇章—python虚拟机#xff0c;将聚焦在python的执行部分#xff0c;搞懂从“代码”到“执行”的过程。开启新的篇章之前#xff0c;你也许会有一个疑惑#xff1a;我…前言 前面我们主要围绕pyObject和pyTypeObject聊完了python的内建对象部分现在我们将开启新的篇章—python虚拟机将聚焦在python的执行部分搞懂从“代码”到“执行”的过程。开启新的篇章之前你也许会有一个疑惑我们写的代码是如何执行的从表面看我只要按照python的正确语法书写一段代码然后“剩下的”交给python解释器代码就能被执行了这也许就是大部分人能够给出的解释了在没有学习此篇章之前我也是这么认为的因为我也没有探究过“剩下的部分”python解释器是如何去操作的。因此从这一篇博客开始让我们一起带着这个问题“钻进”python的解释器中看看它到底做了个啥 开始
作为python开发者我相信大家对pyc文件应该并不陌生它虽然经常“藏”在我们看不到的地方但有时作用可不小你也许为了提速将整个项目的py文件转成pyc文件然后再去执行或者你为了加密将py文件转成pyc文件再发给别人…等等这些都和pyc文件的特性有关系这样看来pyc文件似乎比py文件更“抢手”这其中似乎有什么蹊跷还是python解释器对pyc文件有偏心因此在开启“python执行过程”的探索pyc文件似乎比py文件更有研究价值
什么是pyc文件
细心的小伙伴应该早就发现了我们写的py文件夹中有时候会多一个额外的文件夹__pychache_点开这个文件夹你可能还会发现这里面会有一些以.pyc结尾的文件同时你还会发现它们的文件名和上级目录中的py文件名是有一些对应关系的。 这里的以.pyc结尾的文件就是我们常说的pyc文件看看它的目录名:__pycache_根据目录名我想大家应该猜到它的作用是什么了没错我们就可以把它当作是对py文件的一个缓存文件缓存的主要目的就是为了提加载速
pyc文件是怎么产生的
看上面的那个截图发现__pycache__中似乎只有一个.pyc文件为什么其他的py文件没有对应的pyc文件呢这也许从侧面说明了一点pyc文件不是必须的应该只在特定情况下才能触发生成。如果你还是有点怀疑可以查看自己的py文件目录 是和我们写的代码有关系吗答案是肯定的 实际上当我们每次通过import导入一个py文件时都可能会触发这个“生成pyc文件”的开关。 在当前目录所有的py文件中我只对demo2.py文件进行了导入操作没有对其他的py文件执行导入操作因此实际上就是import机制触发了pyc文件的产生。当然如果你想手动生成也是可以的下面是通过代码生成pyc文件的一个方法
import py_compile# 将py文件编译成pyc文件编译成PyCodeObject存放在pyc文件中
# PyCodeObject是编译真正的结果pyc文件只是它存放的位置
pyc_path py_compile.compile(filedemo.py)# 读取pyc文件的内容
pyc_content open(pyc_path, rb).read()
print(pyc_content) # 二进制的通过读取pyc文件的内容可以发现它实际上是一个二进制文件。 pyc文件的结构
到目前为止我们已经知道pyc文件是一个二进制文件用于缓存py文件那么它的结构是怎样的呢它里面包含了哪些东西你是不是不知道该何从下手了别担心你是否还记得它是可以通过import机制生成的那么就说明import机制中一定包含了它生成的逻辑所以那就让我们一起顺藤摸瓜吧
”顺着import摸瓜“
当我在阅读《python源码剖析》时书中介绍的创建pyc的过程是在import.c这个文件中产生的但我找了很长时间都没有找到相应的逻辑最后通过查阅各种资料和AI发现这个逻辑已经放在标准库importlib中实现了这里可能是版本的关系导致的或许有出入但问题不大。 该说不说python中有一个非常好用的东西那就是它的异常栈当一个函数有多处实现不知道具体是哪个地方的时候我们在每一个地方“埋雷”当python解释器不小心踩到我们的雷它的执行路线就会像多米洛骨牌连续翻倒一样清晰的展现在我们面前 可以看到import一个模块时它会走到一个名为get_code的函数中在此函数中就包含了pyc文件生成的逻辑这段代码的大概逻辑就是 找到模块对应的py路径根据py路径找到它对应的pyc路径尝试从pyc中读取如果读取成功校验它和py文件中的内容是否一致这里有两种校验方式基于hash和基于时间戳如果是一致的就直接返回如果发生了变化就从py中读取得到code obejct并写入到对应的pyc文件之后再返回最后调用exec方法执行返回的code obejct。
def get_code(self, fullname):Concrete implementation of InspectLoader.get_code.Reading of bytecode requires path_stats to be implemented. To writebytecode, set_data must also be implemented.source_path self.get_filename(fullname)source_mtime Nonesource_bytes Nonesource_hash Nonehash_based Falsecheck_source Truetry:# 获取py文件对应的pyc文件的路径bytecode_path cache_from_source(source_path)except NotImplementedError:bytecode_path Noneelse:try:- mtime (mandatory) is the numeric timestamp of last sourcecode modification;- size (optional) is the size in bytes of the source code.st self.path_stats(source_path)except OSError:passelse:# py文件最后的修改时间source_mtime int(st[mtime])try:data self.get_data(bytecode_path)except OSError:passelse:exc_details {name: fullname,path: bytecode_path,}try:flags _classify_pyc(data, fullname, exc_details)bytes_data memoryview(data)[16:]hash_based flags 0b1 ! 0if hash_based:check_source flags 0b10 ! 0if (_imp.check_hash_based_pycs ! never and(check_source or_imp.check_hash_based_pycs always)):source_bytes self.get_data(source_path)source_hash _imp.source_hash(_RAW_MAGIC_NUMBER,source_bytes,)_validate_hash_pyc(data, source_hash, fullname,exc_details)else:_validate_timestamp_pyc(data,source_mtime,st[size],fullname,exc_details,)except (ImportError, EOFError):passelse:_bootstrap._verbose_message({} matches {}, bytecode_path,source_path)return _compile_bytecode(bytes_data, namefullname,bytecode_pathbytecode_path,source_pathsource_path)if source_bytes is None:source_bytes self.get_data(source_path)code_object self.source_to_code(source_bytes, source_path)_bootstrap._verbose_message(code object from {}, source_path)if (not sys.dont_write_bytecode and bytecode_path is not None andsource_mtime is not None):if hash_based:if source_hash is None:source_hash _imp.source_hash(source_bytes)data _code_to_hash_pyc(code_object, source_hash, check_source)else:data _code_to_timestamp_pyc(code_object, source_mtime,len(source_bytes))try:self._cache_bytecode(source_path, bytecode_path, data)print(f写入code object到{bytecode_path}文件)if str(bytecode_path).endswith(demo.cpython-311.pyc):print(1/0)except NotImplementedError:passreturn code_object直白一点就是如果有现成的判断一下是不是最新的如果是就用现成的pyc如果没有或者不是最新的就现场生成一个。所以这里就是pyc能够提升加载速度的逻辑在加载一个模块时python解释器会先把我们写的代码进行“编译”然后将“编译”好的代码再拿去执行同时会将编译的结果写到pyc文件中当再次需要导入这个模块时如果已经有编译好的pyc文件并且是最新的就直接使用它就好这样就省去了“编译”的时间。其实就是一个缓存的逻辑
pyc文件的内容
还是在get_code这个函数中我们可以找到两种生成pyc文件的方式 分别是基于时间戳和hash值的两种方式以基于时间戳的方式为例它的具体逻辑如下 基于当前的python解释器的版本MAGIC_NUMBER生成一个字节数组之后在添加其他的一些数据mtime源文件最后的编辑时间source_size源文件的大小code源码编译后的结果以及一个无任何含义的0可能是官方预留的注意除了code以外其他数据的长度都是4这就是pyc文件的所有内容它们分别是magic_numbermtine0source_sizecode不同的python版本内容可能有出入我这里是py3-11-7。 tipsmagic_number主要用于判断解释器和当前要执行的python代码是否兼容你可以尝试用py3-7编译一个pyc文件然后用py3-11去执行它会抛出一个magic_number不兼容的错误。 为了证实pyc真的就是如上那些内容我们可以试着解析一个pyc文件试试
code object
到目前为止我们一直围绕着pyc文件在转从它的生成逻辑到它结构内容以及它的意义对它的了解应该算是了如指掌了但是我们似乎忽略了一个重要的环节—“编译”源文件是如何被编译的以及它的编译结果是什么 通过源代码可以发现其实就是调用了内置的compile方法对源文件的内容进行了“编译”编译的结果通过调用type方法可以知道是code object。 tips关于compile内部是如何执行的可以参考python源文件中的Python/compile.c文件。 code object的结构
在探究code object的结构之前我们可以先来看一下它所对应的源码的内容是怎样的以demo.py文件为例它的内容如下
class A:def fun(self, msghello):print(hello world2)a A()
a.fun()可以看到它包含了一个类A类下面包含了一个方法funfun有两个参数一个是self另外一个是msgfun中执行了一个打印字符串“hello world2”同时在全局下它还包含了一个变量aa调用了方法fun这就是所有的内容如果你是python的开发者你会如何将它“编译”成你需要的内容 想一想这里面是不是一个嵌套关系在全局下它包含了一个类型A和一个实例a在类型A中它包含了一个方法fun在方法fun中它包含了一个位置参数self和一个关键字参数msg且默认值是“hello”同时它内部还包含了一个字符串“hello world2”如何存储这些信息以及它们之间的关系呢 实际上code object也是类似于这样的一个嵌套对象每一个code object对象有它自己的属性同时code object里面可以包含其他code object每一个code object都可以看成是一个域或者是一个code block代码块而这个域就可以是上面说的哪些对象可以是一个模块可以是一个类可以是一个函数可以是一个方法…
demo.py编译得到的code object如下 code object各属性含义如下
属性名称含义co_argcount位置参数的数量。co_kwonlyargcount关键字参数的数量。co_nlocals局部变量的数量。co_stacksize所需的栈大小。co_flags编译标志。co_code实际的字节码指令。co_consts常量池字面量、元组等。co_names在字节码中使用的名称列表。co_varnames局部变量名列表。co_filename源代码文件名。co_name代码对象的名称。co_firstlineno第一行的行号。co_lnotab行号表字节码到源代码行号的映射。co_freevars自由变量的名称闭包中使用的变量。co_cellvars闭包中绑定的局部变量的名称。
每一个code object中都有一个重要的属性就是co_code它存储了字节码指令在实际执行的时候会根据不同的字节码指令执行不同的操作当前code object如果包含了其他的code object它们都会被存储在co_consts这个属性中。
解析code object对象
为了更加清晰地看到code object的嵌套结构我们可以使用json或者xml结构来展示它的结构
import dis
import types
import xml.etree.ElementTree as ET
from notes.utils import utils
import marshal
import pprintdef parse_code_object(code_object, result{}):将code对象转成json# 反编译当前的code object对象得到字节码指令print(f{code_object.co_name})dis.dis(code_object)keys [co_name, co_names, co_consts, co_argcount, co_cellvars, co_code, co_exceptiontable,co_filename,co_firstlineno, co_flags, co_freevars, co_kwonlyargcount, co_lines, co_linetable, co_lnotab,co_nlocals, co_positions, co_posonlyargcount, co_qualname, co_stacksize, co_varnames, replace]# 赋值for key in keys:result[key] getattr(code_object, key)result[co_consts] []co_consts code_object.co_constsfor co_const in co_consts:if isinstance(co_const, types.CodeType):_result {}parse_code_object(co_const, _result)result[co_consts].append(_result)else:result[co_consts].append(co_const)def code_object_to_xml(co, parent_element):code_element ET.SubElement(parent_element, code)code_element.attrib[co_name] co.co_nameET.SubElement(code_element, co_name).text co.co_nameET.SubElement(code_element, co_filename).text co.co_filenameET.SubElement(code_element, co_firstlineno).text str(co.co_firstlineno)ET.SubElement(code_element, co_argcount).text str(co.co_argcount)ET.SubElement(code_element, co_kwonlyargcount).text str(co.co_kwonlyargcount)ET.SubElement(code_element, co_nlocals).text str(co.co_nlocals)ET.SubElement(code_element, co_stacksize).text str(co.co_stacksize)ET.SubElement(code_element, co_flags).text str(co.co_flags)ET.SubElement(code_element, co_varnames).text str(co.co_varnames)ET.SubElement(code_element, co_names).text str(co.co_names)# ET.SubElement(code_element, co_consts).text str(co.co_consts)ET.SubElement(code_element, co_lnotab).text co.co_lnotab.hex()co_code_element ET.SubElement(code_element, co_code)co_code_element.text co.co_code.hex()# Recursively process nested code objectsfor const in co.co_consts:if isinstance(const, types.CodeType):co_consts ET.SubElement(code_element, co_consts)code_object_to_xml(const, co_consts)# code object它是可能是一个嵌套的对象可以转成json或者xml进行可视化展示pyc_file_path __pycache__/demo.cpython-311.pyc
pyc_contents utils.read_pyc_file(pyc_file_path)# 将bytes转成python中的对象
bytecode_data pyc_contents[bytecode_data]
code_object marshal.loads(bytecode_data) # 这里得到的就是一个code object对象# xml
root ET.Element(root)
code_object_to_xml(code_object, root)
tree ET.ElementTree(root)
file_path output.xml
tree.write(file_path, encodingutf-8, xml_declarationTrue)# json
result {}
parse_code_object(code_object, result)pprint.pprint(result, sort_dictsFalse)解析的xml结构如下
?xml version1.0 encodingutf-8?
rootcode co_namelt;modulegt;co_namelt;modulegt;/co_nameco_filename/Users/lvliangliang/Desktop/python/7.project/python-source-code-learning/notes/02python虚拟机/demo.py/co_filenameco_firstlineno1/co_firstlinenoco_argcount0/co_argcountco_kwonlyargcount0/co_kwonlyargcountco_nlocals0/co_nlocalsco_stacksize4/co_stacksizeco_flags0/co_flagsco_varnames()/co_varnamesco_names(A, a, fun)/co_namesco_lnotab00ff02011a051401/co_lnotabco_code970002004700640084006401a6020000ab0200000000000000005a0002006500a6000000ab0000000000000000005a016501a0020000000000000000000000000000000000000000a6000000ab000000000000000000010064025300/co_codeco_constscode co_nameAco_nameA/co_nameco_filename/Users/lvliangliang/Desktop/python/7.project/python-source-code-learning/notes/02python虚拟机/demo.py/co_filenameco_firstlineno1/co_firstlinenoco_argcount0/co_argcountco_kwonlyargcount0/co_kwonlyargcountco_nlocals0/co_nlocalsco_stacksize2/co_stacksizeco_flags0/co_flagsco_varnames()/co_varnamesco_names(__name__, __module__, __qualname__, fun)/co_namesco_lnotab0a01/co_lnotabco_code970065005a0164005a026404640284015a0364035300/co_codeco_constscode co_namefunco_namefun/co_nameco_filename/Users/lvliangliang/Desktop/python/7.project/python-source-code-learning/notes/02python虚拟机/demo.py/co_filenameco_firstlineno2/co_firstlinenoco_argcount2/co_argcountco_kwonlyargcount0/co_kwonlyargcountco_nlocals2/co_nlocalsco_stacksize3/co_stacksizeco_flags3/co_flagsco_varnames(self, msg)/co_varnamesco_names(print,)/co_namesco_lnotab0201/co_lnotabco_code97007401000000000000000000006401a6010000ab010000000000000000010064005300/co_code/code/co_consts/code/co_consts/code
/root我们写的代码是如何执行的
还记得我们最开始的疑惑吗—”我们写的代码是如何执行的“到现在为止我们从pyc文件“下手”搞懂了一个模块被加载时它的源代码会被编译成code object对象这是一个嵌套的对象它包含了执行源代码需要的所有信息为了提高加载速度这个对象也会被缓存到对应的pyc文件中所以从这一点看pyc文件只不过是一个载体用于暂时存储code object罢了code object才是我们后续研究的重点想象一下现在我们已经拿到包含字节码虽然目前我们还不知道这是啥的code object对象虽然不知道它后续是什么样的但是我们能够肯定的是我们离python解释器已经更近一步了离我们的代码被它执行的距离也越来越近了这是一个好的开始暂且记录我们的进度为50%吧当我们的这个进度为100时我们就能彻底搞清楚这个执行的过程啦加油后面我们再继续探索 更多内容可以关注博主的个人博客系统《Python源码剖析》之pyc文件