Osheep

时光不回头,当下最重要。

第五部分 模块

模块是最高级别的程序组织单元,它将程序代码和数据封装起来以便重用。

模块是变量名的封装,被认为是命名空间。模块导入时,模块文件的全局作用域变成了模块对象的命名空间。

在模块文件顶层定义的所有变量名都变成了模块对象的属性。

模块有三个角色:

  • 代码重用
  • 系统命名空间的划分
  • 实现共享服务和数据

1 模块导入

在导入语句中的模块名有两个作用:

  • 识别需要加载的外部文档
  • 赋值给被导入模块的变量

模块定义的对象会在import执行时创建。

导入时运行时的运算,程序第一次导入指定文件时,会执行以下三个步骤:

  • 找到模块文件
  • 编辑成字节码(如果需要)
  • 执行模块代码,创建其中定义的对象

这三个步骤只在程序执行时,模块第一次导入的时候执行。在这之后导入相同模块时会跳过这三个步骤,只是提取内存中已加载的模块对象。

Python把加载的模块存储到sys.modules中,并在第一次导入时检查该表。如果模块不存在,则执行这三个步骤。

1.1 搜索

Python的模块搜索路径由以下几部分组成:

  • 程序的主目录
  • PYTHONPATH环境变量的目录(如果设置了的话)
  • 标准链接库目录
  • 任何.pth路径文件中的内容(如果存在的话)

它们组合起来就是模块搜索路径sys.path。其中第一和第三项是自动定义的,第二和第四项可以拓展搜索路径。Python会从头到尾搜索它们的组合,也就是从左到右搜索sys.path中的目录。

1.1.1 主目录

Python首先在主目录搜索导入的文件。主目录的含义与如何运行代码相关。运行一个程序时,主目录是包含程序顶层脚本的目录;在交互式模式下,主目录是当前的工作目录。

因为Python总是优先搜索这个目录,所以它会覆盖其它目录中的同名模块,小心不要以这种方式覆盖标准库模块。

1.1.2 PYTHONPATH目录

之后,Python会从左到右搜索PYTHONPATH环境变量(如果设置了的话)中列出的目录。可以在该列表中包括所有想要导入的目录,从而扩展模块搜索路径。

只有导入的文件跨目录时,即被导入的文件与进行导入的文件不在同一个目录时,这个设置才显得格外重要。

1.1.3 标准库目录

接着,Python会自动搜索标准库模块所在的目录。这些目录一定会被搜索,所以不需要添加到PYTHONPATH或.pth路径文件中。

1.1.4 .pth文件目录

在后缀为.pth文件中一行一行列出目录,可以作为PYTHONPATH的一种替代方案。Python会把文件中每行罗列的目录从头到尾添加到模块搜索路径列表的最后。

Python可能会把当前工作目录也加进来,放在PYTHONPATH之后,标准库目录之前。从命令行启动程序时,当前工作目录和顶层文件的主目录(也就是程序文件所在的目录)不一定相同。每次执行程序时,当前工作目录可能会发生变化,所以不应该依赖这个值导入。

程序启动时,Python自动将顶层文件的主目录(或者指定当前工作目录的空字符串),所有PYTHONPATH目录,标准库目录,以及已经创建的.pth路径文件中的目录合并。

