如果Python VM执行字节代码,那么非导入模块的字节代码在哪里?

时间:2013-03-07 05:49:15

标签: python

我根据以下链接了解了一些事情(我可能错了!):

http://docs.python.org/2/glossary.html#term-bytecode

  1. .pyc是一个缓存文件,只有在其他地方导入模块时才会生成

  2. .pyc用于帮助加载性能,而不是执行性能。

  3. 运行python foo.py除非在某处导入foo.pyc,否则不生成foo

  4. Python有一个字节码编译器(用于生成.pyc

  5. Python的虚拟机执行字节码。

  6. 因此,当我运行python foo.py时,如果foo.py未在任何地方导入,那么Python实际上是否会创建内存中的字节码?

    缺少的.pyc似乎打破了Python VM的想法。

    这个问题扩展到Python解释器中的代码执行(在终端中运行python)。我相信CPython(或任何语言实现)不能做纯粹的解释。

    我认为问题的核心是:VM是否实际读取了.pyc文件?我假设VM将.pyc加载到执行环境中。

3 个答案:

答案 0 :(得分:4)

你的第1点到第5点是正确的,除了第4点的例外(如果我们是精确的).Python解释器有一个叫做字节码编译器的部分,它将源代码转换为<code object at 0x...>,你可以通过为任何函数f.__code__键入f进行检查。这是解释的真实字节码。然后,这些代码对象可以作为单独的步骤保存在.pyc个文件中。

以下是更详细的操作。当您加载foo.py及其导入的每个模块时,每个模块只运行一次字节码编译器。这不是一个太长的操作,但它仍然需要一些时间,特别是如果您的模块导入许多其他模块。这是.pyc个文件进入图片的地方。在import语句调用字节码编译器之后,它会尝试将生成的<code object>保存在.pyc文件中。下一次,如果.pyc文件已存在且.py文件尚未修改,则会从那里重新加载<code object>。这只是一个优化:它避免了调用字节码编译器的成本。在这两种情况下结果都是相同的:在内存中创建了<code object>并将被解释。

它仅适用于import语句,而不适用于主模块(即命令行foo.py中的python foo.py)。这个想法应该并不重要 - 字节码编译器在典型的中到大程序中放松时间的地方在于编译所有直接和间接导入的模块,而不仅仅是编译foo.py

答案 1 :(得分:1)

有趣......我做的第一件事是呼唤--help

$ python --help
usage: python [option] ... [-c cmd | -m mod | file | -] [arg] ...
Options and arguments (and corresponding environment variables):
-B     : don't write .py[co] files on import; also PYTHONDONTWRITEBYTECODE=x
...

我看到的第一个选项是在导入时禁用自动pyc和pyo文件生成,但这可能会导致其字母顺序。

让我们运行一些测试

$ echo "print 'hello world'" > test.py
$ python test.py 
hello world
$ ls test.py*
test.py
$ python -c "import test"
hello world
$ ls test.py*
test.py     test.pyc

所以它只在导入时生成了pyc文件。

现在为了检查正在使用哪些文件,我将使用类似于linux truss的OS X dtruss来做一个完整的跟踪......

$ echo '#!/bin/sh 
 python test.py' > test.sh 
$ chmod a+x test.sh
$ sudo dtruss -a ./test.sh 2>&1 | grep "test.py*"
975/0x5713:    244829       6      3 read(0x3, "#!/bin/sh \npython test.py\n\b\0", 0x50)         = 26 0
975/0x5713:    244874       4      2 read(0xFF, "#!/bin/sh \npython test.py\n\b\0", 0x1A)        = 26 0
977/0x5729:    658694       6      2 readlink("test.py\0", 0x7FFF5636E360, 0x400)        = -1 Err#22
977/0x5729:    658726      10      6 getattrlist("/Users/samyvilar/test.py\0", 0x7FFF7C0EE510, 0x7FFF5636C6E0 = 0 0
977/0x5729:    658732       3      1 stat64("test.py\0", 0x7FFF5636DCB8, 0x0)        = 0 0
977/0x5729:    658737       5      3 open_nocancel("test.py\0", 0x0, 0x1B6)      = 3 0
977/0x5729:    658760       4      2 stat64("test.py\0", 0x7FFF5636E930, 0x1)        = 0 0
977/0x5729:    658764       5      2 open_nocancel("test.py\0", 0x0, 0x1B6)      = 3 0
从它的外观来看,python甚至都没有触及test.pyc文件!

$ echo '#!/bin/sh 
 python -c "import test"' > test.sh
$ chmod a+x test.sh
$ sudo dtruss -a ./test.sh 2>&1 | grep "test.py*"
$ sudo dtruss -a ./test.sh 2>&1 | grep "test.py*"
1028/0x5d74:    654642       8      5 open_nocancel("test.py\0", 0x0, 0x1B6)         = 3 0
1028/0x5d74:    654683       8      5 open_nocancel("test.pyc\0", 0x0, 0x1B6)        = 4 0
$
好吧,这很有趣,看起来它打开了test.py然后test.pyc

当我们删除pyc文件时会发生什么。

$ rm test.pyc
$ sudo dtruss -a ./test.sh 2>&1 | grep "test.py*"
1058/0x5fd6:    654151       7      4 open_nocancel("/Users/samyvilar/test.py\0", 0x0, 0x1B6)        = 3 0
1058/0x5fd6:    654191       6      3 open_nocancel("/Users/samyvilar/test.pyc\0", 0x0, 0x1B6)       = -1 Err#2
1058/0x5fd6:    654234       7      3 unlink("/Users/samyvilar/test.pyc\0", 0x1012B456F, 0x1012B45E0)        = -1 Err#2
1058/0x5fd6:    654400     171    163 open("/Users/samyvilar/test.pyc\0", 0xE01, 0x81A4)         = 4 0

首先打开test.py然后'尝试'打开test.pyc,它返回一个错误,然后调用unlink并再次生成pyc文件......有趣的是,我认为它会检查。

如果我们删除原始py文件怎么办。

$ sudo dtruss -a ./test.sh 2>&1 | grep "test.py*"
1107/0x670d:    655064       4      1 open_nocancel("test.py\0", 0x0, 0x1B6)         = -1 Err#2
1107/0x670d:    655069       8      4 open_nocancel("test.pyc\0", 0x0, 0x1B6)        = 3 0

毫无疑问它无法打开test.py但它仍然继续,直到今天我不确定这是否真的'好'python应该发出某种警告,我已被烧了几次通过这个,不小心删除我的文件,运行我的测试,并感到松了一口气,因为他们只是在我无法找到源代码时才开始出汗!

在这个测试之后我们假设python只在调用python test.pyc时直接使用pyc文件,或者在导入时间接使用pyc文件,否则它似乎不会使用它们。

据说CPythons编译器设计得相当快,它没有进行太多类型检查,它可能会生成非常高级别的字节码,因此大部分工作负载实际上是由虚拟机完成的...它可能会单次传递,lexing-&gt;编译器 - &gt;字节代码一次性执行,每次执行此操作,它从命令行读取python文件或导入时没有pyc文件,在这种情况下它会创建它。

这可能是为什么其他一些实现更快的原因,因为它们需要更多的时间来编译,但生成可以很好地优化的原始字节码。

构建虚拟机非常难以有效地进行纯解释......

关于平衡,你的字节码越强大,你的编译器就越简单,但你的虚拟机必须越复杂和慢,反之亦然......

答案 2 :(得分:1)

Python无法直接执行源代码(与其他一些特殊解析的脚本语言不同,例如Bash)。无论源是什么,所有Python源代码都必须编译为字节码。 (这包括例如通过evalexec运行的代码)。生成字节码相当昂贵,因为它涉及运行解析器,因此缓存字节码(如.pyc)可以通过避免解析阶段来加速模块加载。

import foopython foo.py之间的区别仅在于后者不会缓存生成的字节码。