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

时间:2011-03-11 01:27:18

标签: python

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

例如,

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

修改

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

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

  2. 如果原因是效益最小,请考虑脚本多次使用的情况(例如在CGI应用程序中)。

7 个答案:

答案 0 :(得分:26)

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

>>>import main
# main.pyc is created

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

python -m compileall .

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中有明确说明。

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

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

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

答案 1 :(得分:24)

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

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

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

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

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

我可以设想的唯一可能(并且非常难以令人信服)的原因是避免您的主目录被一堆.pyc文件弄得乱七八糟。 (这不是一个真正的原因;如果这是一个真正的问题,那么.pyc文件应该保存为dotfiles。)当然没有理由不使用选项来执行此操作。

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

答案 2 :(得分:8)

教学法

我喜欢和讨厌这样的问题,因为那里有一种复杂的情感,意见和有教育的猜测混合在一起,人们开始变得狡猾,不知怎的每个人都失去了轨道实际的事实,并最终完全失去对原始问题的追踪。

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

  1. 指向实现关注项的源代码。这解释了"为什么"从技术意义上讲:唤起这种行为需要哪些先决条件?
  2. 指向参与制定决策的开发人员编写的人类可读工件(评论,提交消息,电子邮件列表等)。这才是真正意义上的"为什么"我认为OP感兴趣的是:为什么Python的开发人员做出这个看似随意的决定?
  3. 第二种类型的答案更难以证实,因为它需要了解编写代码的开发人员的想法,特别是如果没有易于查找的公共文档解释特定决策。

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

    这是我尝试回答""为什么"问题以及引用。

    源代码

    触发编译.pyc的前提条件是什么?我们来看the source code。 (令人恼火的是,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;
    }
    

    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__模块没有经过大多数普通模块导入代码路径,因为它是如此独特,而且作为副作用,它不会通过生成{{1}的代码。文件。

    总结一下:.pyc模块未编译为.pyc的原因是它没有"#34;导入"。是它出现在sys.modules中,但它通过一个与实际模块导入完全不同的代码路径到达那里。

    开发者意图

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

    这里关键的代码行之一(__main__)可以追溯到1990年,由BDFL自己编写,Guido。它在中间进行了修改,但修改是肤浅的。首次编写时,脚本参数的主模块初始化如下:

    m = PyImport_AddModule("__main__");

    在将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; } 文件引入Python之前就已经存在了!难怪当时的设计没有考虑脚本参数的编译。 commit message神秘地说:

      

    "编译"版本

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

    保存已编译的字节码为introduced 6 months later, in 1991

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

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

    总结一下:将所有模块编译到__main__.pyc以外的原因是它是历史的一个小问题。如何设计和实现{在__main__文件存在之前,{1}}工作已经被编入代码中。如果你想了解更多,你需要通过电子邮件发送Guido并询问。

    格伦梅纳德的回答说:

      

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

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

答案 3 :(得分:4)

Since

  

从.pyc或.pyo文件读取程序时,程序运行速度不比从.py文件读取时运行速度快;关于.pyc或.pyo文件的唯一更快的事情就是加载它们的速度。

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

<强>被修改

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

  

明确比隐含更好。

如果想要使用.pyc文件加速,可以手动编译并明确运行.pyc文件。

答案 4 :(得分:3)

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

  

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

答案 5 :(得分:1)

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

答案 6 :(得分:0)

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