【问题标题】:Why does Python compile modules but not the script being run?为什么 Python 编译模块而不是正在运行的脚本?
【发布时间】:2011-03-11 01:27:17
【问题描述】:

为什么 Python 编译脚本中使用的库,而不编译被调用的脚本本身?

例如,

如果存在main.pymodule.py,并且Python 是通过执行python main.py 运行的,那么将有一个编译文件module.pyc,但没有一个用于main。为什么?

编辑

增加赏金。我认为这个问题没有得到正确回答。

  1. 如果响应是main.py目录的潜在磁盘权限,Python为什么要编译模块?它们同样可能(如果不是更有可能)出现在用户没有写入权限的位置。如果 main 可写,Python 可以编译它,或者在另一个目录中编译。

  2. 如果原因是收益微乎其微,请考虑脚本将被大量使用的情况(例如在 CGI 应用程序中)。

【问题讨论】:

  • 如果我提出了一个有趣的问题,我会 +1,但实际上我是 -1,因为你选择了错误的答案。
  • 你认为正确的答案是什么?

标签: python


【解决方案1】:

文件在导入时被编译。这不是安全问题。很简单,如果你导入它,python 会保存输出。请参阅 Fredrik Lundh 在 Effbot 上的 this post

>>>import main
# main.pyc is created

在运行脚本时,python 将使用 *.pyc 文件。 如果您有其他原因需要预编译脚本,您可以使用 compileall 模块。

python -m compileall .

编译所有用法

python -m compileall --help
option --help not recognized
usage: python compileall.py [-l] [-f] [-q] [-d destdir] [-x regexp] [directory ...]
-l: don't recurse down
-f: force rebuild even if timestamps are up-to-date
-q: quiet operation
-d destdir: purported directory name for error messages
   if no directory arguments, -l sys.path is assumed
-x regexp: skip files matching the regular expression regexp
   the regexp is searched for in the full path of the file

问题编辑答案

  1. 如果响应是main.py目录的潜在磁盘权限,Python为什么要编译模块?

    模块和脚本的处理方式相同。导入是触发保存输出的原因。

  2. 如果原因是收益微乎其微,请考虑脚本将被大量使用的情况(例如在 CGI 应用程序中)。

    使用 compileall 并不能解决这个问题。 python 执行的脚本不会使用*.pyc,除非显式调用。这有负面影响,Glenn Maynardhis answer 中很好地说明了这一点。

    给出的 CGI 应用程序示例确实应该通过使用像 FastCGI 这样的技术来解决。如果你想消除编译脚本的开销,你可能也想消除启动 python 的开销,更不用说数据库连接开销了。

    可以使用轻量级的引导脚本,甚至可以使用 python -c "import script",但它们的风格值得怀疑。

Glenn Maynard 提供了一些灵感来纠正和改进这个答案。

【讨论】:

  • 我觉得很有趣,我需要提供一个不可识别的选项来获得 compileall 的用法。除非有人知道他们的头顶。
  • 这并不能真正回答问题,因为脚本是作为运行它的副作用而被导入的。
  • 这也是不正确的:Python 将不会将 .pyc 文件用于主脚本,即使它已经存在,除非你直接运行它 (python script.pyc) ,这是一个坏主意。它甚至不寻找一个。
  • 这是一个很好的答案。我会买那个 main 没有导入,因此没有编译。
  • 在错误信息旁边看到“+10 ✔ +100”令人沮丧:现在人们会浪费时间尝试使用 compileall 来解决这个问题,他们会被误导认为运行脚本不会导入它。有人给错误的答案 +100 也很奇怪,即使错误已得到明确解释。
【解决方案2】:

似乎没有人愿意这么说,但我很确定答案很简单:这种行为没有充分的理由。

到目前为止给出的所有理由基本上都不正确:

  • 主文件没有什么特别之处。它作为一个模块加载,并像任何其他模块一样显示在sys.modules 中。运行一个主脚本无非是用模块名称__main__ 导入它。
  • 由于只读目录而无法保存.pyc文件没有问题; Python 只是忽略它并继续前进。
  • 缓存脚本的好处与缓存任何模块的好处相同:不必在每次运行时都浪费时间重新编译脚本。文档明确承认这一点(“因此,脚本的启动时间可能会减少......”)。

