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

2024-06-26 02:09:17 发布

您现在位置:Python中文网/ 问答频道 /正文

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

例如

如果存在main.pymodule.py,并且Python是通过执行python main.py来运行的,则会有一个编译文件module.pyc,但不会有一个主文件。为什么?

编辑

增加赏金。我认为这个问题没有得到恰当的回答。

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

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


Tags: 模块文件用户py目录脚本权限编辑
3条回答

文件在导入时编译。这不是安全问题。很简单,如果导入它,python会保存输出。见Fredrik Lundh关于Effbot的this post

>>>import main
# main.pyc is created

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

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. If the response is potential disk permissions for the directory of main.py, why does Python compile modules?

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

  2. If the reason is that benefits will be minimal, consider the situation when the script will be used a large number of times (such as in a CGI application).

    使用compileall并不能解决这个问题。 除非显式调用,否则由python执行的脚本不会使用*.pyc。这有副作用,正如his answer中的Glenn Maynard所说。

    给出的CGI应用程序示例应该使用FastCGI这样的技术来处理。如果您希望消除编译脚本的开销,那么您可能也希望消除启动python的开销,更不用说数据库连接开销了。

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

Glenn Maynard为纠正和改进这个答案提供了一些启发。

似乎没人想这么说,但我敢肯定答案很简单:这种行为没有确凿的理由。

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

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

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

您不需要将脚本移动到虚拟模块并设置引导脚本来诱使Python缓存它。这是一个老套的解决办法。

我能想出的唯一可能(而且非常不令人信服)的原因是避免主目录被一堆.pyc文件弄乱。(这不是一个真正的原因;如果这是一个真正的问题,那么.pyc文件应该保存为do t files。)甚至没有理由不使用选项来执行此操作。

Python肯定能够缓存主模块。

教育学

我喜欢和讨厌这样的问题,因为有一个复杂的混合情绪,意见,和教育猜测进行,人们开始窃笑,不知何故,每个人都失去了实际的事实,最终完全失去了原来的问题。

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

  1. 通过指向实现关注项的源代码。这从技术角度解释了“为什么”:引发这种行为需要什么先决条件?
  2. 通过指向参与决策的开发人员编写的可读工件(注释、提交消息、电子邮件列表等)。这就是我认为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扩展名。唯一的直接逻辑是布尔值^{}。其余的逻辑只是错误处理。因此,我们寻求的答案不在这里,但我们至少可以看到,在大多数默认配置下,调用此函数的任何代码都将生成一个.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,然后调用编译器创建字节码。

那么这些代码块是冗余的还是有不同的用途?不同之处在于,一个块从文件加载模块,而另一个块将模块作为参数。模块参数是case-这个__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神秘地说:

"Compiling" version

这是三天内几十次犯罪中的一次。。。看起来Guido已经深入到了一些黑客/重构中,这是第一个恢复稳定的版本。这个承诺甚至比the Python-Dev mailing list的创建还要早5年!

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

这仍然早于列表服务,所以我们不知道Guido在想什么。似乎他只是认为导入程序是缓存字节码的最佳连接位置。他是否认为为__main__做同样的事情是不清楚的:要么他没有想到,要么他认为这比它值得的麻烦多。

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

总结一下:除了__main__之外,所有模块都编译成.pyc的原因是,这是一个历史怪癖。如何__main__工作的设计和实现是在.pyc文件甚至存在之前就被烘焙到代码中的。如果你想知道更多,你需要给Guido发邮件询问。

格伦·梅纳德的回答是:

Nobody seems to want to say this, but I'm pretty sure the answer is simply: there's no solid reason for this behavior.

我完全同意。有间接证据支持这一理论,而这条线索中没有其他人提供了一点证据来支持任何其他理论。我对格伦的回答投了高票。

相关问题 更多 >