首先,这是一个实现细节。我将我的答案限制在 CPython 和 PyPy 上,因为我熟悉它们。Jython、IronPython 和其他实现的答案会有所不同——可能根本不同。
Python 更接近于“虚拟机模型”。Python 代码,与一些对他们的知识水平来说太大声的人的陈述相反,尽管每个人(包括我)在随意的讨论中都将它混为一谈,但从未被解释过。它在加载时总是被编译成字节码(同样,在 CPython 和 PyPy 上)。如果它是因为模块被导入并从 .py 文件加载而被加载的,则可能会创建一个 .pyc 文件来缓存编译输出。此步骤不是强制性的;您可以通过各种方式将其关闭,并且程序执行不会受到任何影响(除非下一个加载模块的过程必须再次执行)。但是,编译成字节码是不可避免的,如果不是从磁盘加载,字节码是在内存中生成的。
然后在模块级别执行此字节码(其确切细节是实现细节并且版本之间不同),这需要构建函数对象、类对象等。这些对象只是重用(持有指向)已经在内存中的字节码。这与 C++ 和 Java 不同,其中代码和类在编译期间/之后是一成不变的。在执行过程中,import
可能会遇到语句。我缺乏篇幅、时间和理解来描述进口机械,但简短的故事是:
- 如果它已经被导入一次,您将获得该模块对象(静态语言仅在编译时具有的另一个运行时构造)。在任何 Python 代码运行之前,已经导入了几个内置模块(好吧,它们都在 PyPy 中,原因超出了这个问题的范围),这仅仅是因为它们与解释器的核心紧密集成并且非常基础。
sys
就是这样一个模块。一些 Python 代码也可以预先运行,尤其是当您启动交互式解释器时(查找site.py
)。
- 否则,模块被定位。这方面的规则不是我们关心的。最后,这些规则到达 Python 文件或动态链接的机器代码片段(Windows 上的 .DLL,虽然 Python 模块专门使用扩展名 .pyd 但这只是一个名称;在 unix 上使用等效的 .so )。
- 模块首先加载到内存中(动态加载,或解析并编译为字节码)。
- 然后,模块被初始化。扩展模块对被调用的对象有一个特殊的功能。Python 模块只是从上到下运行。在表现良好的模块中,这只是设置全局数据、定义函数和类以及导入依赖项。当然,其他任何事情也可能发生。生成的模块对象被缓存(记住第一步)并返回。
所有这些都适用于标准库模块以及第三方模块。这也是为什么如果您调用您的脚本就像您在该脚本中导入的标准库模块一样调用您的脚本(它会自行导入,尽管不会由于缓存而崩溃 - 这是我忽略的许多事情之一),您可能会收到一条令人困惑的错误消息。
字节码的执行方式(问题的最后一部分)不同。CPython 只是简单地解释它,但正如您正确指出的那样,这并不意味着它神奇地不使用 CPU。取而代之的是一个大而丑陋的循环,它检测接下来应该执行什么字节码指令,然后跳转到一些执行该指令语义的本机代码。PyPy 更有趣;它开始解释但沿途记录一些统计数据。当它认为值得这样做时,它开始详细记录解释器所做的事情,并生成一些高度优化的本机代码。解释器仍然用于 Python 代码的其他部分。请注意,它与许多 JVM 和可能的 .NET 相同,但您引用的图表掩盖了这一点。