pyc文件是什么,python中pyc文件详解
pyc 文件的触发
上一篇文章我们介绍了字节码,当时提到,py 文件在执行的时候会先被编译成 PyCodeObject 对象,并且该对象还会被保存到 pyc 文件中。
但不幸的是,事实并不总是这样,有时当我们运行一个简单的程序时,并没有产生 pyc 文件。因此我们猜测:有些 Python 程序只是临时完成一些琐碎的工作,这样的程序仅仅只会运行一次,然后就不会再使用了,因此也就没有保存至 pyc 文件的必要。
如果我们在代码中加上了一个 import abc 这样的语句,再执行你就会发现 Python 为 abc.py 生成了 pyc 文件,这就说明 import 会触发 pyc 的生成。
实际上,在运行过程中,如果碰到 import abc 这样的语句,那么 Python 会在设定好的 path 中寻找 abc.pyc 或者 abc.pyd 文件。如果没有这些文件,而是只发现了 abc.py,那么会先将 abc.py 编译成 PyCodeObject,然后写入到 pyc 文件中。
接下来,再对 abc.pyc 进行 import 动作。对的,并不是编译成 PyCodeObject 对象之后就直接使用。而是先写到 pyc 文件里,然后再将 pyc 文件里面的 PyCodeObject 对象重新在内存中复制出来。
当然啦,触发 pyc 文件生成不仅可以通过 import,还可以通过 py_compile 模块手动生成。比如当前有一个 tools.py:
a = 1 b = "你好啊"
如何将其编译成 pyc 呢?
import py_compile py_compile.compile("tools.py")
查看当前目录的 __pycache__ 目录,会发现 pyc 已经生成了。
然后 py文件名.cpython-版本号.pyc 为编译之后的 pyc 文件名。
pyc 文件的导入
如果有一个现成的 pyc 文件,我们要如何导入它呢?
from importlib.machinery import SourcelessFileLoader tools = SourcelessFileLoader( "tools", "__pycache__/tools.cpython-38.pyc" ).load_module() print(tools.a) # 1 print(tools.b) # 你好啊
以上我们就成功手动导入了 pyc 文件。
pyc 文件包含的内容
pyc 文件在创建的时候都会往里面写入哪些内容呢?
1. magic number
这是 Python 定义的一个整数值,不同版本的 Python 会定义不同的 magic number,这个值是为了保证 Python 能够加载正确的pyc。
比如 Python3.7 不会加载 3.6 版本的 pyc,因为 Python 在加载 pyc 文件的时候会首先检测该 pyc 的 magic number。如果和自身的 magic number 不一致,则拒绝加载。
2. pyc 文件的写入时间
这个很好理解,在加载 pyc 之前会先比较源代码的最后修改时间和 pyc 文件的写入时间。如果 pyc 文件的写入时间比源代码的修改时间要早,说明在生成 pyc 之后,源代码被修改了,那么会重新编译并写入 pyc,而反之则会直接加载已存在的 pyc。
3. py 文件的大小
py 文件的大小也会被记录在 pyc 文件中。
4. PyCodeObject 对象
编译之后的 PyCodeObject 对象,这个不用说了,肯定是要存储的,并且是序列化之后再存储。
因此 pyc 文件的结构如下:
注意:以上是 Python 3.7+ 的 pyc 文件结构,如果版本低于 3.7,那么开头没有 4 个 \x00。我们实际验证一下:
import struct from importlib.util import MAGIC_NUMBER from datetime import datetime with open("__pycache__/tools.cpython-38.pyc", "rb") as f: data = f.read() # 0 ~ 4 字节是 MAGIC NUMBER print(data[: 4]) # b'U\r\r\n' print(MAGIC_NUMBER) # b'U\r\r\n' # 4 ~ 8 字节是 4 个 \x00 print(data[4: 8]) # b'\x00\x00\x00\x00' # 8 ~ 12 字节是 pyc 的写入时间(小端存储),一个时间戳 ts = struct.unpack("<I", data[8: 12])[0] print(ts) # 1671001724 print( datetime.fromtimestamp(ts) ) # 2022-12-14 20:32:23 # 12 ~ 16 字节是 py 文件的大小 print( struct.unpack("<I", data[12: 16])[0] ) # 21
结果和我们分析的一样,因此对于任何一个 pyc 文件来说,前 16 字节是固定的(如果 Python 低于 3.7,那么前 12 个字节是固定的)。
16 个字节往后就是 PyCodeObject 对象,并且是序列化之后的,因为该对象显然无法直接存在文件中。
import marshal with open("__pycache__/tools.cpython-38.pyc", "rb") as f: data = f.read() # 通过 marshal.loads 可以反序列化 # marshal.dumps 则表示序列化 code = marshal.loads(data[16:]) # 此时就拿到了 py 文件编译之后的 PyCodeObject print(code) """ <code object <module> at 0x..., file "tools.py", line 1> """ # 查看常量池 print(code.co_consts) # (1, '你好啊', None) # 符号表 print(code.co_names) # ('a', 'b')
问题来了,既然我们可以根据 pyc 文件反推出 PyCodeObject,那么能否手动构建 PyCodeObject 然后生成 pyc 呢?来试一下。
a = 1 b = 2 c = 3
上述代码编译之后的结果,就是我们要构建的 PyCodeObject。
本文地址:百科问答频道 https://www.neebe.cn/wenda/903398.html,易企推百科一个免费的知识分享平台,本站部分文章来网络分享,本着互联网分享的精神,如有涉及到您的权益,请联系我们删除,谢谢!