Python实战指南:手把手教你掌握300个精彩案例
上QQ阅读APP看书,第一时间看更新

2.2 包

案例47 让普通目录变成包

导语

包(Package)实际上是一个目录(就好比模块实际上是一个代码文件一样)。当某个目录下存在一个名为__init__.py的代码文件时,Python就会将该目录视为包。

包的作用是对模块进行分类。包与模块的关系,如同目录与文件的关系,如果代码模块数量较多,而且都放到根目录(根目录一般是相对的路径)下,会显得混乱,既不利于管理,也不利于识别。通过包对模块进行分类存放,会使应用程序的功能划分更加细化,结构更加清晰。

操作流程

步骤1:新建目录,命名为packages。

步骤2:在packages目录下新建代码文件,命名为__init__.py。__init__.py文件中无须编写任何代码,只要目录下存在此文件,该目录就会被识别为包。

步骤3:在packages目录下新建代码文件,命名为demo.py(模块名为demo),作为packages包的子模块。然后在demo模块中定义show函数。

步骤4:包是可以作为模块来导入的,导入时,__init__.py文件中的代码会被执行(如果__init__.py文件是空白的,则没有代码被执行)。下面代码将包作为模块来导入。

步骤5:还可以从包中导入指定的子模块。

步骤6:也可以使用from…import语句来导入子模块。

步骤7:或者从子模块中导入指定的成员。

访问子模块的方式与访问文件类似,写上子模块所在包的名称,每个目录层次之间使用点号(.)分隔。

案例48 __init__.py文件

导语

目录中包含__init__.py文件时,Python就会将该目录视为包,而且__init__.py文件可以留空白。当包初始化时会执行__init__.py文件(例如当包被导入时)。

__init__.py虽然有特殊用途,但其本质也是代码文件,因此该文件中可以放置任意可执行的代码,也可以定义变量、函数、类等对象。当包作为模块导入时,还可以通过__init__.py文件来合并子模块中的成员(即把子模块的成员导入__init__.py文件中)。

操作流程

步骤1:新建目录,命名为my_pkg,然后在目录下新建代码文件__init__.py,使my_pkg目录成为包目录。

步骤2:在__init__.py文件中调用print函数,输出字符串。当my_pkg包初始化时会执行此代码。

步骤3:再在__init__.py文件中定义两个函数。

__name__属性返回test_f1和test_f2函数的名称,并以字符串形式呈现。

步骤4:在需要访问my_pkg包的代码中导入刚刚定义的两个函数。

步骤5:现在可以直接调用这两个函数了。

调用后,屏幕上会打印以下文本:

案例49 合并子模块的成员列表

导语

由于包是一个目录,因此它下面既可以包含代码模块(子模块),也可以包含子目录(子包),子包下还可以包含代码模块,就类似于常见的文件夹与文件的关系。

如果编写的代码结构很复杂(目录层次很多),其他人在调用时会感觉到吃力,还需花时间和精力去弄清楚代码结构(有时候即便撰写了帮助文档也无法将代码结构描述清楚),而且有些代码可能是为了实现某些功能而编写的,只用于内部实现,代码的调用者是不需要关注的。

为了解决上述问题,让代码调用者能够很方便地访问相关的成员对象,可以在包下面的__init__.py文件中将子模块的成员进行合并,统一公开。__init__.py文件本质上也是代码模块,所以可以在该文件中定义新的变量来引用子模块的成员,也可以使用__all__变量。

操作流程

步骤1:新建目录my_lib。

步骤2:在my_lib目录下新建__init__.py文件,使该目录成为包目录。

步骤3:在my_lib目录下新建mod1.py文件,模块名为mod1。并在mod1模块中定义两个函数。

_add函数进行加法运算,_sub函数用于减法运算。

步骤4:再在my_lib目录下新建文件mod2.py,即mod2模块,并在模块中定义两个函数。

_mult函数用于乘法运算,_div函数用于除法运算。

步骤5:回到my_lib目录下的__init__.py文件,依次导入mod1和mod2模块中的成员。

在mod1和mod2模块名称前面加上点号(.),表示相对路径,表示当前包目录下的子模块。如果是两个点,例如..mod,表示当前包目录的上一层目录中的mod模块;要是有三个点,例如...mod,则表示当前包目录的上两层目录下的子模块……以此类推。

步骤6:定义新的变量,分别引用_add,_sub,_mult和_div四个函数。

mod1和mod2模块中的成员就被合并到__init__.py文件中,并被新的变量引用(相当于分配了别名)。

步骤7:还可以定义__all__属性,为import∗导入方式提供所有公开的成员列表。

步骤8:在需要使用以上模块的代码中,只需要将my_lib包作为模块导入,就可以访问其子模块中的函数了。

