模块¶
Python 如此强大和受欢迎很大一部分是依赖于它丰富而强大的模块。
模块与 import 语句¶
任何 Python 源文件都能以模块的形式使用。例如:
# spam.py
a = 37
def foo():
print("I'm foo and a is %s" % a)
def bar():
print("I'm bar and I'm calling foo")
foo()
class Spam(object):
def grok(self):
print("I'm Spam.grok")
要以模块的形式加载这段代码,可以使用 import spam 语句。首次使用 import 加载模块时,它将做 3 件事。
创建新的命名空间,用作在相应源文件中定义的所有对象的容器。在模块中定义的函数和方法在使用 global 语句时将访问该命名空间。
在新创建的命名空间中执行模块中包含的代码。
在调用函数中创建名称来引用模块命名空间。这个名称与模块的名称相匹配,按如下方式使用:
import spam # 加载并执行模块spam
x = spam.a # 访问模块spam的一个成员
spam.foo() # 调用模块spam中的一个函数
s = spam.Spam() # 创建spam.Spam()的一个实例
s.grok()
...
需要强调的是,import 执行已加载的源文件中的所有语句。如果模块执行计算或生成了除定义变量、函数和类以外的输出,就可以看到生成的这些结果。另外,使用模块时一个常见的疑难点就是对类的访问。 请记住,如果文件 spam.py 定义了类 Spam,必须使用名称 spam.Spam 来引用该类。
要导入多个模块,可以为import提供逗号分隔的模块名称列表,例如:
import socket, os, re
用于引用模块的名称可以使用 as 限定符进行更改,例如:
import spam as sp
import socket as net
sp.foo()
sp.bar()
net.gethostname()
像这样使用其他的名称加载模块时,新名称仅应用于出现了 import 语句的源文件或上下文。 其他程序模块仍然可以使用模块原来的名称加载它。
更改已导入模块的名称对于编写可扩展的代码很有用。例如,设想有两个模块 xmlreader.py 和 csvreader.py, 它们都定义了函数 read_data(filename),作用是从文件中读取一些数据,但采用不同的输入格式。 可以编写代码来选择性地挑选读取模块,例如:
if format == 'xml':
import xmlreader as reader
elif format == 'csv':
import csvreader as reader
data = reader.read_data(filename)
模块是 Python中 的第一类对象。这意味着它们可以被分配给变量,放置在列表等数据结构中,以及以数据的形式在程序中传递。
例如,上一个例子中的 reader 变量只用于引用相应的模块对象。在后台,模块对象是位于字典之上的一层,这个字典用于持有模块命名空间的内容。
这个字典以模块的 __dict__ 形式使用,并且只要查找或更改模块中的值,就会对该字典进行相应操作。
import 语句可以出现在程序中的任何位置。但是,每个模块中的代码仅加载和执行一次,无论 import 语句被使用了多少次。 后续的 import 语句仅将模块名称绑定到前一次导入所创建的模块对象。你可以在变量 sys.modules 中找到一个字典,其中包含了所有当前加载的模块。该字典将模块名称映射到模块对象。该字典的内容用于确定 import 是否加载模块的全新拷贝。
从模块导入选定符号¶
from 语句用于将模块中的具体定义加载到当前命名空间中。from 语句相当于 import,但它不会创建一个名称来引用新创建的模块命名空间,而是将对模块中定义的一个或多个对象的引用放到当前命名空间中:
from spam import foo # 导入spam并将foo放在当前命名空间中
foo() # 调用spam.foo()
spam.foo() # NameError: spam
from 语句还接受用逗号分隔的对象名称列表。例如:
from spam import foo, bar
如果要导入一个极长的名称列表,可以将名称放在括号中。这样可以轻松地将 import 语句放在多行上,例如:
from spam import (foo,
bar,
Spam)
另外,as 限定符可用于重命名使用 from 导入的具体对象,例如:
from spam import Spam as Sp
s = Sp()
星号 * 通配符也可用于加载模块中的所有定义,但以下划线开头的定义除外,例如:
from spam import * # 将所有定义加载到当前命名空间中
from module import * 语句只能在模块最顶层使用。具体来讲,在函数体内使用这种导入形式是不合法的,原因在于这种导入语句与函数作用域规则之间具有独特的交互方式(例如,将函数编译为内部字节码时,函数中使用的所有符号都需要明确指定)。
通过定义列表 __all__ ,模块可以精确控制 from module import * 导入的名称集合,例如:
# 模块:spam.py
__all__ = [ 'bar', 'Spam' ] # 将使用from spam import *导出的名称
使用 from 导入形式导入定义不会更改定义的作用域规则。例如,考虑以下代码:
from spam import foo
a = 42
foo() # 打印"I'm foo and a is 37"
在这个例子中,spam.py 中 foo() 的定义引用了全局变量 a。在不同命名空间中引用 foo 时,并不会更改该函数中的变量绑定规则。 因此,函数的全局命名空间始终是定义该函数的模块,而不是将函数导入并调用该函数的命名空间。这也适用于函数调用。 例如,在以下代码中,对 bar() 的调用会导致调用 spam.foo(),而不是上一个代码示例中重新定义的 foo():
from spam import bar
def foo():
print("I'm a different foo")
bar() # 当bar调用foo()时,它将调用spam.foo(),而不是
# 上面的foo()定义
对于 from 导入形式,另一个容易混淆的地方是全局变量的行为。例如,考虑以下代码:
from spam import a, foo # 导入全局变量
a = 42 # 修改该变量
foo() # 打印"I'm foo and a is 37"
print(a) # 打印"42"
这里需要理解的一点是,Python 中的变量赋值不是一种存储操作。也就是说,上例中对 a 的赋值不会将新值存储在 a 中并覆盖以前的值。 而是将创建包含值 42 的新对象,并用名称 a 来引用它。此时,a 不再绑定到导入模块中的值,而是绑定到其他对象。 因此,使用 from 语句没办法让导入的变量模仿全局变量和 C 或 Fortran 等语言中通用块的行为。如果需要在程序中使用可变的全局程序参数,可以将这些参数放在模块中并通过 import 语句显式使用模块名称(也就是显式使用 spam.a)。
以主程序的形式执行¶
Python 源文件可以通过两种方式执行。import 语句在自己的命名空间中以库模块的形式执行代码。 但是,代码也可以主程序或脚本的形式执行。将程序作为脚本名称提供给解释器时,就是这样执行的:
% python spam.py
每个模块会定义一个包含模块名称的变量 __name__ 。程序可以检查该变量,以确定它们在哪个模块中执行。
解释器的顶层模块名为 __main__ 。在命令行中指定或交互式输入的程序将在 __main__ 模块中运行。
有时,程序可能改变其行为,这取决于程序是以模块的形式导入还是在 __main__ 中运行。
例如,模块可能包含一些测试代码,如果模块以主程序的形式执行,将执行这些测试代码,如果模块只是由另一个模块导入,则不会执行测试代码。
可以通过以下方式实现这一功能:
# 检查模块是否以程序的形式运行
if __name__ == '__main__':
# 是
statements
else:
# 否,我必须以模块的形式导入
statements
对于计划用作库的源文件, 使用该技巧来包含可选的测试或示例代码是一种常见的做法。例如,如果你正在开发一个模块,可以将测试库功能 的代码放在如上所示的 if 语句中,并以主程序的形式运行模块。用户在导入你的库时,测试代码并不会运行。
模块搜索路径¶
加载模块时,解释器会搜索 sys.path 中的目录列表。sys.path 中的第一个条目通常是一个空字符串’’,表示当前正在使用的目录。
sys.path 中的其他条目可能包含目录名称、 .zip 压缩文件和 .egg 文件。各个条目在 sys.path 中排列的顺序决定了加载模块时的搜索顺序。要将新条目添加到搜索路径中,只需将它们添加到该列表中即可。
尽管该路径通常包含目录名称,也可以将包含 Python 模块的 zip 压缩文件添加到搜索路径中。通过这种方式,可以方便地将一组模块打包为一个文件。例如,假设你创建了两个模块 foo.py 和 bar.py,并将它们放在一个名为 mymodules.zip 的 zip 文件中。就可以按如下方式将这个文件添加到 Python 搜索路径中:
import sys
sys.path.append("mymodules.zip")
import foo, bar
也可以使用 zip 文件目录结构中的具体位置。另外,zip 文件可以与常规路径名称混合使用,例如:
sys.path.append("/tmp/modules.zip/lib/python")
除了 .zip 文件,还可以在搜索路径中添加 .egg 文件。.``egg`` 文件是由 setuptools 库创建的包。这是安装第三方 Python 库和扩展时会碰到的一种常见格式。
.egg 文件实际上只是添加了额外的元数据(如版本号、依赖关系等)的 .zip 文件。因此,可以使用处理 .zip 文件的标准工具来从 .egg 文件中检查和提取数据。
尽管支持 zip 文件导入,还是有一些限制需要注意。首先,只能从压缩文件中导入 .py 、 .pyw 、 .pyc 和 .pyo 文件。
使用 C 编写的共享库和扩展模块无法直接从压缩文件中加载,尽管 setuptools 等打包系统有时能够提供一种变通方案(通常将 C 扩展提取到一个临时目录并从该目录加载模块)。
而且,从压缩文件加载 .py 文件时,Python 不会创建 .pyc 和 .pyo 文件(稍后将介绍)。因此,一定要确保提前创建了这些文件,并将其放在归档文件中,以避免在加载模块时性能下降。
模块加载和编译¶
到目前为止,本章将模块描述为包含纯 Python 代码的文件。但是,使用 import 加载的模块实际上可分为 4 个通用类别:
使用Python编写的代码(
.py文件);已被编译为共享库或DLL的C 或 C++ 扩展;
包含一组模块的包;
使用 C 编写并链接到 Python 解释器的内置模块。
查找模块(如 foo)时,解释器在 sys.path 中的每个目录下搜索以下文件(按搜索顺序列出):
目录 foo,它定义了一个包
foo.pyd、foo.so、foomodule.so 或 foomodule.dll(已编译的扩展)
foo.pyo(只适用于使用了 -O 或 -OO 选项时)
foo.pyc
foo.py(在 Windows 上,Python 还会查找 .pyw 文件。)
对于 .py 文件,首次导入模块时,它会被编译为字节码并作为 .pyc 文件写回磁盘。
在后续的导入操作中,解释器将加载这段预编译的字节码,除非 .py 文件有最新的修改(在这种情况下,将重新生成 .pyc 文件)。
.pyo 文件与解释器的 -O 选项结合使用。这些文件包含已删除了行号、断言和其他调试信息的字节码。
因此,这些文件会相对更小,解释器的运行速度也会稍快一些。如果指定了 -OO 选项,而不是 -O,那么还会从文件中删除文档字符串。文档字符串只会在创建 .pyo 文件时删除,而不是在加载它们的时候。
如果 sys.path 中的所有目录下都不存在这些文件,解释器将检查该名称是否为内置的模块名称。如果不存在匹配的名称,将引发 importError 异常。
只有使用 import 语句才能将文件自动编译为 .pyc 和 .pyo 文件。在命令行或标准输入中指定的程序不会生成这类文件。另外,如果包含模块的 .py 文件的目录不允许写入(例如,可能是由于权限不够或者该文件包含在一个 zip 归档文件中),将不会创建这些文件。
解释器的 -B 选项也可以禁止生成这些文件。
如果存在 .pyc 和 .pyo 文件,则可以没有相应的 .py 文件。因此,如果在打包代码时不希望包含源文件,可以只打包一组 .pyc 文件。但是请注意,Python 提供了对内省(introspection)和分解的广泛支持。即使没有提供源文件,细心的用户仍然可以检查并发现程序的大量细节。还请注意, .pyc 文件特定于具体的 Python 版本。因此,为某个 Python 版本生成的 .pyc 文件可能不适用于未来的 Python 版本。
import 语句搜索文件时,文件名匹配是区分大小写的,即使机器上的底层文件系统不区分大小写也是如此,如 Windows 和 OS X(不过这些系统会保留名称的大小写形式)。 所以,import foo 将只导入文件 foo.py,不会导入文件 FOO.PY。但是,作为一般规则,应该避免使用仅大小写形式不同的模块名称。
模块重新加载和卸载¶
Python 实际上不支持重新加载或卸载以前导入的模块。尽管可以从 sys.modules 删除模块,但这种方法通常不会从内存中卸载模块。这是因为,对模块对象的引用可能仍然存在于使用 import 加载该模块的其他程序组件中。 而且,如果存在模块中定义类的实例,这些实例将包含对其类对象的引用,类对象进而会拥有对定义它的模块的引用。
由于模块引用存在于多个位置,因此在更改了模块实现之后再重新加载该模块通常行不通。例如,如果从 sys.modules 删除一个模块,然后使用 import 重新加载它,不会追溯性地更改程序中以前对该模块的引用。 相反,你将拥有由最新的 import 语句创建的对新模块的引用,以及由其他部分代码中的 import 语句创建的一组对旧模块的引用。这通常不是我们所期望的,而且在正常的生产代码中使用这种导入方式也不安全,除非你能谨慎控制整个执行环境。
最后,应该注意 Python 的 C/C++ 扩展无法以任何方式安全地卸载或重新加载。Python 没有提供对此操作的任何支持,而且底层操作系统也可能会禁止这么做。因此,唯一的解决办法是重新启动 Python 解释器进程。