另一个需要注意的问题:如果您运行 python foo.py 并且 foo.pyc 存在,它将不会被使用。你必须明确python foo.pyc。这是一个非常糟糕的主意:这意味着 Python 在不同步时(由于 .py 文件更改)不会自动重新编译 .pyc 文件,因此在您手动重新编译之前不会使用对 .py 文件的更改.如果您升级 Python 并且 .pyc 文件格式不再兼容(这种情况经常发生),它也会因 RuntimeError 彻底失败。通常,这一切都是透明地处理的。

您不需要将脚本移动到虚拟模块并设置引导脚本来欺骗 Python 对其进行缓存。这是一个骇人听闻的解决方法。

我能想到的唯一可能(而且非常不可信)的原因是避免你的主目录被一堆 .pyc 文件弄得乱七八糟。 (这不是一个真正的原因;如果这是一个真正的问题,那么 .pyc 文件应该保存为点文件。)当然没有理由甚至没有选项来执行此操作。

Python 应该可以缓存主模块。

【讨论】:

  • @Matt:对于“为什么要这样做”这个问题,没有答案。这是一个有缺陷前提的问题:任何事情都有原因。 Python 是由人类编写的,因此与其他所有语言一样,它也有其缺陷、不一致和缺陷,我相信这就是其中之一。因此我的回答是:没有好的、令人信服的理由。如果存在,这里没有提示,文档中也找不到。
  • 我真的希望人们能像@GlennMaynard 一样真正回答问题。所以,干得好!我还发现,当人们给出机械式的答案时,他们似乎在回答问题,而实际上只是在避免解决问题背后的意图。我一直看到这种糟糕的答案,以及人们誓死捍卫不正确的行为,只是因为这是目前的现状。
  • 随着__pycache__Python3.2 中的引入,即使是避免混乱的理由也不再有效。
  • 脚本文件常见于/usr/bin/usr/local/bin 等,其中__pycache__ 目录或.pyc 文件将完全不合适。因此,只有通常放置在sys.path 上的专用目录中的库文件(模块、包等)才能获得缓存文件。
【解决方案3】:

教育学

我喜欢和讨厌 SO 上的此类问题,因为情绪、意见和有根据的猜测混合在一起,人们开始变得敏感,并且不知何故 每个人 都忘记了实际情况事实并最终完全忘记了原来的问题。

关于 SO 的许多技术问题至少有一个明确的答案(例如,可以通过执行验证的答案或引用权威来源的答案),但这些“为什么”问题通常不只有一个明确的答案。在我看来,有两种可能的方法可以明确地回答计算机科学中的“为什么”问题:

  1. 通过指向实现关注项的源代码。这从技术意义上解释了“为什么”:唤起这种行为需要哪些先决条件?
  2. 通过指向参与决策的开发人员编写的人类可读的工件(cmets、提交消息、电子邮件列表等)。这就是我认为 OP 感兴趣的“为什么”的真正意义:为什么 Python 的开发人员会做出这个看似武断的决定?

第二种答案更难证实,因为它需要让编写代码的开发人员记住,尤其是在没有易于找到的公开文档解释特定决定的情况下。

迄今为止,该线程有 7 个答案,仅专注于阅读 Python 开发人员的意图,但整批中只有 一个引用。 (并且它引用了 Python 手册的一部分,回答了 OP 的问题。)

这是我尝试回答 “为什么”问题的两面以及引文。

源代码

触发.pyc 编译的先决条件是什么?让我们看看the source code。 (烦人的是,GitHub 上的 Python 没有任何发布标签,所以我就告诉你我在看715a6e。)

load_source_module() 函数中的import.c:989 中有很有前途的代码。为简洁起见,我在这里删减了一些内容。

static PyObject *
load_source_module(char *name, char *pathname, FILE *fp)
{
    // snip...

    if (/* Can we read a .pyc file? */) {
        /* Then use the .pyc file. */
    }
    else {
        co = parse_source_module(pathname, fp);
        if (co == NULL)
            return NULL;
        if (Py_VerboseFlag)
            PySys_WriteStderr("import %s # from %s\n",
                name, pathname);
        if (cpathname) {
            PyObject *ro = PySys_GetObject("dont_write_bytecode");
            if (ro == NULL || !PyObject_IsTrue(ro))
                write_compiled_module(co, cpathname, &st);
        }
    }
    m = PyImport_ExecCodeModuleEx(name, (PyObject *)co, pathname);
    Py_DECREF(co);

    return m;
}

