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

8ftvxx2r  于 2023-03-11  发布在  Python
关注(0)|答案(7)|浏览(96)

为什么Python编译脚本中使用的库,而不是脚本本身?
例如,
如果有main.pymodule.py,并且Python是通过python main.py运行的,那么就会有一个编译文件module.pyc,但是main没有,为什么?
1.如果回应是main.py目录的潜在磁盘权限,Python为什么要编译模块呢?它们很可能(如果不是更可能的话)出现在用户没有写权限的位置。如果main是可写的,Python可以编译它,或者在另一个目录中。
1.如果原因是好处太少,请考虑脚本将被大量使用的情况(例如在CGI应用程序中)。

pvabu6sv

pvabu6sv1#

文件在导入时被编译。这不是安全问题。只是如果你导入它,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

如果响应是main.py目录的潜在磁盘权限,Python为什么要编译模块?
模块和脚本被同等对待。导入是触发输出被保存的原因。
如果原因是好处太少,请考虑脚本将被大量使用的情况(例如在CGI应用程序中)。
使用compileall并不能解决这个问题,python执行的脚本不会使用*.pyc,除非显式调用,这有负面的副作用,Glenn Maynard在他的回答中很好地说明了这一点。
CGI应用程序的例子应该使用FastCGI这样的技术来解决,如果你想消除编译脚本的开销,你可能也想消除启动python的开销,更不用说数据库连接的开销了。
可以使用一个简单的引导脚本,甚至python -c "import script",但这些脚本的风格有问题。

ctehm74n

ctehm74n2#

似乎没有人想这么说,但我很确定答案很简单:这种行为没有确凿的理由。
到目前为止给出的所有理由基本上都是不正确的:

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

另一个需要注意的问题是:如果你运行python foo.py并且foo.pyc已经存在,那么它 * 将不会被使用 *。你必须 * 显式地 * 使用python foo.pyc。这是一个非常糟糕的主意:这意味着当.pyc文件不同步时(由于.py文件发生了变化),Python不会自动重新编译它,所以对.py文件的修改只有在你手动重新编译它之后才能使用。如果你升级Python,而.pyc文件格式不再兼容,它也会直接失败,并返回RuntimeError,这种情况经常发生。通常,这一切都是透明处理的。
你不需要将脚本移动到一个伪模块中,然后设置一个引导脚本来欺骗Python缓存它,这是一个很难的解决方法。
我能想出的唯一可能(而且非常不令人信服)的理由是避免您的home目录被一堆.pyc文件弄得乱七八糟。如果这确实是一个问题,那么.pyc文件应该保存为点文件。)当然没有理由不使用 option 来执行此操作。
Python绝对应该能够缓存主模块。

5rgfhyps

5rgfhyps3#

教育学

我喜欢也讨厌这样的问题,因为有一个复杂的混合情绪,意见,和有根据的猜测进行,人们开始变得暴躁,不知何故 * 每个人 * 失去了实际事实的轨道,并最终失去了原来的问题完全轨道。
许多关于SO的技术问题至少有一个明确的答案(例如,可以通过执行验证的答案或引用权威来源的答案),但这些“为什么”的问题往往没有一个单一的、明确的答案。在我看来,有两种可能的方法来明确回答计算机科学中的“为什么”问题:
1.通过指向实现关注项目的源代码。这从技术意义上解释了“为什么”:引发这种行为的前提条件是什么?
1.通过指向由参与决策的开发人员编写的可读工件(注解、提交消息、电子邮件列表等),这才是我假设OP感兴趣的真实的意义上的“为什么”:为什么Python的开发者会做出这个看似武断的决定
第二种答案更难证实,因为它需要了解编写代码的开发人员的想法,特别是如果没有容易找到的公共文档来解释某个特定的决定。
到目前为止,这篇文章有7个答案,都是关于阅读Python开发者的意图的,但在整篇文章中只有一个引用(它引用了Python手册中的一个章节,但没有回答OP的问题)。
下面是我试图回答“为什么”问题的两个方面,沿着引用。

源代码

触发编译.pyc的前提条件是什么?让我们看看the source code。(令人烦恼的是,GitHub上的Python没有任何release标签,所以我只告诉你我正在查看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;
}

这里最突出的方面是函数解析和编译文件,并返回指向字节码的指针(如果成功)。
现在我们仍然是死胡同,所以让我们从一个新的Angular 来处理这个问题。Python如何加载它的参数并执行它?在pythonrun.c中,有一些函数可以从文件加载代码并执行它。PyRun_AnyFileExFlags()可以处理交互式和非交互式文件描述符。对于交互式文件描述符,它委托给PyRun_InteractiveLoopFlags()(这是REPL),对于非交互式文件描述符,它委托给PyRun_SimpleFileExFlags()PyRun_SimpleFileExFlags()检查文件名是否以.pyc结尾。如果是,然后它调用run_pyc_file()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的创建早了大约5年!
保存已编译的字节码是introduced 6 months later, in 1991
这仍然是在列表服务器之前,所以我们不知道Guido是怎么想的,看起来他只是认为导入器是最好的连接位置,以便缓存字节码,但他是否考虑过对__main__做同样的事情还不清楚:要么他没有想到,要么他认为这是更多的麻烦比它的价值。
我在www.example.com上找不到bugs.python.org与缓存主模块的字节码相关的any bugs,在邮件列表上也找不到任何关于它的消息,所以显然没有其他人认为值得尝试添加它。

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

格伦·梅纳德的回答是:
似乎没有人想这么说,但我很确定答案很简单:这种行为没有确凿的理由。
我100%同意。有间接证据支持这个理论,在这个帖子里没有人提供任何证据来支持任何其他理论。我投票支持格伦的答案。

js4nwp54

js4nwp544#

要回答你的问题,请参考Python官方文档中的6.1.3.“编译”Python文件。
当通过在命令行上指定脚本的名称来运行脚本时,脚本的字节码永远不会写入.pyc或.pyo文件。因此,通过将脚本的大部分代码移到模块中并使用导入该模块的小型引导脚本,可以减少脚本的启动时间。也可以直接在命令行上命名.pyc或.pyo文件。

cgyqldqp

cgyqldqp5#

自:
程序从.pyc或.pyo文件中读取时的运行速度并不比从.py文件中读取时快;pyc或.pyo文件唯一快的地方是它们的加载速度。
主脚本不需要生成.pyc文件,只需要编译可能多次加载的库即可。

编辑日期

看起来你没有理解我的意思。首先,要知道编译成.pyc文件的整个想法是为了让同一个文件在第二次运行时执行得更快。然而,考虑一下Python是否编译了正在运行的脚本。解释器会在第一次运行时将字节码写入.pyc文件。这需要时间。所以它甚至会运行得慢一点。你可能会争辩说,它会运行得更快之后。嗯,这只是一个选择。另外,正如this所说:
显性比隐性好。
如果想通过使用.pyc文件来加速,应该手动编译它并显式运行.pyc文件。

kmynzznz

kmynzznz6#

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

nzk0hqpo

nzk0hqpo7#

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

相关问题