0

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

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

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

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

  3. runningpython foo.py不会生成foo.pyc,除非foo在某处导入。

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

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

那么,当我运行时python foo.py,如果foo.py没有在任何地方导入,Python 是否真的会创建内存字节码?

失踪.pyc似乎打破了 Python VM 的想法。

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

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

4

3 回答 3

4

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

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

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

于 2013-03-07T08:41:16.373 回答
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 仅在调用时直接使用 pyc 文件, python test.pyc或者在导入时间接使用,否则它似乎不使用它们。

据说 CPythons 编译器被设计得相当快,它不做太多类型检查,它可能会生成非常高级的字节码,所以大部分工作负载实际上是由虚拟机完成的......它可能只通过一次, lexing->compiler->byte-code 一次完成,它每次都会这样做,它从命令行读取一个 python 文件,或者在导入时没有 pyc 文件,在这种情况下它会创建它。

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

构建一个虚拟机来高效地做纯解释是极其困难的......

这一切都与平衡有关,您的字节码越强大,您的编译器就越简单,但您的虚拟机必须越复杂和越慢,反之亦然......

于 2013-03-07T09:05:06.420 回答
1

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

import foo和之间的区别python foo.py只是后者不缓存生成的字节码。

于 2013-03-07T09:13:33.680 回答