个人总结难免疏漏,请多包涵。更多内容请查看原文。本文以及学习笔记系列仅用于个人学习、研究交流。
import操作和模块是Python之中程序架构的核心。本文主要介绍了模块、属性以及导入的基础知识,并探索了import语句的操作(搜索、可选编译、运行),以及模块搜索路径(主目录、PYTHONPATH目录、标准链接库目录、任何.pth文件的内容),全文较简单。
介绍了导入会在模块搜索路径上寻找指定的文件,将其编译成字节码,并执行其中的所有语句从而产生其内容。也介绍也学到如何配置搜索路径,以便于从主目录和标准库目录以外的其他目录进行导入,主要是通过对PYTHONPATH的设置来实现的。
目录
模块
这里开始深入学习Python模块,模块是最高级别的程序组织单元,它将程序代码和数据封装起来以便重用。
从实际的角度来看,模块往往对应于Python程序文件(或是用外部语言如C、Java或C#编写而成的扩展)。每一个文件都是一个模块,并且模块导入其他模块之后就可以使用导入模块定义的变量名。
模块可以由两个语句和一个重要的内置函数进行处理。
import
使客户端(导入者)以一个整体获取一个模块。
from
允许客户端从一个模块文件中获取特定的变量名。
imp.reload
在不中止Python程序的情况下,提供了一种重新载入模块文件代码的方法。
在之前的第三笔记介绍了模块文件的基础知识,并且之前也已经使用过这些知识。从本章开始扩展了核心的模块文件的概念,之后开始探索更高级的模块应用。
本章主要提供了一个模块文件在整个程序结构中扮演的角色的概览。将会学到reload、__name__和__all__属性、封装import、相对导入语法等。因为模块和类实际上就是一个重要的命名空间,也会在这里正式介绍关于命名空间的概念。
为什么使用模块
模块通过使用自包含的变量的包,也就是所谓的命名空间提供了将部件组织为系统的简单的方法。在一个模块文件的顶层定义的所有的变量名都成了被导入的模块对象的属性。
正如前一部分中见到的那样,导入给予了对模块的全局作用域中的变量名的读取权。也就是说,在模块导入时,模块文件的全局作用域变成了模块对象的命名空间。最后,Python的模块允许将独立的文件连接成一个更大的程序系统。
更确切地说,从抽象的视角来看,模块至少有三个角色,如下所示。
代码重用
模块可以在文件中永久保存代码。不像在Python交互提示模式输入的代码,当退出Python时,代码就会消失,而在模块文件中的代码是永久的。
可以按照需要任意次数地重新载入和重新运行模块。除了这一点之外,模块还是定义变量名的空间,被认作是属性,可以被多个外部的客户端引用。
系统命名空间的划分
模块还是在Python中最高级别的程序组织单元。从根本上讲,它们不过是变量名的软件包。模块将变量名封装进了自包含的软件包,这一点对避免变量名的冲突很有帮助。如果不精确导入文件的话,是不可能看到另一个文件中的变量名。
事实上,所有的一切都“存在于”模块文件中,执行的代码以及创建的对象都毫无疑问地封装在模块之中。正是由于这一点,模块是组织系统组件的天然的工具。
实现共享服务和数据
从操作的角度来看,模块对实现跨系统共享的组件是很方便的,而且只需要一个拷贝即可。例如,如果你需要一个全局对象,这个对象会被一个以上的函数或文件使用,你可以将它编写在一个模块中以便能够被多个客户端导入。
为了真正理解Python系统中模块的角色,需要偏离主题来探索Python程序的通用结构。
Python程序架构
在此之前的一些Python程序的复杂性,都对之进行了简化。实际上,程序通常都不仅仅涉及一个文件,除了最简单的脚本之外,程序都会采用多文件系统的形式。而且即使能够自己编写单个文件,几乎一定会使用到其他人已经写好的外部文件。
这一部分介绍了通用的Python程序的架构:这种架构是将一个程序分割为源代码文件的集合以及将这些部分连接在一起的方法。在这个过程中,也将会探索Python模块、导入以及对象属性的一些核心概念。
如何组织一个程序
一般来讲,一个Python程序包括了多个含有Python语句的文本文件。程序是作为一个主体的、顶层的文件来构造的,配合有零个或多个支持的文件,在Python中这些文件称作模块。
在Python中,顶层文件(又称为脚本)包含了程序的主要的控制流程:这就是你需要运行来启动应用的文件。
模块文件就是工具的库,这些工具是用来收集顶层文件(或者其他可能的地方)使用的组件。顶层文件使用了在模块文件中定义的工具,而这些模块使用了其他模块所定义的工具。
模块文件通常在运行时不需直接做任何事。然而,它们定义的工具会在其他文件中使用。在Python中,一个文件导入了一个模块来获得这个模块定义的工具的访问权,这些工具被认作是这个模块的属性(也就是说,附加类似于函数这样的对象上的变量名)。
总而言之,我们导入了模块、获取它的属性并使用它的工具。
导入和属性
下面进行详细的介绍。
图21-1是一个包含有三个文件的Python程序的草图:a.py、b.py和c.py。
文件a是顶层文件,它是一个含有语句的简单文本文件,在运行时这些语句将会从上至下执行。文件b.py和c.py是模块,它们也是含有语句的简单文本文件,但是它们通常并不是直接运行。就像之前解释的那样,取而代之的是,模块通常是被其他的文件导入的,这些文件想要使用这些模块所定义的工具。
图 21-1 Python的程序架构。
一个程序是一个模块的系统。它有一个顶层脚本文件(启动后可运行程序)以及多个模块文件(用来导入工具库)。脚本和模块都是包含了Python语句的文本文件,尽管在模块中的语句通常都是创建之后使用的对象。Python的标准库提供了一系列的预先编写好的模块
例如,图21-1中的文件b.py定义了一个名为spam的函数,供外部来使用。b.py包含一个Python的def语句来生成函数,这个函数将会在之后通过给函数名后的括号中传入零个或更多的值来运行。
现在,假设a.py想要使用spam。为了实现这个目标,a.py中也许就要包含如下这样的Python语句。
首先,一个Python import语句,给文件a.py提供了由文件b.py在顶层所定义的所有对象的访问权限。概括来讲,这也就是意味着“载入文件b.py(除非它已经被载入了),并能够通过变量名b获取它的所有的属性”。import(以及之后会见到的from)语句会在运行时并载入其他的文件。
在Python中,交叉文件的模块连接在导入语句执行时才会进行解析。实际效果就是,将模块名(简单地认为是变量名)赋值给载入的模块对象。事实上,在一个导入语句中的模块名起到两个作用:识别加载的外部文档,但是它也会变成赋值给被载入模块的变量。模块定义的对象也会在执行时创建,就在import执行时;import会一次运行在目标文档中的语句从而建立其中的内容。
a.py中的第二行语句调用了模块b中所定义的函数spam,使用了对象属性语法。代码b.spam指的是“取出存储对象b中变量名为spam的值。”在这个例子中,碰巧是个可调用的函数,所以,在小括号内传入字符串('gumby')。如果实际输入这些文件,保存之后,执行a.py,"gumby spam"这些字符串就会打印出来。
在Python脚本中随处见到object.attribute这类表示法:多数对象都有一些可用的属性,可以通过“.”运算符取出。有些是可调用的对象。例如,函数,而其他的则是简单数据数值,给予对象特定的属性(例如,一个人的名称)。
导入的概念在Python之中贯穿始末。任何文件都能从任何其他文件中导入其工具。例如,文件a.py可以导入b.py从而调用其函数,但b.py也可能导入c.py以利用在其中定义了的不同工具。导入链要多深就有多深:在这个例子中,模块a可导入b,而b可导入c,c可再导入b,诸如此类。
除了作为最高级别的组织结构外,模块(以及模块包第23笔记介绍)也是Python中程序代码重用的最高层次。在模块文件中编写组件,可让原先的程序以及其他可能编写的任何程序得以使用。例如,编写图21-1中的程序后,我们发现函数b.spam是通用的工具,可在完全不同的程序中再次使用。我们所需要做的,就是从其他程序文件中再次导入文件b.py。
标准库模块
注意图21-1最右侧位置。程序导入的模块是由Python自身提供的,而并非是程序员所编写的文件。
Python自带了很多实用的模块,称为标准链接库。这个集合体大约有200个模块,包含与平台不相关的常见程序设计任务:操作系统接口、对象永久保存、文字模式匹配、网络和Internet脚本、GUI建构等。这些工具都不是Python语言的组成部分,但是,你可以在任何安装了标准Python的情况下,导入适当的模块来使用。
因为这些都是标准库模块,可以理所当然地认为它们一定可以用,而且在执行Python的绝大多数平台上都可运行。本书例子中会看到一些标准库模块的运用,但是就整体而言应该查看Python标准库参考手册,这份手册在Python安装后就可以看到(通过Windows上的IDLE或"Python Start"按钮),或者也可以使用http://www.python.org的在线版本。
import如何工作
上一节谈到了导入模块,然而并没有实际解释当这么做时都发生了什么。
因为导入是Python中程序结构的重点所在,深入讨论导入这个操作,让这个流程尽量不再那么抽象。
有些C程序设计者喜欢把Python的模块导入操作比作C语言中的#include,但其实不应该这么比较:在Python中,导入并非只是把一个文件文本插入另一个文件而已。导入其实是运行时的运算,程序第一次导入指定文件时,会执行三个步骤。
- 找到模块文件。
- 编译成位码(需要时)。
- 执行模块的代码来创建其所定义的对象。
要对模块导入有更好的理解,将会逐一探索这些步骤。记住,这三个步骤只在程序执行时,模块第一次导入时才会进行。在这之后,导入相同模块时,会跳过这三个步骤,而只提取内存中已加载的模块对象。
从技术上讲,Python把载入的模块存储到一个名为sys.modules的表中,并在一次导入操作的开始检查该表。如果模块不存在,将会启动一个三个步骤的过程。
1.搜索
首先,Python必须查找到import语句所引用的模块文件。
注意:上一节例子中的import语句所使用的文件名中没有a.py,也没有目录路径,只有import b,而不是import c:\dir1\b.py。
事实上,只能列出简单名称。路径和后缀是刻意省略掉的,因为Python使用了标准模块搜索路径来找出import语句所对应的模块文件。
因为这是程序员对于import操作所必须了解的主要部分,下面仔细研究这个步骤。
在标准的import中引入路径和后缀名等细节,从语法上讲是非法的。后面遇到的包的导入,依然依赖普通模块搜索路径,来找出包路径最左侧的目录(也就是相对于搜索路径中的目录)。包导入也不能在import语句中使用平台特定的目录语法;这类语法只能使用在搜索路径上。此外,当冻结可执行文件时,就不关心模块文件搜索路径的问题了,因为这种可执行文件通常是把字节码嵌入到二进制代码中。
2.编译(可选)
遍历模块搜索路径,找到符合import语句的源代码文件后,如果必要的话,Python接下来会将其编译成字节码。
Python会检查文件的时间戳,如果发现字节码文件比源代码文件旧(例如,如果你修改过源文件),就会在程序运行时自动重新生成字节代码。另一方面,如果发现.pyc字节码文件不比对应的.py源代码文件旧,就会跳过源代码到字节码的编译步骤。
此外,如果Python在搜索路径上只发现了字节码文件,而没有源代码,就会直接加载字节码(这意味着你可以把一个程序只作为字节码文件发布,而避免发送源代码)。换句话说,如果有可能使程序的启动提速,就会跳过编译步骤。
注意:当文件导入时,就会进行编译。因此,通常不会看见程序顶层文件的.pyc字节码文件,除非这个文件也被其他文件导入:只有被导入的文件才会在机器上留下.pyc。顶层文件的字节码是在内部使用后就丢弃了;被导入文件的字节码则保存在文件中从而可以提高之后导入的速度。
顶层文件通常是设计成直接执行,而不是被导入的。稍后将会看到,设计一个文件,使其作为程序的顶层文件,并同时扮演被导入的模块工具的角色也是有可能的。这类文件既能执行也能导入,因此,的确会产生.pyc。要了解其运作方式,可参考第24笔记关于特定的__name__属性以及__main__的讨论。
3.运行
import操作的最后步骤是执行模块的字节码。文件中所有语句会依次执行,从头至尾,而此步骤中任何对变量名的赋值运算,都会产生所得到的模块文件的属性。因此,这个执行步骤会生成模块代码所定义的所有工具。
例如,文件中的def语句会在导入时执行,来创建函数,并将模块内的属性赋值给那些函数。之后,函数就能被程序中这个文件的导入者来调用。
因为最后的导入步骤实际上是执行文件的程序代码,如果模块文件中任何顶层代码确实做了什么实际的工作,你就会在导入时看见其结果。例如,当一个模块导入时,该模块内顶层的print语句就会显示其输出。函数的def语句只是简单地定义了稍后使用的对象。
正如所见到的,import操作包括了不少的操作:搜索文件、或许会运行一个编译器以及执行Python代码。
因此,任何给定的模块在默认情况下每个进程中只会导入一次。未来的导入会跳过导入的这三个步骤,重用已加载内存内的模块。如果你在模块已加载后还需要再次导入(例如,为了支持终端用户的定制),就得通过调用reload强制处理这个问题。
Python已经导入的模块保存在一个内置的sys.modules字典中,以便可以记录哪些已经导入了。实际上,如果想要看看已经导入了哪些模块,可以导入sys并打印list(sys.modules.keys())。
模块搜索路径
正如前面所提到的,通常对程序员来说,导入过程的最重要的部分是定位要导入的文件(搜索部分)。因为我们需要告诉Python到何处去找到要导入的文件,需要知道如何输入其搜索路径以扩展它。
在大多数情况下,可以依赖模块导入搜索路径的自动特性,完全不需要配置这些路径。不过,如果你想在用户间定义目录边界来导入文件,就需要知道搜索路径是如何运作的,并予以调整。
概括地讲,Python的模块搜索路径是这些主要组件组合而成的结果。其中有些进行了预先定义,而其中有些你可以进行调整来告诉Python去哪里搜索。
- 程序的主目录。
- PYTHONPATH目录(如果已经进行了设置)。
- 标准链接库目录。
- 任何.pth文件的内容(如果存在的话)。
最后,这四个组件组合起来就变成了sys.path,它是下一部分详细介绍的目录名称字符串的列表。
搜索路径的第一和第三元素是自动定义的,但是因为Python会从头到尾搜索这些组件组合的结果,第二和第四元素,就可以用于拓展路径,从而包含你自己的源代码目录。
以下是Python使用路径组件的方式。
主目录
Python首先会在主目录内搜索导入的文件。这一入口的含义与你如何运行代码相关。当你运行一个程序的时候,这个入口是包含程序的顶层脚本文件的目录。当在交互模式下工作时,这一入口就是你当前工作的目录。
因为这个目录总是先被搜索,如果程序完全位于单一目录,所有导入都会自动工作,而并不需要配置路径。另一方面,由于这个目录是先搜索的,其文件也将覆盖路径上的其他目录中具有同样名称的模块。如果你需要在自己的程序中使用库模块的话,小心不要以这种方式意外地隐藏库模块。
PYTHONPATH目录
之后,Python会从左至右(假设你的设置了的话)搜索PYTHONPATH环境变量设置中罗列出的所有目录。简而言之,PYTHONPATH是设置包含Python程序文件的目录的列表,这些目录可以是用户定义的或平台特定的目录名。你可以把想导入的目录都加进来,而Python会使用你的设置来扩展模块搜索的路径。
因为Python会先搜索主目录,当导入的文件跨目录时,这个设置才显得格外重要。也就是说,如果你需要被导入的文件与进行导入的文件处在不同目录时。一旦你开始编写大量程序时,你可能想要设置PYTHONPATH变量,但是刚开始编写时,只要把所有模块文件放在交互模式所使用的目录下就行了(也就是说,主目录),如此一来,导入都可运作,而你不需要去担心如何进行这个设置。
标准库目录
接着,Python会自动搜索标准库模块安装在机器上的那些目录。因为这些一定会被搜索,通常是不需要添加到PYTHONPATH之中或包含到路径文件(接下来介绍)中的。
.pth文件目录
最后,Python允许用户把有效的目录添加到模块搜索路径中去,也就是在后缀名为.pth(路径的意思)的文本文件中一行一行地列出目录。这些路径配置文件是和安装相关的高级功能。
简而言之,当内含目录名称的文本文件放在适当目录中时,也可以概括地扮演与PYTHONPATH环境变量设置相同的角色。例如,如果你在运行Windows和Python 3.0,一个名为myconfig.pth的文件可以放在Python安装目录的顶层(C:\Python30)或者在标准库所在位置的sitepackages子目录中(C:\Python30\Lib\sitepackages),来扩展模块搜索路径。在类似Unix的系统中,文件可能位于usr/local/lib/python3.0/site-packages或/usr/local/lib/site-python中。
当存在目录的时候,Python会把文件每行所罗列的目录从头至尾加到模块搜索路径列表的最后。实际上,Python将收集它所找到的所有路径文件中的目录名,并且过滤掉任何重复的和不存在的目录。因为这些是文件,而不是shell设置值,路径文件可以适用于所安装系统的所有用户,而并非仅限于一个用户或者一个shell。此外,某些用户文本文件可能比环境设置更容易编码。
这个特性比这里介绍的更为复杂。相关细节,可参考Python标准库手册,尤其是标准库模块网站的说明文档。这个模块允许配置Python库和路径文件的位置,并且其文档描述了一般的期待位置。建议初学者使用PYTHONPATH或单个的.pth文件,并且只有进行跨目录的导入时才使用。路径文件作为第三方库经常使用,它通常在Python的site-packages目录安装一个路径文件,从而不需要用户设置。
配置搜索路径
所有这些的直接效果是,搜索路径的PYTHONPATH和路径文件部分允许我们调整导入查找文件的地方。设置环境变量的方法以及存储路径文件的位置,随着每种平台而变化。例如,在Windows上,我们可能使用控制面板的系统图标来把PYTHONPATH设置为分号隔开的一串目录:
或者,可以创建一个名为C:\Python30\pydirs.pth的文本文件,其内容如下所示:
这些设置在其他平台上也是类似的,但是细节可能有很大变化,无法在这里一一介绍。参考附录A有关PYTHONPATH或.pth文件在各种平台上扩展模块搜索路径的常见方式。
搜索路径的变动
这里对模块搜索路径的说明只算一般性说明,搜索路径的配置可能随平台以及Python版本而异。取决于你所使用的平台,附加的目录也可能自动加入模块搜索路径。
例如,Python可能会把当前的工作目录也加进来(也就是启动程序所在的目录),放在搜索路径PYTHONPATH之后,并在标准库这项之前。在从命令行启动时,当前工作目录和顶层文件的主目录(也就是程序文件所在的目录)不一定相同。因为每次程序执行时,当前工作目录可能都会变,一般来说,不应该依赖这个值进行导入。
默认情况下,在Python 3.0中,一个包的目录不会被导入自动搜索,除非包自身中的文件使用了相对导入。
想知道Python在平台上配置的模块搜索路径,可以查看sys.path
sys.path列表
如果你想看模块搜索路径在机器上的实际配置,可以通过打印内置的sys.path列表(也就是标准库模块sys的path属性)来查看这个路径,就好像Python知道一样。目录名称字符串列表就是Python内部实际的搜索路径。导入时,Python会由左至右搜索这个列表中的每个目录。
其实,sys.path是模块搜索的路径。Python在程序启动时进行配置,自动将顶级文件的主目录(或者指定当前工作目录的一个空字符串)、任何PYTHONPATH目录、已经创建的任何.pth文件路径的内容,以及标准库目录合并。结果是Python在每次导入一个新文件的时候查找的目录名的字符串的列表。
Python出于两种合理的理由来描述这个列表。首先,提供一种方式来确认你所做的搜索路径的设置值:如果在列表中看不到设置值,就需要重新检查你的设置。
例如,如下是我的模块搜索路径在Windows的Python 3.0中的样子,其中我的PYTHONPATH设置为C:\users并列出了C:\users\mark的一个C:\Python30\mypath.py路径文件。前面的空字符串表示当前路径,并且我的两个设置都合并了进去(其余的部分是标准库目录和文件):
其次,如果你知道在做什么,这个列表也提供一种方式,让脚本手动调整其搜索路径。后面就会知道,通过修改sys.path这个列表,你可以修改将来的导入的搜索路径。然而,这种修改只会在脚本存在期间保持而已。PYTHONPATH和.pth文件提供了更持久的路径修改方法。
模块文件选择
记住,文件名的后缀(例如,.py)是刻意从import语句中省略的。Python会选择在搜索路径中第一个符合导入文件名的文件。
例如,import b形式的import叙述可能会加载。
- ·源代码文件b.py。
- ·字节码文件b.pyc。
- ·目录b,包导入。
- ·编译扩展模块(通常用C或C++编写),导入时使用动态连接(例如,Linux的b.so以及Cygwin和Windows的b.dll或b.pyd)。
- ·用C编写的编译好的内置模块,并通过静态连接至Python。
- ·ZIP文件组件,导入时会自动解压缩。
- ·内存内映像,对于frozen可执行文件。
- ·Java类,在Jython版本的Python中。
- ·NET组件,在IronPython版本的Python中。
C扩展、Jython以及包导入,都不仅仅是简单文件的导入机制的延伸。不过,对导入者来说,完全忽略了需要加载的文件类型之间的差异,无论是在导入时或者是在读取模块的属性时都是这样。
例如,import b就是读取模块b,根据模块搜索路径,b是什么就是什么,而b.attr则是取出模块中的一个元素,可能是Python变量或连接的C函数。这里所用的某些标准模块实际上是用C编写的而不是Python。正是因为这种透明度,客户端并不在乎文件是什么。
如果在不同目录中有b.py和b.so,Python总是在由左至右搜索sys.path时,加载模块搜索路径那些目录中最先出现(最左边的)的相符文件。但是,如果是在相同目录中找到b.py和b.so,会发生什么事情呢?在这种情况下,Python遵循一个标准的挑选顺序,不过这种顺序不保证永远保持不变。通常来说,不应该依赖Python会在给定的目录中选择何种的文件类型:让模块名独特一些,或者设置模块搜索路径,让模块选择的特性更明显一些。
高级的模块选择概念
一般来说,导入工作起来就像这一部分所介绍的那样:在机器上搜索并载入文件。然而,重新定义Python中import操作所做的事也是可能的,也就是使用所谓的导入钩子(import hook)。这些钩子可以让导入做各种有用的事,例如,从归档中加载文件,执行解密等。
事实上,Python使用这些钩子让文件可直接从ZIP文件库中导入:当选择一个在搜索路径中的.zip文件后,归档后的文件会在导入时自动解压缩。更多细节,可以参考Python标准库手册中关于内置的__import__函数的说明,这个函数是实际执行的import语句的可定制的工具。
Python也支持最佳化字节码文件.pyo,这种文件在创建和执行时要加上-O这个Python标志位。因为这些文件执行时会比普通的.pyc文件快一点(一般快5%),然而,它们并没有频繁的使用。Psyco系统提供更实质性的加速效果。
第三方工具:distutils
以上对模块搜索路径设置的说明,主要是针对自己编写的用户定义的源代码。
Python的第三方扩展,通常使用标准链接库中的distutils工具来自动安装,所以不需要路径设置,就能使用它们的代码。
使用distutils的系统一般都附带setup.py脚本,执行这个脚本可以进行程序的安装。这个脚本会导入并使用distutils模块,将这种系统放在属于模块自动搜索路径一部分的目录内(通常是在Python安装目录树下的Lib\site-packages子目录中,而不管Python安装到了目标机器的哪一部分)。
更多关于distutils分发和安装的细节,可参考Python标准手册集。例如,此工具还提供一些方式,可在目标机器上自动编译C所编写的扩展。此外,参考最近涌现出的第三方开源eggs系统,它增加了对已安装的Python软件的依存关系的检查。
小结
本文介绍了模块、属性以及导入的基础知识,并探索了import语句的操作。也学到,导入会在模块搜索路径上寻找指定的文件,将其编译成字节码,并执行其中的所有语句从而产生其内容。我们也学到如何配置搜索路径,以便于从主目录和标准库目录以外的其他目录进行导入,主要是通过对PYTHONPATH的设置来实现的。
正如本文所展示过的,import操作和模块是Python之中程序架构的核心。较大程序可分成几个文件,利用导入在运行时连接在一起。而导入会使用模块搜索路径来寻找文件,并且模块定义了属性,供外部使用。
当然,导入和模块就是为程序提供结构,让程序将其逻辑分割成一些独立完备的软件组件。一个模块中的程序代码和另一个的程序代码彼此隔离。事实上,没有文件可以看到另一个文件中定义的变量名,除非明确地运行import语句。因此,模块最小化了程序内不同部分之间的变量名冲突。