pathname 是模块的路径,cpathname 是相同的路径,但扩展名为 .pyc。唯一直接的逻辑是布尔值sys.dont_write_bytecode。其余的逻辑只是错误处理。所以我们寻求的答案不在这里,但我们至少可以看到,在大多数默认配置下,任何调用它的代码都会生成一个 .pyc 文件。 parse_source_module() 函数与执行流程没有真正的关系,但我会在这里展示它,因为我稍后会回到它。

static PyCodeObject *
parse_source_module(const char *pathname, FILE *fp)
{
    PyCodeObject *co = NULL;
    mod_ty mod;
    PyCompilerFlags flags;
    PyArena *arena = PyArena_New();
    if (arena == NULL)
        return NULL;

    flags.cf_flags = 0;

    mod = PyParser_ASTFromFile(fp, pathname, Py_file_input, 0, 0, &flags, 
                   NULL, arena);
    if (mod) {
        co = PyAST_Compile(mod, pathname, NULL, arena);
    }
    PyArena_Free(arena);
    return co;
}

这里的重点是该函数解析和编译文件并返回指向字节码的指针(如果成功)。

现在我们还处于死胡同,所以让我们从一个新的角度来解决这个问题。 Python如何加载它的参数并执行它?在pythonrun.c 中有一些函数用于从文件加载代码并执行它。 PyRun_AnyFileExFlags() 可以处理交互式和非交互式文件描述符。对于交互式文件描述符,它委托给PyRun_InteractiveLoopFlags()(这是REPL),对于非交互式文件描述符,它委托给PyRun_SimpleFileExFlags()PyRun_SimpleFileExFlags() 检查文件名是否以 .pyc 结尾。如果是,则调用run_pyc_file(),后者直接从文件描述符加载编译后的字节码,然后运行它。

在更常见的情况下(即.py 文件作为参数),PyRun_SimpleFileExFlags() 调用PyRun_FileExFlags()。这是我们开始寻找答案的地方。

PyObject *
PyRun_FileExFlags(FILE *fp, const char *filename, int start, PyObject *globals,
          PyObject *locals, int closeit, PyCompilerFlags *flags)
{
    PyObject *ret;
    mod_ty mod;
    PyArena *arena = PyArena_New();
    if (arena == NULL)
        return NULL;

    mod = PyParser_ASTFromFile(fp, filename, start, 0, 0,
                   flags, NULL, arena);
    if (closeit)
        fclose(fp);
    if (mod == NULL) {
        PyArena_Free(arena);
        return NULL;
    }
    ret = run_mod(mod, filename, globals, locals, flags, arena);
    PyArena_Free(arena);
    return ret;
}

static PyObject *
run_mod(mod_ty mod, const char *filename, PyObject *globals, PyObject *locals,
     PyCompilerFlags *flags, PyArena *arena)
{
    PyCodeObject *co;
    PyObject *v;
    co = PyAST_Compile(mod, filename, flags, arena);
    if (co == NULL)
        return NULL;
    v = PyEval_EvalCode(co, globals, locals);
    Py_DECREF(co);
    return v;
}

这里的重点是,这两个函数与导入器的load_source_module()parse_source_module()的作用基本相同。它调用解析器从 Python 源代码创建 AST,然后调用编译器创建字节码。

那么这些代码块是多余的还是它们有不同的用途?不同之处在于一个块从文件加载模块,而另一个块将模块作为参数。在本例中,该模块参数是 __main__ 模块,它是在初始化过程的早期使用低级 C 函数创建的。 __main__ 模块不会通过大多数正常的模块导入代码路径,因为它是如此独特,并且作为副作用,它不会通过生成 .pyc 文件的代码。

总结一下:__main__ 模块没有被编译成 .pyc 的原因是它没有被“导入”。 是的,它出现在 sys.modules 中,但它得到了通过与真正的模块导入完全不同的代码路径。

开发者意图

好的,所以我们现在可以看到,这种行为更多地与 Python 的设计有关,而不是与源代码中任何明确表达的基本原理有关,但这并不能回答这是一个有意的决定还是仅仅是一个不会打扰任何人的副作用,值得改变。开源的好处之一是,一旦我们找到了我们感兴趣的源代码,我们就可以使用 VCS 来帮助追溯导致当前实施的决策。

这里的关键代码行之一 (m = PyImport_AddModule("__main__");) 可以追溯到 1990 年,由 BDFL 本人 Guido 编写。它已在其间的几年中进行了修改,但修改是肤浅的。最初编写时,脚本参数的主模块是这样初始化的:

