Python基础手册27——模块

一、模块

1、模块和导入

当程序代码量变得相当大、逻辑结构变得非常复杂的时候,我们最好把代码按照逻辑和功能划分成一些有组织的代码块,并将其保存到一个个独立的文件当中。这些文件可以包含可执行代码、函数、类或者这些东西的组合,这些自我包含并且有组织的代码块就是模块( module )。模块是最高级别的 Python 代码组织单元。

模块往往对应于物理机上的 Python 文件(或者是用外部语言如C、Java或C#编写而成的扩展)。当你创建了一个 Python 源文件,其对应的模块的名字就是不带 .py 后缀的文件名。一个模块(Python程序文件)创建之后, 你可以从另一个文件中使用 import 语句导入这个模块来使用,从而实现代码的重用。这个把其他模块附加到你的代码中的操作叫做导入( import )。导入其他模块之后就可以使用导入的模块中定义的变量名。


2、模块的作用

代码重用

因为模块对应于 Python 文件,所以模块中的代码可以永久保存。你可以按照需要在代码中任意次数的使用导入的模块中定义的变量名(函数、类等),甚至可以重新导入模块。除了作为最高级别的 Python 代码组织单元,模块(以及 模块包)也是 Python 中程序代码重用的最高层次。

系统命名空间的划分

模块还是定义变量名的空间,其内部定义的变量名作为模块的属性,可以通过导入被多个外部的文件中的代码引用。

模块将变量名封装进了自己的命名空间,这一点对避免变量名的冲突很有帮助。所有的一切都存在于 ”模块“ 中,可执行的代码以及创建的对象都毫无疑问的封装在模块之中。正式由于这一点,模块是组织系统组件的天然工具。

实现共享服务和数据

从操作的角度来看,模块对实现跨系统共享的组件是很方便的,只需要在不同的文件中导入相同的模块即可。


3、Python 的程序架构

一个 Python 程序通常都不仅仅涉及一个文件,一般都会采用多文件系统的形式。即使编写单个文件,几乎也一定会导入标准库模块或者使用到其他人已经写好的外部文件。

一般来讲一个完整的程序由启动运行的脚本文件以及零个或多个作为支持(用作导入)的文件组成。

在 Python 中,顶层文件包含了程序的主要的控制流程:这就是你需要运行来启动程序的文件。作为模块被导入的文件通常在运行时不需要直接做任何事,它提供了顶层文件运行所需要的各种组件(普通变量、函数、类等)。顶层文件使用了在模块文件中定义的组件,而这些模块使用了其他模块所定义的组件。

在 Python 中,一个文件导入了一个模块来获得这个模块中定义的变量的访问权,这些变量被认作是这个模块的属性。导入的概念在 Python 之中贯穿始末。任何文件都能从任何其他文件中导入其变量,导入链要多深就有多深。

4、标准库模块

Python 自带了很多实用的模块,称为标准链接库。这个集合体大约有200多个模块,包含与平台不相关(不依赖于具体的系统,可以在任何系统上以同样的方式调用,也就是说这些标准库模块是跨平台的)的常见程序设计任务:操作系统接口、对象永久保存、文字模式匹配、网络和 internet 脚本、GUI 建构等。

这些工具都不是 Python 语言的组成部分,但是,你可以在任何安装了 Python 的环境中,导入适当的模块来使用。因为这些都是标准库模,所以他们一定可以用,而且在执行 Python 的绝大数平台上都可以运行。


二、模块的导入

模块中的代码会在首次导入时执行,首先建立空的模块对象,然后按照从头到尾的顺序,逐一执行该模块内的语句。顶层(不在def或class之内)的赋值语句(例如,=、def 和 class等)生成的变量会成为模块对象的属性,这些变量名会存储在模块的命名空间内。模块的命名空间能通过属性 __dict__ 或内建函数 dir() 获取。

1、模块文件的命名

任何以 “.py” 为后缀名的 Python 文件都会被自动认为是 Python 模块,一般来说,Python 文件怎么命名都可以,但是如果打算将其作为模块导入,文件必须以 ”.py“ 结尾。
对于会执行但不会被导入的顶层文件而言,.py 后缀从技术上来说是可有可无的,但是每次都加上去,可以确保文件类型更醒目,并使其以后可以被导入到任何文件中。

因为模块名在 Python 程序中会变成变量名(没有.py)。因此Python文件应该遵循普通变量名的命名规则。事实上,包导入中所用的模块的文件名和目录名都必须遵循变量名规则。


2、导入模块的步骤

在Python中,导入并不是把一个文件文本插入另一个文件中。导入其实是运行时的运算,程序第一次导入指定文件时,会执行三个步骤。
1、搜索找到模块文件。
2、编译成字节码(需要时)。
3、执行模块的代码来创建其所定义的对象,定义 import 语句所在文件的作用域的局部命名空间中的一个或多个变量名。

这三个步骤只在模块第一次导入时才会执行。在这之后,导入相同模块时,会跳过这三个步骤,而只是提取内存中已加载的模块对象。这是有意而为之的,因为该操作开销较大。如果你在模块已加载后还需要再次导入(例如,为了支持终端用户的定制),你就得通过调用 reload() 强制导入模块。

从技术上讲,Python 把载入的模块存储到一个名为 sys.modules 的表中,并在导入操作的开始检查该表。如果模块不存在,将会自动执行上面的三个步骤。

搜索

Python 会遍历模块搜索路径,查找 import 语句所引用的模块文件。在导入者文件中,只能列出要导入的模块文件的简单名称,路径和后缀是刻意省略掉的。

当一个模块被导入时,Python 会把程序内部的模块名映射到外部物理环境中的文件名,也就是将模块搜索路径中的目录路径添加在模块名前边,并在模块名的后边添加 .py 或其他后缀名。

编译

找到模块文件后,Python 会查找对应的 .pyc 字节码文件。如果没有字节码文件,Python 会将模块文件编译成字节码文件。如果找到对应的字节码文件,Python 会检查文件的时间戳,如果发现字节码文件比模块文件旧(例如,如果你修改过源文件),就会重新编译模块文件生成新的字节码文件。如果字节码文件不比对应的 .py 源代码文件旧,就会跳过源代码到字节码的编译步骤。

如果 Python 在搜索路径上只发现了字节码文件,而没有源代码,就会直接加载字节码文件(这意味着你可以把一个程序只作为字节码文件发布,而避免发送源代码)。换句话说,直接使用字节码文件跳过编译步骤,会提高程序的启动提速。

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

顶层文件通常是设计成直接执行,而不是被导入的。

运行

import 操作的最后步骤是执行模块的字节码。文件中所有语句会从头到尾依次执行,而此步骤中任何对变量名的赋值运算,都会产生模块文件的属性。因此,这个执行步骤会生成模块代码所定义的所有工具。

因为最后的导入步骤实际上是执行文件的程序代码,如果模块文件中任何顶层代码确实做什么实际的工作,你就会在导入时看见其结果。


3、import 语句

常见的 import 导入语句可以分为两种:单独的 import 语句用来导入模块名;带有
from 的 import 语句用来导入模块中的变量名,同时可以使用 * 号导入模块中的所有变量。在以上两种语句中,我们都可以使用 as 语句为导入的模块或变量指定别名。当语句包含多个子句(以逗号分隔)时,为每个子句分别执行模块导入的三个步骤,就像子句已被分隔为单独的 import 语句一样。

如果导入的模块被成功检索到,它将通过以下三种方式之一绑定到本地命名空间:

  • 如果模块名后面是 as,则 as 之后的变量名将在本地命名空间中绑定为对导入的模块对象的引用。
  • 如果未指定其他名称,并且正在导入的模块是顶级模块(),则模块的名称将在本地命名空间中绑定为对导入模块对象的引用。
  • 如果正在导入的模块不是顶级模块,则包含该模块的顶级包的名称在本地命名空间中被绑定为对顶级包的引用。导入的模块必须使用其完全限定名称而不能直接访问。包的概念会在后续章节介绍。

from 形式会多一些复杂的过程:

  1. 找到 from 子句中指定的模块,如果需要,加载和初始化它;
  2. 对于 import 子句中指定的每个标识符:
    a. 检查导入的模块是否具有该名称的属性;
    b. 如果没有,请尝试导入具有该名称的子模块,然后再次检查导入的模块的该属性;
    c. 如果未找到该属性,则引发 ImportError;
    d. 如果找到该名称的属性,对该属性的引用存储在本地命名空间中,使用 as 子句中的名称(如果存在),否则使用属性名称;

如果在 from 语句中 import 后面的标识符列表被替换为星号(*),则模块中定义的所有公共名称都在 import 语句所在的作用域的本地命名空间中绑定。

(1)import 形式

import 语句将模块导入文件中:
import module_name

import 是可执行语句,就像 def 一样,它是隐性的赋值语句。当 Python 执行到这个语句时,会将导入生成的模块对象赋值给 import 语句后面的模块名,而模块文件顶层对任意类型赋值了的变量名,都会产生为模块对象的属性。

一旦导入完成,一个模块的属性(函数和变量)可以通过熟悉的 (. )句点属性标识法访问。
module.function()
module.variable

import 语句组合两个操作;它搜索指定的模块并根据需要执行模块以得到模块对象,然后将模块对象绑定到本地作用域中的模块名。

import 语句的搜索操作被定义为:使用适当的参数调用 __import__() 函数。直接调用 __import__() 只执行模块搜索,如果找到,则执行模块创建操作,并返回模块对象。如果找不到指定的模块,则会引发 ImportError。虽然可能会伴随着某些其他的操作,例如导入父包以及更新各种缓存(包括sys.modules),但只有 import 语句会执行名称绑定操作。

属性名的点号运算

在 Python 之中,可以使用点号运算语法 object.attribute 获取任意的 object 的attribute 属性。

点号运算符其实就是表达式,传回和对象相配的属性名的值。当使用点号运算符来读取变量名时,就把明确的对象提供给 Python , LEGB 规则只适用于无点号运算的纯变量名。

简单变量名

X 是指在当前作用域内搜索变量名 X(遵循LEGB规则)

点号运算

X,Y 是指在当前范围内搜索 X,然后搜索对象 X 之中的属性 Y(而非在作用域里)。

多层点号运算

X,Y,Z 指的是在当前范围内搜索 X,然后搜索对象 X 之中的属性 Y,然后在对象X.Y 中搜索属性 Z 。

通用性

点号运算可用于任何具有属性的对象:模块、类、C 扩展类型等。


(2)from - import 形式

使用 from-import 语句可以将模块的属性导入到当前作用域,并绑定到指定的变量名。
from module import name1[, name2[,... nameN]]

和 import 一样,from - import 语句也是可执行的隐性赋值语句。import 将导入的模块对象赋值给一个模块名。而 from - import 将模块中的一个或多个变量(也就是生成的模块对象的一个或多个属性)绑定到当前文件中 import 语句指定的变量名。因为 from 会把模块中定义的变量名复制到另一个文件的作用域中,所以它就可以让我们直接在另一个文件中直接使用从模块中导入的变量名,而不需要通过模块名。(例如:variate)

from 的第一步骤也是普通的导入操作。因此,from 总是会把整个模块导入到内存中(如果还没被导入的话),无论是从这个文件中复制出多少变量名。只加载模块文件的一部分(例如,一个函数)是不可能的。但是因为模块在 Python 之中是字节码而不是机器码,通常可以忽略效率的问题。

from 语句潜在的陷阱

因为 from 语句会让变量位置更隐秘和模糊,所以 form 语句可能会破坏命名空间。如果使用 from 导入变量,而那些变量碰巧和作用域中现有变量同名,变量就会被悄悄地覆盖掉。使用简单的 import 语句就不会有这种问题,因为你一定得通过模块名才能获取其属性(变量名)。不过使用 from 时,只要你了解并预料到可能发生这种事,在实际情况下这就不是一个大问题了,尤其当你明确列出导入的变量名时(例如,from moudle import a, b, c)。

和 reload 调用同时使用时,from 语句有比较严重的问题,因为导入的变量名可能引用之前导入的对象。

简单模块一般倾向于使用 import,而不是 from。多数的 from 语句是用于明确列举出想要的变量,而且限制在每个文件中只用一次 from * 形式。当你必须使用两个不同模块内定义的相同的变量名时,才真的必须使用 import,这种情况下不能用 from(当然你可以在 from 语句中使用 as 语句来个规避变量名冲突的问题)。


(3)from - import * 形式

从一个模块导入许多变量名时,import 行会越来越长,直到自动换行,而且我们需要使用反斜杠字符 \ 让一条语句横跨多行 。

from module import name1, name2, name3, name4,\
   name5, name6, name7

你可以选择使用多行的 from-import 语句:

from module import name1, name2, name3, name4
from module import name5, name6, name7

在 from 语句的 import 子句中,当我们使用 * 时,会取得模块顶层所有赋值的变量名的拷贝。从根本上来说,这就是把一个模块的命名空间融入另一个模块之中;同样地,实际效果就是可以让我们少输入一些代码。from * 语句形式只能用在一个模块文件的顶部,尝试在类或函数定义中使用它将引发 SyntaxError。


核心风格: 限制使用 " from - import * "

在实践中, 我们认为 "from - import *" 不是良好的编程风格,因为它"污染"当前名称空间,让变量名难以理解。而且很可能覆盖当前名称空间中现有的名字,尤其是在导入一个以上的模块时。事实上,from * 形式会把一个命名空间融入到另一个,所以会使得模块的命名空间的分割特性失效。

如果某个模块有很多要经常访问的变量或者模块的名字很长,这也不失为一个方便的好办法。我们只在两种场合下建议使用这样的方法,一个场合是:要使用的目标模块中的属性非常多,反复键入模块名很不方便,例如 Tkinter (Python/Tk) 和 NumPy (Numeric Python) 模块,可能还有 socket 模块。另一个场合是在交互解释器下,因为这样可以减少输入次数。

一般情况下,我们不提倡使用不再流行的 from module import * 语句 。真正的 Python 程序员应该使用 Python 的标准分组机制(圆括号)来创建更合理更明确的多行导入语句。

最小化 from * 的破坏:_x 和 __all__

把下划线放在变量名前面(例如,_x),可以防止客户端使用 from * 语句导入模块名时,把其中的那些变量名复制出去。这其实是为了对命名空间的破坏最小化而已。下划线不是私有变量的声明:你还是可以使用其他导入形式看见并修改这类变量名。

此外,你也可以在模块顶层把变量名的字符串列表赋值给变量名 __all__ ,以达到类似于 _x 命名惯例的隐藏效果。

使用此功能时,from * 语句只会把列在 __all__ 列表中的这些变量名赋值出来。事实上这和 x 惯例相反 __all时指出要复制的变量名,而_x 是指出不被复制的变量名。Python 会先寻找模块内的 __all_ 列表;如果没有定义的话,from * 就会复制出开头没有单下划线的所有变量名。

就像 _x 惯例一样,__all__ 列表只对 from * 语句这种形式有效,它并不是私有声明。


(4)扩展的导入语句(as)

有时候你导入的模块名或是模块属性名称已经在你的程序中使用了,或者你不想使用导入的名字,可能是它太长不便输入什么的。 这已经成为 Python 程序员的一个普遍需求:使用自己想要的名字替换模块的原始名称。使用扩展的 as 子句,你就可以在导入的同时指定局部绑定名称。

import 语句和 from 语句都可以扩展,让模块可以在脚本中给予不同的变量名。

import modulename as name
相当于:
import modulename
name = modulename
del modulename

from modulename import attrname as name
相当于:
from modulename import attrname
name = attrname
del attrname

这个扩展功能很常用,替代变量名较长的变量提供简短一些的同义词,而且当已在脚本中使用一个变量名使得执行普通 import 语句会被覆盖时,使用 as,就可避免变量名冲突。


4、模块重载

在同一个进程中模块只在第一次导入时,加载和执行该模块的代码。之后的导入只会使用已加载的模块对象,而不会重载或重新执行文件的代码。要强制使模块重新载入并重新运行,可以使用 reload() 函数。

reload()

reload() 函数位于Python中的 imp 模块内,使用前必须先导入。它会强制已加载的模块的代码重新载入并重新执行。因为 reload() 期望得到的是对象,在重载之前,模块一定是已经预先成功导入了。

重新执行模块文件的代码会覆盖其现有的命名空间。重载会影响所有使用 import 导入模块的程序,因为使用 import 的程序需要通过点号运算符取出属性,在重载后,使用的模块对象变成了新的值。重载只会对重载后使用 from 语句导入模块的程序造成影响。之前使用 from 来读取属性的客户端并不会受到重载的影响,那些程序引用的依然是重载前所取出的旧对象。

reload() 函数使得可以修改模块程序的一些代码,而无须停止整个程序。因此,利用reload() ,可以立即看到对模块的修改效果。重载无法用于每种情况,但是能用时,可缩短开发的流程。一般的用法是:导入一个模块,在文本编辑器内修改其源代码,然后将其重载。当调用 reload() 时,Python 会重读模块文件的源代码,重新执行其顶层语句。

因为 Python 是解释性的(或多或少),其实已经避免了类似 C 语言程序执行时所需的编译连接步骤:在执行程序导入时,模块会动态加载。重载进一步的提供了性能优势,让你可以修改执行中的程序的一部分,而不需要中止。注意:reload() 当前只能用在Python 编写的模块;用 C 这类语言编写的编译后的扩展模块也可在执行中动态加载,但无法重载。


《Python基础手册》系列:

Python基础手册 1 —— Python语言介绍
Python基础手册 2 —— Python 环境搭建(Linux)
Python基础手册 3 —— Python解释器
Python基础手册 4 —— 文本结构
Python基础手册 5 —— 标识符和关键字
Python基础手册 6 —— 操作符
Python基础手册 7 —— 内建函数
Python基础手册 8 —— Python对象
Python基础手册 9 —— 数字类型
Python基础手册10 —— 序列(字符串)
Python基础手册11 —— 序列(元组&列表)
Python基础手册12 —— 序列(类型操作)
Python基础手册13 —— 映射(字典)
Python基础手册14 —— 集合
Python基础手册15 —— 解析
Python基础手册16 —— 文件
Python基础手册17 —— 简单语句
Python基础手册18 —— 复合语句(流程控制语句)
Python基础手册19 —— 迭代器
Python基础手册20 —— 生成器
Python基础手册21 —— 函数的定义
Python基础手册22 —— 函数的参数
Python基础手册23 —— 函数的调用
Python基础手册24 —— 函数中变量的作用域
Python基础手册25 —— 装饰器
Python基础手册26 —— 错误 & 异常
Python基础手册27 —— 模块
Python基础手册28 —— 模块的高级概念
Python基础手册29 —— 包

推荐阅读更多精彩内容