为什么Python编译模块而不是正在运行的脚本?

Mik*_*ike 37 python

为什么Python编译脚本中使用的库,而不是自己调用的脚本?

例如,

如果有main.pymodule.py,并且Python正在运行python main.py,将会有一个已编译的文件,module.pyc但不会有一个用于main.为什么?

编辑

添加赏金.我认为这没有得到妥善回答.

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

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

kev*_*pie 27

文件在导入时编译.这不是安全的事情.简单地说,如果你导入它,python会保存输出.请参阅Fredrik Lundh在Effbot上发表的这篇文章.

>>>import main
# main.pyc is created
Run Code Online (Sandbox Code Playgroud)

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

python -m compileall .
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

问题编辑的答案

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

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

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

    使用compileall并不能解决这个问题.*.pyc除非明确调用,否则python执行的脚本不会使用.这有负面的副作用,Glenn Maynard他的回答中说得很清楚.

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

    可以使用轻型引导脚本,甚至可以使用python -c "import script",但这些脚本的样式有问题.

格伦梅纳德提供了一些灵感来纠正和改善这个答案.

  • 在不正确的信息旁边看到"+10✔+ 100"是令人沮丧的:现在人们会浪费时间尝试使用compileall来解决这个问题,并且他们会被误导认为运行脚本不会导入它.有人+ 100'da错误的回答也很奇怪,即使错误被清楚地解释了. (3认同)
  • 我觉得有趣的是我需要提供一个无法识别的选项来获取compileall的用法.除非有人知道他们的头脑. (2认同)

Gle*_*ard 25

似乎没有人想这么说,但我很确定答案很简单:这种行为没有坚实的理由.

到目前为止给出的所有理由基本上都是错误的:

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

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

您不需要将脚本移动到虚拟模块并设置引导脚本以欺骗Python进行缓存.这是一个hackish解决方法.

我可以设想的唯一可能(并且非常难以令人信服)的原因是避免您的主目录被一堆.pyc文件弄得乱七八糟.(这不是真正的理由;如果这是一个实际的问题,则pyc文件应保存为点文件.)这当然没有理由不甚至有一个选项来做到这一点.

Python绝对应该能够缓存主模块.

  • @Matt:对于"为什么会这样做"的问题,没有答案.这是一个有缺陷前提的问题:一切都存在的原因.Python是由人类编写的,因此它有其他语言的毛刺,不一致和瑕疵,我相信这是其中之一.因此我的答案是:没有好的,令人信服的理由.如果存在,则没有在此处暗示,并且在文档中找不到它. (5认同)
  • 我真的希望人们能够真正回答问题,就像你有@GlennMaynard一样.好的,干得好!当人们给出机械上的答案时,我也觉得很令人沮丧,似乎他们正在回答问题而实际上只是避免解决问题背后的意图.我总是看到这种糟糕的答案,以及人们为死亡辩护不正确的行为,只因为这是当前的现状. (2认同)

Mar*_*ase 8

教育学

我又爱又恨类似这样的问题上如此,因为有感情,意见的复杂混合物,和受过教育的猜测事情,人们开始变得snippy,不知何故每个人都失去了跟踪实际的事实,并最终失去了轨道原题的共.

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

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

第二种类型的答案更难以证实,因为它需要了解编写代码的开发人员的想法,特别是如果没有易于查找的公共文档来解释特定的决策.

到目前为止,这个主题有7个答案,专注于阅读Python开发人员的意图,但整批中只有一个引用.(它引用了Python手册的一部分,但没有回答OP的问题.)

这是我在试图回答这两个与引文沿着"为什么"的问题的侧面.

源代码

触发编译.pyc的前提条件是什么?我们来看看源代码.(令人讨厌的是,GitHub上的Python没有任何发布标签,所以我只是告诉你我正在看715a6e.)

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

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;
}
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

这里的突出方面是函数解析并编译文件并返回指向字节代码的指针(如果成功).

现在我们仍处于死胡同,所以让我们从一个新的角度来看待它.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;
}
Run Code Online (Sandbox Code Playgroud)

这里的重点是这两个函数基本上与导入器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;
}
Run Code Online (Sandbox Code Playgroud)

这在.pyc文件被引入Python 之前就存在了!难怪当时的设计没有将脚本参数的编译考虑在内.在提交信息神秘地说:

"编译"版本

这是3天内几十次提交之一...... Guido似乎深陷一些黑客攻击/重构,这是第一个恢复稳定的版本.这个提交甚至早于创建Python-Dev邮件列表大约五年!

保存编译的字节码是在6个月后于1991年引入的.

这仍然早于列表服务,所以我们不知道Guido在想什么.看起来他只是认为导入器是为了缓存字节码而挂钩的最佳位置.他是否认为做同样的事情的想法__main__还不清楚:要不是他没有想到,要么他认为这比它的价值更麻烦.

我找不到任何错误被缓存到了主模块的字节码相关的bugs.python.org,也可以找到关于它的邮件列表上的任何消息,因此,显然没有人认为这是值得的麻烦,尝试添加它.

总结一下:编译所有模块的原因.pyc除了__main__它是历史的怪癖.__main__.pyc文件存在之前,如何将工作的设计和实现融入到代码中.如果你想了解更多,你需要通过电子邮件发送Guido并询问.

格伦梅纳德的回答是:

似乎没有人想这么说,但我很确定答案很简单:这种行为没有坚实的理由.

我同意100%.有支持这一理论的间接证据,这个主题中没有其他人提供过一丝证据来支持任何其他理论.我赞成格伦的答案.