个人总结难免疏漏,请多包涵。更多内容请查看原文。本文以及学习笔记系列仅用于个人学习、研究交流。
本来计划中秋发布几篇文章,结果阳了,发烧、头疼、咽疼,修养了近一周,还没好完。希望大家都能有个好身体,工作、学习都是其次,祝大家安康。
本文是作为第五部分(模块)的结尾,拓展了模块相关的话题、陷阱,主要介绍了数据隐藏、__future__模块、__name__变量、sys.path修改、列表工具、通过名称字符串来运行模块、过渡式重载等,以及列举了模块相关的常见错误(主要是from导入),从而在代码中避免发生类似的错误。大家视需要读取。
目录
高级模块话题
在模块中隐藏数据
最小化from*的破坏:_X和__all__
启用以后的语言特性
混合用法模式:__name__和__main__
以__name__进行单元测试
使用带有__name__的命令行参数
修改模块搜索路径
Import语句和from语句的as扩展
模块是对象:元程序
用名称字符串导入模块
过渡性模块重载
模块设计理念
模块陷阱
顶层代码的语句次序的重要性
from复制变量名,而不是连接
from*会让变量语义模糊
reload不会影响from导入
reload、from以及交互模式测试
递归形式的from导入无法工作
高级模块话题
将构建一些比目前所见到的要更大和更有用的工具,它们组合了函数和模块。和函数一样,当模块接口定义良好时,它们要更有效率一些。
尽管标题有“高级”二字﹐是因为混合了一些额外的模块话题。这里所讨论的有些话题(例如,__name__技巧)都得到了广泛使用
在模块中隐藏数据
Python模块会导出其文件顶层所赋值的所有变量名。没有对某一个变量名进行声明,使其在模块内可见或不可见这种概念。实际上,如果客户想的话,是没有防止客户端修改模块内变量名的办法的。
在Python中,模块内的数据隐藏是一种惯例,而不是一种语法约束。的确可以通过破坏模块名称使这个模块不能工作。有些纯粹主义者对Python资料隐藏采取的这种开放态度不以为然,并宣称这表明Python无法实现封装。然而,Python的封装更像是打包,而不是约束。
最小化from*的破坏:_X和__all__
有种特定的情况,把下划线放在变量名前面(例如,_X),可以防止客户端使from*语句导入模块名时,把其中的那些变量名复制出去。这其实是为了对命名空间的破坏最小化而已。因为from*会把所有变量名复制出去,导入者可能得到超出它所需的部分(包括会覆盖导入者内的变量名的变量名)。下划线不是“私有”声明:还是可以使用其他导入形式看见并修改这类变量名。例如,使用import语句。
此外,也可以在模块顶层把变量名的字符串列表赋值给变量__all__,以达到类似于_X命名惯例的隐藏效果。例如:
使用此功能时,from*语句只会把列在__all__列表中的这些变量名复制出来。事实上,这和_X惯例相反:__all__是指出要复制的变量名,而_X是指出不被复制的变量名。Python会先寻找模块内的__all__列表;如果没有定义的话,from*就会复制出开头没有单下划线的所有变量名。
就像_X惯例一样,__all__列表只对from*语句这种形式有效,它并不是私有声明。模块编写者可以使用任何一种技巧实现模块,在碰上from*时,能良好地运行。
启用以后的语言特性
可能破坏现有代码语言方面的变动会不断引进。一开始,是以选用扩展功能的方式出现,默认是关闭的。要开启这类扩展功能,可以使用像以下形式的特定的import语句:
这个语句一般应该出现在模块文件的顶端(也许在docstring之后),因为这是以每个模块为基础,开启特殊的代码编译。此外,在交互模式提示符下提交这个语句也是可以的,从而能够实验今后的语言变化。于是,接下来的交互会话过程中,就可以使用这些功能了。
这种特定的导入形式,将作为可选的功能,逐步接受。
混合用法模式:__name__和__main__
这是一个特殊的与模块相关的技巧,可把文件作为成模块导入,并以独立式程序的形式运行。每个模块都有个名为__name__的内置属性,Python会自动设置该属性:
- ·如果文件是以顶层程序文件执行,在启动时,__name__就会设置为字符串"__main__"。
- ·如果文件被导入,__name__就会改设成客户端所了解的模块名。
结果就是模块可以检测自己的__name__,来确定它是在执行还是在导入。例如,假设我们建立下面的模块文件,名为runme.py,它只导出了一个名为tester的函数。
这个模块定义了一个函数,让用户可以正常地导入并使用:
然而,这个模块也在末尾包含了当此文件以程序执行时,就会调用该函数的代码:
实际上,一个模块的__name__变量充当一个使用模式标志,允许它编写成一个可导入的库和一个顶层脚本。将会看到这一钩子几乎在可能遇到的每个Python程序文件中应用。
也许使用__name__测试最常见的就是自我测试代码。简而言之,可以在文件末尾加个__name__测试,把测试模块导出的程序代码放在模块中。如此一来,你可以继续导入,在客户端使用该文件,而且可以通过检测其逻辑在系统shell中(或其他启动方式)运行它。实际上,在文件末端的__name__测试中的自我测试程序代码,可能是Python中最常见并且是最简单的单元测试协议。
编写既可以作为命令行工具也可以作为工具库使用的文件时,__name__技巧也很好用。
例如,假设用Python编写了一个文件寻找脚本。如果将其打包成一些函数,而且在文件中加入__name__测试,当此文件独立执行时,就自动调用这些函数,这样就能提高代码的利用效率。如此一来,脚本的代码就可以在其他程序中再利用了。
以__name__进行单元测试
看下面的脚本,从一组传进来的参数中计算出其最小值。
这个脚本在末端包含了自我测试程序代码。所以不用每次执行时,都得在交互模式命令行中重新输入所有代码就可以进行测试。然而,这种写法的问题在于,每次这个文件被另一个文件作为工具导入时,都会出现调用自我测试所得到的输出:这可不是用户友好的特性!
改进之后,在__name__检查区块内封装了自我测试的调用,使其在文件作为顶层脚本执行时才会启动,而导入时则不会。
也在顶端打印__name__的值,目的是来跟踪它的值。Python开始加载文件时,就创建了这个用法模式的变量并对其赋值。当以顶层脚本执行这个文件的时候,它的名称就会设置为__main__,所以,它的自我测试程序代码会自动执行。
但是,如果我们导入这个文件,其名称不是__main__,就必须明确地调用这个函数来执行。
同样地,无论这是否用于测试,结果都是让代码有两种不同的角色:作为工具的库模块,或者是作为可执行的程序。
使用带有__name__的命令行参数
这里是一个更为真实的模块示例,它展示了通常使用__name__技巧的另一种方式。如下的模块formats.py,为导入者定义了字符串格式化工具,还检查其名称看它是否作为一个顶层脚本在运行;如果是这样的话,它测试并使用系统命令行上列出的参数来运行一个定制的或传入的测试。
在Python中,sys.argv列表包含了命令行参数,它是反映在命令行上录入的单词的一个字符串列表,其中,第一项总是将要运行的脚本的名称:
当直接运行时,它像前面那样测试自己,但是它使用命令行上的选项来控制测试行为。可以自己直接运行这个文件而不带命令行参数,看看它的自测试代码打印出什么。要测试特定的字符串,用一个最小的字段宽度将它们传入到命令行上:
和前面一样,由于这段代码针对双模式用法编写,一般也可以把这些工具作为库的部分导入到其他的环境中:
这个文件使用了第15章介绍的文档字符串功能,也可以使用help函数来研究其工具——它充当一个通用目的的工具:
可以用类似的方式来使用命令行参数,为脚本提供通用的输入。这些脚本可能也会把自己的代码包装成函数和类以供导入者重用。想了解更高级的命令行处理,请参阅Python的标准库和手册中的getopt和optparse模块。
修改模块搜索路径
之前已经介绍过,模块搜索路径是一个目录列表,可以通过环境变量PYTHONPATH以及可能的.pth路径文件进行定制。还没有介绍的是,实际上Python程序本身是如何修改搜索路径的,也就是修改名为sys.path(内置模块sys的path属性)的内置列表。sys.path在程序启动时就会进行初始化,但在那之后,可以随意对其元素进行删除、附加和重设。
一旦做了这类修改,就会对Python程序中将要导入的地方产生影响,因为所有导入和文件都共享了同一个sys.path列表。事实上,这个列表可以任意修改
因此,可以使用这个技巧,在Python程序中动态配置搜索路径。不过,要小心:如果从路径中删除重要目录,就无法获取一些关键的工具了。
例如,上一个例子中,从路径中删除Python的源代码库目录的话,就再也无法获取string模块。
此外,记住sys.path的设置方法只在修改的Python会话或程序(即进程)中才会存续。在Python结束后,不会保留下来。PYTHONPATH和.pth文件路径配置是保存在操作系统中,而不是执行中的Python程序。因此使用这种配置方法更全局一些:机器上的每个程序都会去查找PATHONPATH和.pth,而且在程序结束后,它们还存在着。
Import语句和from语句的as扩展
import和from语句都可以扩展,让模块可以在脚本中给予不同的变量名。下面的import语句:
相当于:
在这类import之后,可以(事实上是必须)使用列在as之后的变量名来引用该模块。from语句也可以这么用,把从某个文件导入的变量名,赋值给脚本中的不同的变量名:
这个扩展功能很常用,替变量名较长的变量提供简短一些的同义词,而且当已在脚本中使用一个变量名使得执行普通import语句会被覆盖时,使用as,就可避免变量名冲突。
在之前所提到的包导入功能时,也可为整个目录路径提供简短、简单的名称,十分方便。
模块是对象:元程序
因为模块通过内置属性显示了它们的大多数有趣的特性,因此,可很容易地编写程序来管理其他程序。通常称这类管理程序为元程序(metaprogram),因为它们是在其他系统之上工作。这也称为内省(introspection),因为程序能看见和处理对象的内部。内省是高级功能,但是,它可以用做创建程序的工具。
例如,要取得M模块内名为name的属性,可以使用结合点号运算,或者对模块的属性字典进行索引运算(在内置__dict__属性中显示)。Python也在sys.modules字典中导出所有已加载的模块的列表(也就是sys模块的modules属性),并提供一个内置函数getattr,让我们以字符串名来取出属性(就好像是object.attr,而attr是运行时的字符串)。
因此,下列所有表达式都会得到相同的属性和对象。
通过像这样揭示了模块的内部机制,Python可帮助你建立关于程序的程序。
为函数可以像这里一样通过sys.modules表来获得它所在模块,所以,模拟global语句的效果是有可能的。例如,global X。X=0的效果可在函数内这样写以进行模拟(只不过要输入比较多的字):import sys;glob=sys.modules[__name__];glob.X=0。记住,每个模块都可取得__name__属性,在模块内的函数中,这是可见的全局变量。这个技巧还有另一种方式,可以修改函数内同名的局部变量和全局变量。
例如,以下是名为mydir.py的模块,运用这些概念,可以实现定制版本的内置函数dir。它定义并导出了一个名为listing的函数,这个函数以模块对象为参数,打印该模块命名空间的格式化列表。
注意顶端的文档字符串,就像在前面的formats.py示例中一样,因为我们可能想要将其用作一个通用的工具,编写一个文档字符串来提供通过__doc__属性或help函数可以访问的功能性信息(可以参考第15章)。
在这个模块的最后也提供了自测试逻辑,它导入并列出自己。
要使用这一工具来列出其他的模块,直接把模块作为对象传入到这个文件的函数中。
这里,它列出了标准库中的tkinter GUI模块中的属性
稍后会再遇见getattr以及与它作用相似的操作。重点就在于mydir是一个可以浏览其他程序的程序。因为Python能够展现其内部,通常可以像这样处理各种对象。
像mydir.listing这类工具可以由PYTHONSTARTIP环境变量所引用的文件进行导入,预先在交互模式的命名空间中加载。因为在启动文件内的程序代码会在交互模式命名空间内(模块__main__)执行,在启动文件内导入常用工具,可以节省一些输入。
用名称字符串导入模块
一条import或from语句中的模块名是直接编写的变量名称。然而,有时候,我们的程序可以在运行时以一个字符串的形式获取要导入的模块的名称(例如,如果一个用户从一个GUI中选择一个模块名称)。
遗憾的是,无法使用import语句来直接载入以字符串形式给出其名称的一个模块,Python期待一个变量名称,而不是字符串。例如:
直接把该字符串赋给一个变量名称也是无效的:
这里,Python将会尝试导入一个文件x.py,而不是string模块——一条import语句中的名称既变成了赋给载入的模块的一个变量,也从字面上标识了该外部文件。
需要使用特殊的工具,从运行时生成的一个字符串来动态地载入一个模块。最通用的方法是,把一条导入语句构建为Python代码的一个字符串,并且将其传递给exec内置函数以运行。
exec函数(及其近亲eval)编译一个代码字符串,并且将其传递给Python解释器以执行。在Python中,字节代码编译器在运行时可以使用,因此,我们像这样编写构建和运行其他程序的程序。默认情况下,exec运行当前作用域中的代码,但是,你可以通过传入可选的命名空间字典来更加具体地应用。
exec唯一的、真正的缺点是,每次运行时它必须编译import语句,如果它运行多次,如果使用内置的__import__函数来从一个名称字符串载入的话,代码可能会运行得更快。效果是类似的,但是,__import__运行模块对象,因此,在这里将其赋给一个名称以保存它:
过渡性模块重载
学习了模块重载,这是选择代码中的修改而不需要停止或重新启动一个程序的一种方式。当重载一个模块时,Python只重新载入特殊模块的文件,它不会自动重载那些为了导入要重载文件的模块。
例如,如果要重载某个模块A,并且A导入模块B和C,重载只适用于A,而不适用于B和C。A中导入B和C的语句在重载的时候重新运行,但是,它们只是获取已经载入的B和C模块对象(假设它们之前已经导入了)。在实际的代码中,文件A.py如下:
默认情况下,这意味着不能依赖于重载来过渡性地选择程序中的所有模块中的修改;相反,必须使用多次reload调用来独立地更新子部分。对于交互测试的大系统而言,工作量很大。你可以通过在A这样的父模块中添加reload调用,从而设计自己的系统能够自动重载它们的子部分,但是,这会使模块的代码变复杂。
一种更好的办法是,编写一个通用的工具来自动进行过渡性重载,通过扫描模块的__dict__属性并检查每一项的type以找到要重新载入的嵌套模块。这样的一个工具函数应该递归地调用自己,来导航任意形式的导入依赖性链条。模块__dict__属性在前面已介绍过,并且第9章介绍过type调用,只需要把两种工具组合起来。
例如,下面列出的模块reloadall.py有一个reload_all函数来自动地重载一个模块,以及该模块导入的每个模块等,所有通往每个导入链条最底端的通路都被考虑到。它使用字典来记录已经重载的模块,递归地遍历导入链条,以及标准库的types模块,该模块直接为内置类型预定义type结果。访问字典的技术在这里用来在导入是递归或冗余的时候避免循环,因为模块对象可以是字典键
要使用这一工具,导入其reload_all函数并将一个已经载入的模块的名称传递给它。当文件独立地运行,其自测试代码将会测试自己,它必须导入自己,因为它自己的名字并没有在没有一个导入的文件中定义。
如下是这个模块对于Python 3.0下的某些标准库模块工作的情况。注意,o s是如何由tkinter导入的,但tkinter在os之前已经导入了sys
如下的会话展示了常规重载和过渡性重载的对比效果——除非使用过渡性工具,否则重载不会选取对两个嵌套的文件的修改:
要更深入地了解,自己研究并体验这个示例,它是你可能想要添加到自己的源代码库中的另一个可导入工具。
模块设计理念
就像函数一样,模块也有设计方面的折中考虑:需要思考哪些函数要放进模块、模块通信机制等。当开始编写较大的Python系统时,这些就会变得明朗起来。但是要记住以下是一些通用的概念。
- ·总是在Python的模块内编写代码。
没有办法写出不在某个模块之内的程序代码。事实上,在交互模式提示符下输入的程序代码,其实是存在于内置模块__main__之内。交互模式提示符独特之处就在于程序是执行后就立刻丢弃,以及表达式结果是自动打印的。
- ·模块耦合要降到最低:全局变量。
就像函数一样,如果编写成闭合的盒子,模块运行得最好。原则就是,模块应该尽可能和其他模块的全局变量无关,除了与从模块导入的函数和类。
- ·最大化模块的黏合性:统一目标。
可以通过最大化模块的黏合性来最小化模块的耦合性。如果模块的所有元素都享有共同的目的,就不太可能依赖外部的变量名。
- ·模块应该少去修改其他模块的变量。
在第17章中以代码做过说明,但值得在这里重复:使用另一个模块定义的全局变量,这完全是可以的(毕竟这就是客户端导入服务的方式),但是,修改另一个模块内的全局变量,通常是出现设计问题的征兆。当然,也有些例外,但是应该试着通过函数参数返回值这类机制去传递结果,而不是进行跨模块的修改。否则,全局变量的值会变成依赖于其他文件内的任意远程赋值语句的顺序,而模块会变得难以理解和再利用。
总之,图24-1描绘了模块操作的环境。模块包含变量、函数、类以及其他的模块(如果导入了的话)。函数有自己的本地变量。在第25章会介绍类(模块中的另一种对象)。
图 24-1 模块的执行环境。模块是被导入的,但模块也会导入和使用其他模块,这些模块可以用Python或其他语言(例如,C语言)写成。模块可内含变量、函数以及类来进行其工作,而函数和类可包含变量和其他元素。不过,从最顶端来看,程序也只是一个模块的集合而已
模块陷阱
下面介绍常见的极端案例。有些很罕见,很难举例说明,但大多数都示范了语言中重要的部分。
顶层代码的语句次序的重要性
当模块首次导入(或重载)时,Python会从头到尾执行语句。这里有些和前向引用(forward reference)相关的含义,值得在此强调:
- ·在导入时,模块文件顶层的程序代码(不在函数内)一旦Python运行到时,就会立刻执行。因此,该语句是无法引用文件后面位置赋值的变量名。
- ·位于函数主体内的代码直到函数被调用后才会运行。因为函数内的变量名在函数实际执行前都不会解析,通常可以引用文件内任意地方的变量。
一般来说,前向引用只对立即执行的顶层模块代码有影响,函数可以任意引用变量名。以下是示范前向引用的例子。
当这个文件导入时(或者作为独立程序运行时),Python会从头到尾运行它的语句。对func1的首次调用失败,因为func1 def尚未执行。只要func1调用时,func2的def已运行过,在func1内对func2的调用就没有问题(当第二个顶层func1调用执行时,fun2的def还没有运行)。文件末尾最后对func1的调用可以工作,因为func1和func2都已经赋值了。
在顶层程序内混用def不仅难读,也造成了对语句顺序的依赖性。作为一条原则,如果需要把立即执行的代码和def一起混用,就要把def放在文件前面,把顶层代码放在后面。这样的话,你的函数在使用的代码运行的时候,可以保证它们都已定义并赋值过了。
from复制变量名,而不是连接
尽管常用,但from语句也是Python中各种潜在陷阱的源头。from语句其实是在导入者的作用域内对变量名的赋值语句,也就是变量名拷贝运算,而不是变量名的别名机制。它的实现和Python所有赋值运算都一样,但是其微妙之处在于,共享对象的代码存在于不同的文件中。
例如,假设定义了下列模块(nested1.py)。
如果在另一个模块内(nested2.py)使用from导入两个变量名,就会得到两个变量名的拷贝,而不是对两个变量名的连接。在导入者内修改变量名,只会重设该变量名在本地作用域版本的绑定值,而不是nested1.py中的变量名:
如果使用import获得了整个模块,然后赋值某个点号运算的变量名,就会修改nested1.py中的变量名。点号运算把Python定向到了模块对象内的变量名,而不是导入者的变量名(nested3.py)。
from*会让变量语义模糊
之前提到过这些内容,但把细节留在这里描述。使用from module import*语句形式时,因为你不会列出想要的变量,可能会意外覆盖了作用域内已使用的变量名。更糟的是,这将很难确认变量来自何处。如果有一个以上的被导入文件使用了from*形式,就更是如此了。
例如,如果在三个模块上使用from*,没有办法知道简单的函数调用真正含义,除非去搜索这三个外部的模块文件(三个可能都在其他目录内)。
解决办法就是不要这么做:试着在from语句中明确列出想要的属性,而且限制在每个文件中最多只有一个被导入的模块使用from*这种形式。如此一来,任何未定义的变量名一定可以减少到某一个from*所代表的模块。如果你总是使用import而不是from,就可完全避开这个问题,但这样的建议过于苛刻。就像其他大多数程序设计中,如果合理使用的话,from也是一种很方便的工具。
reload不会影响from导入
这是另一个和from相关的陷阱:正如前边讨论过的那样,因为from在执行时会复制(赋值)变量名,所以不会连接到变量名的那个模块。通过from导入的变量名就简单地变成了对象的引用,当from运行时这个对象恰巧在被导入者内由相同的变量名引用。
正是由于这种行为,重载被导入者对于使用from导入模块变量名的客户端没有影响。也就是说,客户端的变量名依然引用了通过from取出的原始对象,即使之后原始模块中的变量名进行了重新设置:
为了保证重载更有效,可以使用import以及点号运算,来取代from。因为点号运算总是会回到模块,这样就会找到模块重载后变量名的新的绑定值。
reload、from以及交互模式测试
之前第三章提过,通常情况下,最好不要通过导入或重载来启动程序,因为其中牵涉到了许多复杂的问题。当引入from之后,事情就变得更糟了。Python初学者常常会遇到这里所提到的陷阱。在文本编辑窗口开启一个模块文件后,假设你启动一个交互模式会话,通过from加载并测试模块:
发现了一个bug,跳回编辑窗口,做了修改,并试着重载模块:
但是这样行不通:from语句赋值的是变量名function,而不是module。要在reload中引用模块,得先通过import至少将其加载一次:
然而,这样也无法运行:reload更新了模块对象,但是就像上一节的讨论,像function这样的变量名之前从模块复制出来,依然引用了旧的对象(在这个例子中,就是function的原始版本)。要确实获得新的function,必须在reload之后调用module.function,或者重新执行from:
现在,新版本的function终于可以执行了。
正如见到的那样,使用reload和from有些本质上的问题:不但得记住导入后要重载,还得记住在重载后重新执行from语句。
不应该对reload和from能完美地合作抱有幻想。最佳的原则就是不要将它们结合起来使用:使用reload和import,或者以其他方式启动程序,如第3章的建议(例如,使用IDLE中的"Run"/"Run Module"菜单选项、点击文件图标或者系统命令行)。
递归形式的from导入无法工作
把最诡异(值得庆幸的事,并且也是最罕见)的陷阱留到最后。因为导入会从头到尾执行一个文件的语句,使用相互导入的模块时,需要十分小心(称为递归导入)。因为一个模块内的语句在其导入另一个模块时不会全都执行,有些变量名可能还不存在。
如果使用import取出整个模块,这也许重要,也许不重要。模块的变量名在稍后使用点号运算,在获得值之前都不会读取。但是,如果使用from来取出特定的变量名,必须记住,只能读取在模块中已经赋值的变量名。
例如,考虑下列模块recur1和recur2。recur1给变量名X赋了值,然后在赋值变量名Y之前导入recur2。这时,recur2可以用import把recur1整个取出(recur1已经存在于Python的内部的模块表中了),但是,如果使用from,就只能看见变量名X。变量名Y是在导入recur1后赋值的,现在不存在,所以会产生错误。
当recur1的语句由recur2递归导入时,Python会避免重新执行(否则导入会让脚本变成死循环),但是,被recur2导入时,recur1的命名空间还不完整。
解决办法就是,不要在递归导入中使用from(真的,不要)。如果这么做,Python不会卡在死循环中,但是,程序又会依赖于模块中语句的顺序。
有两种方式可避开这个陷阱:
- ·小心设计,通常可以避免这种导入循环:最大化模块的聚合性,同时最小化模块间的耦合性,是一个很好的开始。
- ·如果无法完全断开循环,就要使用import和点号运算(而不是from),将模块变量名的读取放在后边,要么就是在函数中,或者在文件末尾附近去执行from(而不是在模块顶层),以延迟其执行。