步骤9:尝试访问my_lib中的四个成员。

案例50 合并多个__init__.py文件中的__all__属性

导语

将其他__init__.py文件中的__all__属性内容合并到当前__init__.py文件的__all__属性中,这种情况多用于把包的子目录中的__all__属性合并根目录中,以方便其他代码访问(使用from…import∗语句就可以从包的根目录导入各级子目录下的所有成员)。

本案例中,包的根目录名为root,root目录下面有两个子目录——project1和project2。project1目录下有part_a、part_b两个模块;project2目录下有file_checker模块。整体结构如下:

该案例最终要实现将子模块的成员逐层合并到包(目录)模块的__all__属性中。即将part_a模块中的list_all函数和part_b模块中的set_task_id函数合并到project1.__init__模块的__all__属性中;将file_checker模块中的is_same函数合并到project2.__init__模块的__all__属性中;最后再把project1.__init__.__all__和project2.__init__.__all__两个属性的内容合并到root.__init__.__all__属性中。

操作流程

步骤1:新建root目录,作为包的根目录,然后在root目录下建立__init__.py文件(先保留空白,后面步骤中会添加代码)。

步骤2:在root目录下新建project1目录,再在project1目录下新建模块part_a,里面定义一个函数。

步骤3:在project1目录下再新建part_b模块,里面也定义一个函数。

步骤4:在project1目录下新建__init__.py文件,将part_a和part_b模块的成员导入,并组成__all__属性。

__all__属性需要字符串序列,直接访问函数的__name__属性可以获取其名称的字符串表示形式。

接下来完成project2子目录的内容。

步骤5:在root目录下新建project2目录。

步骤6:在project2目录下新建file_checker模块,并在模块中定义一个函数。

步骤7:在project2目录下新建__init__.py文件,导入file_checker模块的成员,并放到__all__属性中。

步骤8:回到root目录下的__init__.py文件中,分别将project1和project2作为模块导入。

步骤9:在设置__all__属性的值之前,需要完成一个重要操作。由于root.__init__模块中并没有导入root.project1和root.project2目录中的模块,因此root模块的名称空间中是不存在list_all、set_task_id和is_same这些函数的记录的,如果直接设置__all__属性,在运行时会出错(名称空间中找不到这些成员)。所以,在设置__all__属性前,要把project1和project2中由__all__属性列出的成员复制到当前名称空间中,代码如下:

globals函数返回当前模块的名称空间列表(字典格式),然后调用字典的update方法将从project1和project2模块中获得的成员添加到该字典中,这样一来,当前模块中就存在这些成员的引用了,设置__all__属性后不会出错。

步骤10:合并__all__属性的内容。

步骤11:在需要访问root包的代码中,直接导入它的所有成员列表。

步骤12:为了验证一下子模块中的成员是否都合并到root.__all__属性中,可以打印全局的变量名。

打印结果如下:

从结果中看到,list_all、set_task_id、is_same这三个函数的名字已经在当前模块的名称空间中了,表明它们已被成功合并。

案例51 __main__.py文件的用途

导语

作为包的目录下有时会存在一个名为__main__.py的文件,当包作为模块被直接运行时(作为顶层代码运行,而非被其他模块导入),就会执行__main__.py文件中的代码。这种情况类似于当模块文件被作为顶层代码运行时,模块的__name__属性会变成__main__。

假设有一个名为test的包,可通过以下python命令运行:

执行命令后,会出现下面的提示信息:

从提示信息中可以得知:包作为顶层代码被运行时,其目录下面需要__main__模块。在这个模块中可以编写任意可执行的代码,在包被直接运行时,__main__模块中的代码就会执行。

操作流程

步骤1:新建demo目录,即包名称为demo。

步骤2:在demo目录下新建代码文件__init__.py文件,然后在该模块中定义两个函数。

步骤3:在demo目录下新建__main__.py文件,在这个模块中调用刚刚定义的两个函数。

步骤4:在命令行终端输入以下命令,将demo包作为顶层代码运行。

此时,__main__.py文件中的代码被执行,输出结果如下:

案例52 基于名称空间的包

导语

如果包作为模块被导入,此模块对象会存在一个__path__属性。对于规范的包(目录下面包含__init__.py文件)而言,__path__属性是列表类型(list),其中包含包目录的路径。不过,如果某个目录下没有__init__.py文件,尽管不会被识别为正常的包,但是该目录仍然可以在代码中进行导入,这种将普通目录导入为模块的包称为“基于名称空间的包”(Namespace Package)。

基于名称空间的包目录被导入后,它的__path__属性并非常规的列表类型,而是名为_NamespacePath的内部类型(完整路径为_frozen_importlib_external._NamespacePath)。

