系统学习Python——模块和包:import工作方式

之前我们谈到了导入模块,然而并没有解释当你这么做时会发生什么。因为导入是Python中程序结构的核心,所以现在要深入讨论导入这个操作,让这个流程尽量不再那么抽象。

有些C程序设计者喜欢把Python的模块导入操作比作C语言中的#include,但其实不应该这么比较:在Python中,导入并非只是把一个文件文本插入另一个文件。导入其实是运行时的操作,程序第一次导人指定文件时,会执行三个步骤:

  1. 找到模块文件
  2. 编译成字节码(如果需要的话)
  3. 执行模块的代码来创建其所定义的对象

要深入理解模块导入,我们将逐一探索这些步骤。这三个步骤只在程序执行期间模块第一次导入时才会进行。在这之后导人相同模块时,会跳过这三个步骤,而只提取内存中已加载的模块对象。事实上,Python把载入的模块存储到一个名为sys,modules的表中,并在每次导入操作的开始首先检查该表。如果模块不存在,则启动这个三个步骤的过程。

搜索

首先,Python必须查找到import语句所引用的模块文件。注意:上文例子中的import语句所使用的文件名中没有py的扩展名,也没有目录路径,只有import b,而不是import c:\dir1\b.py。路径和后缀是刻意省略掉的,因为Python使用了标准模块搜索路径来找出import语句所应的模块文件。因为这是程序员对于import操作所必须了解的主要部分,我们之后会再详细讨论这个话题。

编译(可选)

在遍历模块搜索路径找到符合import语句的源代码文件后,如果需要的话,Python接下来会将其编译成字节码。我们在之前的文章中讨论过字节码,但这里我们要更详细地讨论它。在导入操作发生时,Python会同时检查文件最近一次的修改时间和生成的字节码对应的Python版本号,从而决定接下来的行为。前者使用文件的“时间戳”,后者使用字节文件内嵌的“魔数”或是字节文件的文件名(取决于使用的Python版本)。这一步的两种选择分别为编译和不编译。

编译:如果发现字节码文件比源代码文件旧,例如修改过源文件,或者是由不同的Python版本编译的,就会在程序运行时自动重新生成字节代码。如前文所述,这一模型在Python
3.2及之后的版本中有相应改动一字节码文件被集中存放在__pycache__子目录中,并以编译它们的Python版本来命名,从而避免跨版本使用Python带来的竞争和重复编译。这种新模型的设计避免了在字节码文件中检查版本号,但是时间戳检查仍被保留下来用于检查源文件是否发生改变。
不编译:另一方面,如果发现.pyc字节码文件不比对应的.py源代码文件旧,而且是由同Python版本所编译的,那么Python就会跳过源代码到字节码的编译步骤。此外,如果Python在搜索路径上只发现了字节码文件,而没有源代码,就会直接加载字节码(这意味着你可以把一个程序只作为字节码文件发布,而避免发送源代码)。换句话说,如果不会影响程序的正常运行,就会跳过编译步骤从而加速程序启动。

注意当文件导人时,就会进行编译。因此,你通常不会看见程序顶层文件的.pyc字节码文件,除非这个文件也被其他文件导入:只有被导入的文件才会在机器上留下.pyc文件。顶层文件的字节码是在内部使用后就丢弃了的:被导入文件的字节码则保存在文件中从而可以提高之后导人的速度。

顶层文件通常是设计成直接执行,而不是被导入的。稍后我们会看到,将一个文件设计成程序的顶层文件,以及被导入的模块工具是可行的。这类文件既能执行也能被导人,因此会产生.pyc文件。

运行

import操作的最后步骤是执行模块的字节码。文件中所有语句会从头至尾依次执行,而此步骤中任何对名称的赋值运算,都会产生所得到的模块对象的属性。这就是创建模块代码所定义的工具的方式。例如,文件中的def语句会在导入时执行,来创建函数并将它们赋值给模块对象的属性名称。之后,函数就能被程序中这个文件的导入者调用了。

因为这最后的导入步骤实际上是执行被导入文件的程序代码,所以如果模块文件中的任何顶层代码做了什么实际的工作,那么你就会在导入时看见其结果。例如当一个模块导入时,该模块内顶层的print语句就会显示其输出。函数的def语句只是简单地定义了稍后使用的函数对象。

import操作包括了不少的操作:搜索文件、可能运行一个编译器以及执行Python代码。因此,任何给定的模块默认情况下在每个进程中只会导入一次。之后的导入会跳过导入的这三个步骤,并重用已加载到内存中的模块。如果你在模块已加载后还需要再次导入(例如,为了支持终端用户的动态定制),那么你就得通过调用imp.reload函数。

猜你喜欢

转载自blog.csdn.net/hy592070616/article/details/124993266
今日推荐