Python会选择搜索路径中第一个符合导入文件名的文件。import b语句可能会加载:

  • 源文件b.py
  • 字节码文件b.pyc
  • 目录b(包导入时)
  • 编译后的扩展模块(通常用C\C++编写),导入时使用动态链接(比如.so.dll,或.pyd
  • 用C编写的内置模块,并使用静态链接
  • ZIP文件组件,导入时自定解压
  • 内存中的映像(对于fronzen可执行文件)
  • Java类(Jython版本的Python中)
  • .NET组件(IronPython版本的Python中)

如果不同目录中有b.pyb.so两个文件,Python会从左到右搜索sys.path,并加载最先出现(最左边)的文件。如果同一个目录下有b.pyb.so,则不一定加载哪个文件。

1.2 编译

遍历模块搜索路径,并找到源代码文件后,如果有必要,Python会将其编译成字节码文件。

Python比较字节码文件和源文件的时间戳。如果字节码文件比源文件旧,则会在程序运行时自动生成字节码文件;否则会跳过编译步骤。

如果Python在搜索路径上只发现了字节码文件,而没有源代码文件,则会直接加载字节码文件。

当文件导入时会进行编译。只有被导入的文件才会在机器上留下.pyc字节码文件,从而提高之后的导入速度。顶层文件的字节码文件在内部使用后就丢弃了。

1.3 运行

import操作最后的步骤是执行模块的字节码。文件中的所有语句会被依次执行。在这个步骤中,对任何变量名的赋值运算都会产生模块文件的属性。

2 模块使用

在Python 3中,from *语句只能用于模块文件的顶层,而不能用于函数中。

importfrom都是隐性的赋值语句:

  • import将整个模块对象赋值给一个对象名
  • from将一个或多个变量名赋值给另一个模块中的同名对象

from复制的变量名会变成对共享对象的引用。与函数参数一样,对已取出的变量名重新赋值,不会影响其复制之处的模块;但是修改一个已取出的可变对象,则会影响导入模块内的对象。例如文件small.py

x = 1
y = [1, 2]

% python
>>> from small import x, y
>>> x = 42
>>> y[0] = 42

这里x并不是一个共享的可变对象,但y是。导入者中的变量名y和被导入者都引用了同一个列表对象。所以在其中一个地方修改,也会影响另一个地方的对象。

>>> import small
>>> small.x
1
>>> small.y
[42, 2]

from复制而来的变量名与其来源的文件之间没有联系。要实际修改另一个文件中的全局变量名,必须使用import

% python
>>> from small import x, y
>>> x = 42    # 只修改了本地的x

>>> import small
>>> small.x = 42   # 修改了另一个模块中的x

from的第一步也是普通的导入操作,它总是会把整个模块导入内容(如果还没有导入的话)。

3 模块命名空间

在模块文件顶层(而不是函数或者类的主体内)每一个赋值了的变量名都会变成该模块的属性。

模块的命名空间可以通过__dict__属性或dir(M)获取。

变量的含义一定是由源代码中的赋值语句的位置决定的。

4 包导入

Python首次导入某个目录时,会自动执行该目录下__init__.py文件中的所有代码。所以,可以在该文件内放置包内文件所需要的初始化代码。

可以在__init__.py文件中使用__all__列表,在其中定义目录以from *语句导入时需要导出的子模块的名称。如果没有设置__all__,则from *语句不会自动加载嵌套与该目录内的子模块。

4.1 包相对导入

Python 3中包导入的变化:

  1. 修改了模块导入时搜索路径的语义,它默认会跳过包自身的目录,只检查搜索路径的其它组件,这叫“绝对”导入。
  2. 扩展了from语法,允许显式地要求导入只搜索包自身的目录,这叫“相对”导入。

from语句使用点号导入位于同一个包中的模块(相对导入),而不是导入位于模块搜索路径上某处的模块(绝对导入):

  1. 只在包内搜索,不会搜索模块搜索路径上某处的同名文件。直接效果就是包模块覆盖了外部的模块。
  2. 在Python 3中,包代码的常规导入(前面不含点号)默认是绝对的——忽略包含包自身的目录,只在sys.path上的目录搜索。

相对导入适用于只在包内导入,并且只能用于from语句。

Python 3中的模块查找规则:

  1. 简单模块名(import A)通过搜索sys.path列表中的每个目录查找。
  2. 包是带有__init__.py文件的模块目录,它可以使用A.B.C语法。其中A目录位于sys.path搜索路径中。
  3. 在一个包内的模块文件中,常规的import语句使用sys.path搜索规则。如果包内的模块文件使用前面带点号的from语句时,则它相对于包导入。也就是说,只检查包目录,并不使用sys.path常规查找。

5 其它

5.1 最小化from *的破坏

在变量前加下划线,可以防止客户端使用from *语句导入模块时,把其中的变量名复制出去。也可以在模块顶层把变量名的字符串列表赋值给__all__变量。

5.2 用名称字符串导入模块

有两种方式可以用模块的字符串名称导入模块:

  1. 把一条导入语句构建为一个字符串,并将它传递给exec()内置函数.
  2. 使用内置函数__import__()
点赞