由于基于命名空间的包没有__init__.py文件,不能编写初始化代码,所以一般不会直接导入目录,而是导入目录中的代码模块(.py文件)。导入之后,对模块成员的访问方式与正常的包一样。

操作流程

步骤1:新建目录,将其命名为my_lib,用于存放模块文件。

步骤2:在my_lib目录下新建代码文件,命名为mod_1.py,即模块名为mod_1,并在模块中定义一个函数。

步骤3:在my_lib目录下新建代码文件,命名为mod_2.py,对应的模块名为mod_2,然后在模块中也定义一个函数。

步骤4:在顶层代码模块中,依次导入test_fun_a和test_fun_b函数。

步骤5:调用导入的函数。

步骤6:还可以用import语句直接导入my_lib目录。

步骤7:打印my_lib对象的__path__属性。

得到的输出内容如下:

括号中包含的是目录的完整路径。

注意:普通目录可以作为基于名称空间的包使用,但.zip文件中的普通目录是不能作为基于名称空间的包使用的。也就是说,.zip文件中的目录如果要作为包使用,目录中就必须存在__init__.py文件。

案例53 __package__属性

导语

如果导入的模块是包(Package),那么它的__package__属性表示此包的路径(路径用点号分隔),多数情况下,__package__属性的值与__name__属性相同;如果导入的模块不是包,那么__package__属性的值是一个空字符串。

操作流程

步骤1:新建目录lib_root。

步骤2:在lib_root目录下新建pack1目录,再在pack1目录下新建__init__.py文件,表明pack1目录是包(__init__.py是个空文件)。

步骤3:在lib_root目录下新建pack2目录,再在pack2目录下新建__init__.py文件,使pack2目录成为包(__init__.py也是空文件)。

步骤4:在顶层代码模块中分别导入pack1和pack2两个包。

步骤5:依次打印pack1和pack2的__package__属性的值。

输出结果为:

案例54 自定义包或模块的搜索路径

导语

Python应用程序在运行时,会在sys模块的path属性所提供的路径列表中查找被导入的包(或模块)。由于path属性是列表类型(list类),因此可以在运行时通过代码进行修改。

通常不建议删除path列表中元素,因为Python程序在初始化的过程中会向path列表添加一些必要的路径(这些路径包含Python标准库的路径),如果将这些路径删除,有可能导致Python代码无法正常执行。所以,推荐的做法是向path列表中添加需要的路径。

操作流程

步骤1:新建目录,命名为demo。

步骤2:在demo目录下新建__init__.py文件,使demo目录成为包目录。

步骤3:在__init__模块中定义一个函数。

步骤4:将demo目录移动到当前示例以外的路径中。例如,本案例将demo目录移动到Windows操作系统下的C:\cust_libs目录下。

步骤5:在顶层代码模块中,在sys.path列表添加自定义的查找路径,本案例中为C:\cust_libs。

insert方法将自定义的路径添加到path列表的顶部。路径C:\cust_libs加上了r前缀,表示此字符串中的字符不进行转义(即原义字符),如果不使用r前缀,那么路径中的“\”字符必须进行转义(即“\\”)。

步骤6:从demo包中导入test_fun函数。

步骤7:测试调用test_fun函数。

步骤8:为了验证demo包是否从自定义的路径中导入,可以输出一下包的__path__属性的值。

屏幕上打印的内容为:

以上输出表明demo包确实是从自定义的路径下找到的。

案例55 从.zip文件中导入包

导语

Python支持从zip压缩文档中导入包。处理方法与普通目录下的包导入类似,只需将zip文档当作一层目录来处理即可。

假设test包位于sample.zip文件中,那么,test包在导入时会查找以下代码文件:

但在执行import语句前,要将zip文档所在的路径添加到sys.path列表中,以便应用程序能够搜索压缩包中的内容。

操作流程

步骤1:新建demo目录,并在目录下新建__init__.py文件。

步骤2:在__init__.py文件中定义一个函数。

步骤3:将demo目录(demo包)放到一个zip压缩文件中,压缩文件命名为myLib.zip。其目录结构如下:

步骤4:在顶层代码中导入sys模块。

步骤5:将myLib.zip文档的相对路径添加到sys.path列表中。

步骤6:从demo包中导入func函数。

步骤7:尝试调用func函数。

步骤8:执行案例代码,如果看到程序输出文本信息“测试程序”,则说明myLib.zip文件中的demo包已被成功导入。

注意:Python程序在执行zip文档中的包时,是不会生成编译文件(.pyc)的。如果希望执行编译后的文件,应当先生成.pyc文件再将其放进压缩文档中。