教育学
我喜欢和讨厌 SO 上的此类问题,因为情绪、意见和有根据的猜测混合在一起,人们开始变得敏感,并且不知何故 每个人 都忘记了实际情况事实并最终完全忘记了原来的问题。
关于 SO 的许多技术问题至少有一个明确的答案(例如,可以通过执行验证的答案或引用权威来源的答案),但这些“为什么”问题通常不只有一个明确的答案。在我看来,有两种可能的方法可以明确地回答计算机科学中的“为什么”问题:
- 通过指向实现关注项的源代码。这从技术意义上解释了“为什么”:唤起这种行为需要哪些先决条件?
- 通过指向参与决策的开发人员编写的人类可读的工件(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 的回答。