int
run_script(fp, filename)
    FILE *fp;
    char *filename;
{
    object *m, *d, *v;
    m = add_module("`__main__`");
    if (m == NULL)
        return -1;
    d = getmoduledict(m);
    v = run_file(fp, filename, file_input, d, d);
    flushline();
    if (v == NULL) {
        print_error();
        return -1;
    }
    DECREF(v);
    return 0;
}

这在 .pyc 文件被引入 Python 之前就已经存在了!难怪当时的设计没有考虑到脚本参数的编译。 commit message 神秘地说:

“编译”版本

这是 3 天内的几十次提交中的一个……看来 Guido 已经深入进行了一些黑客/重构,这是第一个恢复稳定的版本。这个提交甚至比the Python-Dev mailing list 的创建早了大约五年!

保存编译后的字节码是introduced 6 months later, in 1991

这仍然早于列表服务,因此我们不知道 Guido 的想法。看来他只是认为导入器是为了缓存字节码而连接的最佳位置。他是否考虑过为__main__ 做同样的事情尚不清楚:要么他没有想到,要么他认为这比它的价值更麻烦。

我在 bugs.python.org 上找不到与缓存主模块的字节码相关的 any bugs,我在邮件列表中也找不到任何关于它的消息,所以显然没有其他人认为值得麻烦尝试添加它。

总结一下:除了__main__ 之外的所有模块都编译为.pyc 的原因是它是历史的一个怪癖。 __main__ 工作方式的设计和实现被纳入了.pyc 文件之前的代码甚至存在。如果您想了解更多信息,您需要给 Guido 发送电子邮件并询问。

格伦梅纳德的回答说:

似乎没有人愿意这么说,但我很确定答案很简单:这种行为没有充分的理由。

我同意 100%。有间接证据支持这一理论,并且该线程中的其他任何人都没有提供任何证据来支持任何其他理论。我赞成 Glenn 的回答。

【讨论】:

    【解决方案4】:

    如需回答您的问题,请参考 Python 官方文档中的6.1.3. “Compiled” Python files

    当通过在命令行中给出脚本名称来运行脚本时,脚本的字节码永远不会写入 .pyc 或 .pyo 文件。因此,可以通过将脚本的大部分代码移动到模块并使用导入该模块的小型引导脚本来减少脚本的启动时间。也可以直接在命令行上命名 .pyc 或 .pyo 文件。

    【讨论】:

    • 这只是一种解决方法,而不是这种行为的原因。
    【解决方案5】:

    Since:

    从 .pyc 或 .pyo 文件中读取程序的运行速度并不比从 .py 文件中读取时快; .pyc 或 .pyo 文件唯一更快的是它们的加载速度。

    不需要为主脚本生成 .pyc 文件。只编译可能被多次加载的库。

    已编辑

    看来你没有明白我的意思。首先,了解编译成.pyc 文件的整个想法是使同一文件在第二次执行时更快。但是,请考虑 Python 是否确实编译了正在运行的脚本。解释器将在第一次运行时将字节码写入.pyc 文件,这需要时间。所以它甚至会运行得慢一点。您可能会争辩说它之后会运行得更快。好吧,这只是一个选择。另外,正如this 所说:

    显式优于隐式。

    如果想通过使用.pyc 文件来加速,应该手动编译它并显式运行.pyc 文件。

    【讨论】:

    • 这没有回答问题。
    • 这对我来说似乎是最合理的,不过我很想知道更多。
    • 在单次执行期间没有多次加载模块;它们被加载一次并存储在 sys.modules 中。 .pyc 文件用于加速初始导入 - 对于主脚本和其他模块,每次执行最多发生一次。
    【解决方案6】:

    因为正在运行的脚本可能位于不适合生成 .pyc 文件的地方,例如 /usr/bin

    【讨论】:

    • 但肯定不是绝对需要将其存储在与原始脚本相同的目录中吗?
    • @user470379: >>> import this ... Special cases aren't special enough to break the rules.
    • 这不能回答问题。所有依赖项也可能位于不可写的挂载上。它在模块页面中明确指出无法写入pyo文件不是错误。此外,对 main 的不同处理是基于其潜在位置的特殊套管。
    • @Matt 是正确的:编写 .pyc 文件始终是可选的;即使您不会编写 .pyc,您也可以随时导入模块。
    【解决方案7】:

    因为不同版本的 Python(3.6、3.7 ...)具有不同的字节码表示,因此尝试为此设计编译系统被认为过于复杂。 PEP 3147 讨论了rationale

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-03-28
      • 2020-05-22
      • 1970-01-01
      • 2011-04-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多