组织大型项目
在本章中,我们将介绍更多Python组织程序的技术。具体来说,我们将学习Python的概念包以及如何使用这些来为您的程序添加结构,因为它超越了简单的模块。
包
您还记得,Python组织代码的基本工具是模块.1模块通常对应于单个源文件,您可以使用进口
关键字。当您导入一个模块时,它由类型的对象表示模块
你可以像对待其他物体一样与它互动。
一个包在Python中只是一种特殊类型的模块。包的定义特征是它可以包含其他模块,包括其他包。所以包是Python中定义模块层次结构的一种方式。这允许您以表达其内聚性的方式将具有相似功能的模块分组在一起。
一个包的例子:urllib
Python标准库的许多部分都是作为包实现的。要查看示例,请打开REPL并导入urllib
而且urllib.request
:
>>>进口urllib>>>进口urllib.请求
现在如果你检查这两个模块的类型,你会看到它们都是类型模块
:
>>>类型(urllib)<类“模块”>>>>类型(urllib.请求)<类“模块”>
这里的重点是urllib.request
是嵌套的内部urllib
.在这种情况下,urllib
是一个包,并且请求
是正常模块。
的__path__
包的属性
如果你仔细观察这些物体,你会发现一个重要的区别。的urllib
包有一个__path__
成员,urllib.request
没有:
>>>urllib.__path__[”。/ urllib ']>>>urllib.请求.__path__回溯(最近一次通话):文件“< stdin >”,行1,在<模块>AttributeError:“模块”对象没有属性“__path__”
此属性是一个文件系统路径列表,用于指示位置urllib
搜索查找嵌套模块。这暗示了包和模块之间区别的本质:包通常由文件系统中的目录表示,而模块由单个文件表示。
注意,在3.3之前的Python 3版本中,__path__
只是一个字符串,不是一个列表。在这本书中,我们主要关注Python 3.5+,但对于大多数目的来说,差异并不重要。
定位模块
在我们了解包的细节之前,了解Python如何定位模块是很重要的。一般来说,当你要求Python进口
模块时,Python会在文件系统中查找相应的Python源文件并加载该代码。但是巨蟒怎么知道去哪里找呢?答案是Python检查路径
标准属性sys
模块,一般简称为sys.path
.
的sys.path
对象是目录的列表。当你要求Python导入一个模块时,它会从中的第一个目录开始sys.path
并检查是否有适当的文件。如果在第一个目录中没有找到匹配项,它会按顺序检查后续的条目,直到找到匹配项或Python耗尽其中的条目sys.path
,在这种情况下ImportError
是提高。
sys.路径
让我们来探讨sys.path
来自REPL。从命令行运行Python,不带参数,输入以下语句:
>>>进口sys>>>sys.路径['','/图书馆/框架/Python.框架/版本/3.5/自由/python3.5/网站-包/rope_ \py3k-0.9.4-py3.5.蛋”、“/图书馆/框架/Python.框架/版本/3.5/自由/python3.5/年代\工艺教育学院-包/装饰-3.4.0-py3.5.蛋”、“/图书馆/框架/Python.框架/版本/3.\.5/自由/python3.5/网站-包/贝克-1.3-py3.5.蛋”、“/图书馆/框架/Python.framewor \k/版本/3.5/自由/python3.5/网站-包/beautifulsoup4-4.1.3.-py3.5.蛋”、“/图书馆/联邦铁路局\meworks/Python.框架/版本/3.5/自由/python3.5/网站-包/pymongo-2.3-py3.5-macos \x-10.6-英特尔.蛋”、“/图书馆/框架/Python.框架/版本/3.5/自由/python3.5/网站-\包/eagertools-0.3-py3.5.蛋”、“/首页/项目/emacs_config/traad/traad”、“/图书馆/\框架/Python.框架/版本/3.5/自由/python3.5/网站-包/瓶-0.11.6-py3.5.\蛋”、“/首页/项目/see_stats”、“/图书馆/框架/Python.框架/版本/3.5/自由/\python3.5/网站-包/女服务员-0.8.5-py3.5.蛋”、“/图书馆/框架/Python.框架/\版本/3.5/自由/python3.5/网站-包/pystache-0.5.3.-py3.5.蛋”、“/图书馆/框架/\Python.框架/版本/3.5/自由/python3.5/网站-包/pyramid_tm-0.7-py3.5.蛋”、“/李\新馆/框架/Python.框架/版本/3.5/自由/python3.5/网站-包/pyramid_debugt \oolbar-1.0.6-py3.5.蛋”、“/图书馆/框架/Python.框架/版本/3.5/自由/python3.5\/网站-包/金字塔-1.4.3.-py3.5.蛋”、“/图书馆/框架/Python.框架/版本/3.\.5/自由/python3.5/网站-包/事务-1.4.1-py3.5.蛋”、“/图书馆/框架/Python.\框架/版本/3.5/自由/python3.5/网站-包/Pygments-1.6-py3.5.蛋”、“/图书馆/联邦铁路局\meworks/Python.框架/版本/3.5/自由/python3.5/网站-包/PasteDeploy-1.5.0-py3.5\.蛋”、“/图书馆/框架/Python.框架/版本/3.5/自由/python3.5/网站-包/交易\nslationstring-1.1-py3.5.蛋”、“/图书馆/框架/Python.框架/版本/3.5/自由/pyt \hon3.5/网站-包/金星人-1.0 a8-py3.5.蛋”、“/图书馆/框架/Python.框架/版本\锡安/3.5/自由/python3.5/网站-包/zope.弃用-4.0.2-py3.5.蛋”、“/图书馆/Framew \半兽人/Python.框架/版本/3.5/自由/python3.5/网站-包/zope.接口-4.0.5-py3.5\-macosx-10.6-英特尔.蛋”、“/图书馆/框架/Python.框架/版本/3.5/自由/python3.5\/网站-包/repoze.lru-0.6-py3.5.蛋”、“/图书馆/框架/Python.框架/版本/\3.5/自由/python3.5/网站-包/WebOb-1.2.3.-py3.5.蛋”、“/图书馆/框架/Python.框架\工作/版本/3.5/自由/python3.5/网站-包/尖吻鲭鲨-0.8.1-py3.5.蛋”、“/图书馆/框架\/Python.框架/版本/3.5/自由/python3.5/网站-包/变色龙-2.11-py3.5.蛋”、“/L \ibrary/框架/Python.框架/版本/3.5/自由/python3.5/网站-包/MarkupSafe-0.\18-py3.5-macosx-10.6-英特尔.蛋”、“/图书馆/框架/Python.框架/版本/3.5/自由/p \ython3.5/网站-包/皮普-1.4.1-py3.5.蛋”、“/图书馆/框架/Python.框架/版本的\ns/3.5/自由/python3.5/网站-包/ipython-1.0.0-py3.5.蛋”、“/图书馆/框架/Python.\框架/版本/3.5/自由/python3.5/网站-包/熊猫-0.12.0-py3.5-macosx-10.6-英特尔.\蛋”、“/图书馆/框架/Python.框架/版本/3.5/自由/python3.5/网站-包/是以\ptools-1.1.6-py3.5.蛋”、“/图书馆/框架/Python.框架/版本/3.5/自由/python3.5\/网站-包/readline-6.2.4.1-py3.5-macosx-10.6-英特尔.蛋”、“/首页/项目/see_stats/d \istribute-0.6报-py3.5.蛋”、“/图书馆/框架/Python.框架/版本/3.5/自由/pytho \n3.5/网站-包/nltk-2.0.4-py3.5.蛋”、“/图书馆/框架/Python.框架/版本/\3.5/自由/python3.5/网站-包/PyYAML-3.10-py3.5-macosx-10.6-英特尔.蛋”、“/图书馆/框架\作品/Python.框架/版本/3.5/自由/python3.5/网站-包/numpy-1.8.0-py3.5-macosx-\10.6-英特尔.蛋”、“/图书馆/框架/Python.框架/版本/3.5/自由/python3.5/网站-pa \ckages/露齿而笑-1.2.1-py3.5.蛋”、“/图书馆/框架/Python.框架/版本/3.5/自由/pyth \on3.5/网站-包/argparse-1.2.1-py3.5.蛋”、“/图书馆/框架/Python.框架/更\离子/3.5/自由/python33.邮政编码”、“/图书馆/框架/Python.框架/版本/3.5/自由/python \3.5”、“/图书馆/框架/Python.框架/版本/3.5/自由/python3.5/平台-达尔文”、“/李\新馆/框架/Python.框架/版本/3.5/自由/python3.5/自由-dynload”、“/图书馆/“弗拉姆”\eworks/Python.框架/版本/3.5/自由/python3.5/网站-包的]
如你所见,你的sys.path
可以相当大。它的精确条目取决于许多因素,包括您安装了多少第三方软件包以及如何安装它们。就我们的目的而言,其中一些条目特别重要。首先,让我们看看第一个条目:
>>>sys.路径[0]''
记住,sys.path
只是一个普通的列表,所以我们可以用索引和切片检查它的内容。我们在这里看到,第一个条目是空字符串。当你不带参数启动Python解释器时,就会发生这种情况,它会指示Python首先在当前目录中搜索模块。
我们再来看看尾巴sys.path
:
>>>sys.路径[-5:][' /图书馆/框架/ Python.framework /版本/ 3.5 / lib / python33.zip ','/图书馆/Framewor \ks/Python.框架/版本/3.5/自由/python3.5”、“/图书馆/框架/Python.框架/Ve \rsions/3.5/自由/python3.5/平台-达尔文”、“/图书馆/框架/Python.框架/版本/3.5\/自由/python3.5/自由-dynload”、“/图书馆/框架/Python.框架/版本/3.5/自由/pytho \n3.5/网站-包的]
这些条目组成了Python的标准库和网站
第三方模块安装目录。
sys.path
在行动
来真正感受一下sys.path
,让我们在Python通常不会搜索的目录中创建一个Python源文件:
$ mkdir not_searches
在该目录中创建一个名为path_test.py
使用以下函数定义:
# not_searched / path_test.pydef发现():返回“巨蟒找到我了!”
现在,从目录,其中包含not_searched
目录然后尝试导入path_test
:
>>>进口path_test回溯(最近一次通话):文件“< stdin >”,行1,在<模块>ImportError:没有命名为“path_test”
的path_test
模块——记住,它体现在模块中not_searched / path_test.py
-没有发现,因为path_test.py
是不是在一个目录中包含sys.path
.为了使path_test
我们需要导入目录not_searched
成sys.path
.
自sys.path
只是一个普通的列表,我们可以使用append ()
方法。启动一个新的REPL并输入以下内容:
>>>进口sys>>>sys.路径.附加(“not_searched”)
现在我们导入path_test
在同一个REPL会话中,我们看到它是有效的:
>>>进口path_test>>>path_test.发现()巨蟒找到我了!
知道如何手动操作sys.path
可能是有用的,有时它是使代码对Python可用的最佳方式。还有另一种方法可以将条目添加到sys.path
不过,这并不需要直接操作列表。
的PYTHONPATH环境
环境变量
的PYTHONPATH环境
环境变量是添加到的路径列表sys.path
Python开始的时候。
的格式PYTHONPATH环境
和路径
在你的平台上。在Windows上PYTHONPATH环境
以分号分隔的目录列表:
c:\ \一些路径;c:\另\路径;d:\ \另外一个
在Linux和OS X上,它是一个冒号分隔的目录列表:
/一些/路径:/另一个/路径:/然而,/另一个
来看看PYTHONPATH环境
可以,我们加一下not_searched
在再次启动Python之前。在Windows上使用集
命令:
>集PYTHONPATH环境=not_searched
在Linux或OS X上,语法将取决于您的shell,但对于bash-like shell,您可以使用出口
:
$ export PYTHONPATH=not_searched
现在启动一个新的REPL并检查not_searched
确实在sys.path
:
>>>[路径为路径在sys.路径如果“not_searched”在路径][“/ home / python_journeyman / not_searched”]
当然我们现在可以导入了path_test
无需手动编辑sys.path
:
>>>进口path_test>>>path_test.发现()巨蟒找到我了!
还有更多的细节sys.path
而且PYTHONPATH环境
,但这是你需要知道的大部分内容。要了解更多信息,你可以查看Python文档的链接:
实施方案
我们已经看到包是可以包含其他模块的模块。但是包是如何实现的呢?要创建一个普通的模块,只需在目录中创建一个Python源文件sys.path
.创建包的过程没有太大不同。
要创建一个包,首先要创建包的根目录.这个根目录需要在某个目录上sys.path
;记住,这是Python如何找到要导入的模块和包。然后,在根目录中创建一个名为__init__ . py
.这个文件,我们通常称之为包初始化File -使包成为模块。__init__ . py
可以是(而且经常是)空的;它的存在本身就足以确立一揽子计划。
在命名空间包中,我们将看到一种更通用的包形式,它可以跨越多个目录树。在那个部分我们会看到,因为PEP 420在Python 3.3中引入,
__init__ . py
从技术上讲,包不再需要文件了。那么为什么我们谈论它们就好像它们是必须的一样?首先,早期版本的Python仍然需要它们。事实上,许多为3.3+编写代码的人都没有意识到这一点
__init__ . py
文件是可选项。因此,您仍然会在绝大多数包中找到它们,因此最好熟悉它们。此外,它们为包初始化提供了强大的工具。所以了解如何使用它们很重要。
也许最重要的是,我们建议您包含
__init__ . py
文件,因为“显式比隐式好”。包初始化文件的存在是一个明确的信号,表明您打算将一个目录作为一个包,这是许多Python开发人员本能地寻找的东西。
第一个套餐:读者
与Python中的许多东西一样,示例比文字更有指导意义。进入你的命令提示符,创建一个名为“reader”的新目录:
$ mkdir读取器
添加一个空__init__ . py
到这个目录。在Linux os os X上,你可以使用touch命令:
$ touch reader/__init__.py
你可以在Windows上使用类型
:
>类型空>读者/__init__.py
现在如果你启动你的REPL,你会看到你可以导入读者
:
>>>进口读者>>>类型(读者)<类“模块”>
的作用__init__ . py
我们现在可以开始研究这个角色__init__ . py
在一个包的功能中起作用。检查__file__
属性。读者
包:
>>>读者.__file__”。/读者/ __init__ . py”
我们看到了读者
是一个模块,尽管在我们的文件系统中,“reader”的名称指的是一个目录。此外,当执行的源文件读者
导入的包init文件是读者
目录,或称读者/ __init__ . py
.换句话说——重申一个关键点——包不过是一个包含名为__init__ . py
.
要看到__init__ . py
实际上是执行像任何其他模块时导入读者
,让我们添加一小段代码:
#读者/ __init__ . py打印(“阅读器正在进口!”)
重新启动REPL,导入读者
,你会看到我们的小信息打印出来:
>>>进口读者读者是被进口!
向包中添加功能
现在我们已经创建了一个基本包,让我们向其中添加一些有用的内容。我们的包的目标是创建一个类,它可以读取三种不同文件格式的数据:未压缩的文本文件,压缩后的文本文件gzip,以及压缩的文本文件bz2.2我们将这个类命名为MultiReader
因为它可以读取多种格式。
我们从定义开始MultiReader
.类的初始版本只知道如何读取未压缩的文本文件;稍后我们将添加对gzip和bz2的支持。创建文件读者/ multireader.py
内容如下:
#读者/ multireader.py类MultiReader:def__init__(自我,文件名):自我.文件名=文件名自我.f=开放(文件名,“rt”)def关闭(自我):自我.f.关闭()def读(自我):返回自我.f.读()
启动一个新的REPL并导入您的新模块以尝试MultiReader
类。让我们用它来阅读的内容读者/ __init__ . py
本身:
>>>进口读者.multireader>>>r=读者.multireader.MultiReader(“读者/ __init__ . py”)>>>r.读()“#读者/ __init__ . py \ n”>>>r.关闭()
在某种程度上,我们的包正在读取它自己的一些源代码!
子包
为了演示包如何为Python代码提供高级结构,让我们向读者
层次结构。我们将添加一个子包到读者
被称为压缩
它将包含处理压缩文件的代码。首先,让我们创建新目录及其关联目录__init__ . py
在Linux或OS X上使用:
$ mkdir读取器/压缩$ touch reader/压缩/__init__.py
在Windows上使用这些命令:
>mkdir读者\压缩>类型空>读者\ \ __init__压缩.py
如果您重新启动您的REPL,您将看到您可以导入reader.compressed
:
>>>进口读者.压缩>>>读者.压缩.__file__“读者/压缩/ __init__ . py”
gzip支持
接下来我们将创建文件读者/压缩/ gzipped.py
它将包含一些使用gzip压缩格式的代码:
#读者/压缩/ gzipped.py进口gzip进口sys开瓶器=gzip.开放如果__name__= =“__main__ ':f=gzip.开放(sys.argv[1],模式=“wt”)f.写(' '.加入(sys.argv[2:]))f.关闭()
正如您所看到的,这段代码没有太多内容:它只是定义了名称开瓶器
这只是一个别名gzip.open ()
.这个函数的行为与普通函数很相似open ()
它返回一个类文件对象3.可以从中读取。当然,主要的区别在于gzip.open ()
在读取时解压缩文件的内容open ()
没有。4
注意这里惯用的“main block”。它使用
gzip
创建一个新的压缩文件并向其写入数据。稍后我们将使用它来创建一些测试文件。欲知更多有关__name__
而且__main__
,见第3章巨蟒学徒.
bz2支持获取
类似地,让我们创建另一个文件来处理bz2压缩读者/压缩/ bzipped.py
:
#读者/压缩/ bzipped.py进口bz2进口sys开瓶器=bz2.开放如果__name__= =“__main__ ':f=bz2.开放(sys.argv[1],模式=“wt”)f.写(' '.加入(sys.argv[2:]))f.关闭()
此时,你的目录结构应该是这样的:
读者├──__init__.py├──multireader.py└──压缩├──__init__.py├──bzipped.py└──gzip.py
如果你开始一个新的REPL,你会看到我们可以导入所有的模块,正如你所期望的那样:
>>>进口读者>>>进口读者.multireader>>>进口读者.压缩>>>进口读者.压缩.gzip>>>进口读者.压缩.bzipped
完整的程序
让我们把所有这些结合在一起,形成一个更有用的程序。我们会更新MultiReader
这样它就可以从gzip文件、bz2文件和普通文本文件中读取。它将根据文件扩展名决定使用哪种格式。改变你的MultiReader
类读者/ multireader.py
在必要时使用压缩处理程序:
1#读者/ multireader.py23.进口操作系统45从读者.压缩进口bzipped,gzip67将文件扩展名映射到相应的打开方法。8extension_map={9“bz2”:bzipped.开瓶器,10. gz的:gzip.开瓶器,11}121314类MultiReader:15def__init__(自我,文件名):16扩展=操作系统.路径.splitext(文件名)[1]17开瓶器=extension_map.得到(扩展,开放)18自我.f=开瓶器(文件名,“rt”)1920.def关闭(自我):21自我.f.关闭()2223def读(自我):24返回自我.f.读()
这个更改中最有趣的部分是第5行,我们在这里导入子包bzipped
而且gzip
.这演示了包的基本组织功能:相关功能可以分组在一个公共名称下,以便于识别。
有了这些变化,MultiReader
现在将检查它所提交的文件名的扩展名。如果那个分机是关键的话extension_map
然后将使用一个专门的文件打开器——在本例中也是如此reader.compressed.gzipped.opener
或reader.compressed.gzipped.opener
.否则,标准open ()
将被使用。
为了测试这一点,让我们首先使用内置到压缩模块中的实用程序代码创建一些压缩文件。直接从命令行执行模块:
python3美元-m读者.压缩.bzipped测试.Bz2数据压缩与bz2python3美元-m读者.压缩.gzip测试.Gz数据压缩与gzip$ ls读者测试.bz2测试获取.广州
请自行核实test.bz2
而且test.gz
实际上是压缩的(或者至少它们不是纯文本!)
启动一个新的REPL,让我们的代码旋转:
>>>从读者.multireader进口MultiReader>>>r=MultiReader(“test.bz2”)>>>r.读()使用bz2压缩的数据>>>r.关闭()>>>r=MultiReader(“test.gz”)>>>r.读()使用gzip压缩的数据>>>r.关闭()>>>r=MultiReader(“读者/ __init__ . py”)>>>r.读()...读者的内容/__init__.py...>>>r.关闭()
如果您将所有正确的代码放在了所有正确的位置,您应该会看到MultiReader
当它看到gzip和bz2文件的相关文件扩展名时,确实可以解压缩它们。
方案评审
我们在本章已经讲了很多信息,让我们复习一下。
- 包是可以包含其他模块的模块。
- 包通常被实现为包含特殊属性的目录
__init__ . py
文件。 - 的
__init__ . py
文件在导入包时执行。 - 包可以包含子包,子包本身实现为包含
__init__ . py
文件。 - 的
模块
包的对象具有__path__
属性。
相对进口
在这本书中,我们已经看到了的一些用法进口
关键字,如果你做过一些Python编程,那么你应该熟悉它。到目前为止,我们看到的所有用法都叫做绝对的进口其中,您可以指定想要导入的任何模块的所有祖先模块。例如,为了导入reader.compressed.bzipped
在上一节中,您必须同时提到这两种情况读者
而且压缩
在进口
声明:
这两个绝对导入都提到了“reader”和“compressed”进口读者.压缩.bzipped从读者.压缩进口bzipped
有另一种形式的导入称为相对进口这允许您使用到模块和包的缩短路径。相对导入是这样的:
从..module_name进口的名字
这种形式的明显区别进口
到目前为止,我们看到的绝对进口是.
年代之前module_name
.总之,每一个.
表示正在进行导入的模块的祖先包,从包含该模块的包开始,一直移动到包根。您可以指定它们,而不是使用包树根的绝对路径指定导入相对导入模块,因此相对进口.
请注意,只能将相对导入用于从<模块>导入<名称>
形式进口
.试着做一些像"进口.module
是语法错误。
重要的是,相对导入只能在当前顶级包中使用,而不能在包外导入模块。在之前的例子中读者
模块可以使用相对导入gzip
,但它需要对顶层以外的任何内容使用绝对导入读者
包中。
让我们用一些简单的例子来说明这一点。假设我们有这样的包结构:
图形/__init__.py原语/__init__.py行.py形状/__init__.py三角形.py场景/__init__.py
进一步假设模块line.py
模块包括以下定义:
#图形原语/ line.pydef渲染(x,y):“从x到y画一条线”#……
的graphics.shapes.triangle
模块负责渲染-你猜对了!-三角形,要做到这一点,它需要使用graphics.primitives.lines.render ()
画线。的一种方法triangle.py
可以使用绝对导入来导入此函数:
#图形/形状/ triangle.py从图形.原语.行进口渲染
或者你也可以使用相对导入:
#图形/形状/ triangle.py从..原语.行进口渲染
领先的..
相对形式表示“包含此模块的包的父包”,换句话说,是图形
包中。
这个表总结了如何.
S在用于从进行相对导入时进行解释graphics.shapes.triangle
:
triangle.py | 模块 |
---|---|
|
|
|
|
|
|
光秃秃的圆点从
条款
在相对进口中也是合法的从
部分完全由点组成。在这种情况下,这些点仍然以完全相同的方式解释。
回到我们的例子,假设graphics.scenes
包用于构建复杂的、多形状的场景。要做到这一点,需要使用的内容graphics.shapes
,所以图形/场景/ __init__ . py
需要导入graphics.shapes
包中。使用绝对进口可以通过多种方式实现这一点:
#图形/场景/ __init__ . py所有这些都以不同的方式导入同一个模块进口图形.形状进口图形.形状作为形状从图形进口形状
另外,graphics.scenes
可以使用相对导入来获得相同的模块:
#图形/场景/ __init__ . py从..进口形状
在这里,..
意思是“当前包的父包”,就像相对导入的第一种形式一样。
很容易看出相对导入对于减少嵌套很深的包结构中的类型输入是多么有用。它们还促进了某些形式的可修改性,因为原则上它们允许您在某些情况下重命名顶级包和子包。不过,总的来说,普遍的共识似乎是,在大多数情况下,最好避免相对进口。
__all__
我们想看的另一个主题是可选的__all__
模块的属性。__all__
控件时导入哪些属性从模块导入
语法。如果__all__
没有指定从x导入*
进口所有公共5导入模块中的名称。
的__all__
模块属性必须是一个字符串列表,每个字符串表示一个名称,该名称将在*
使用语法。例如,我们可以看到什么从reader.compressed导入*
所做的事。首先,让我们添加一些代码读者/压缩/ __init__ . py
:
#读者/压缩/ __init__ . py从读者.压缩.bzipped进口开瓶器作为bz2_opener从读者.压缩.gzip进口开瓶器作为gzip_opener
接下来,我们将启动一个REPL,并显示当前范围内的所有名称:
>>>当地人(){“__loader__”:<类“_frozen_importlib。BuiltinImporter”>,“__name__”:“__main__ ',“__builtins__”:<模块“内置”(建-在)>,“__package__”:没有一个,“__doc__”:没有一个}
现在我们将从导入所有公共名称压缩
:
>>>从读者.压缩进口*>>>当地人(){“bz2_opener”:<函数开放在0 x1018300e0>,“gzip_opener”:<函数开放在0 x101a36320>,“gzip”:<模块“reader.compressed.gzipped”从”。/读者/压缩/ gzipped.py”>,“\bzipped': <模块'读者.压缩.bzipped“来自”./读者/压缩/bzipped.py“>”,__ \package__”:没有一个,“__name__”:“__main__ ',“__builtins__”:<模块“内置”(建-在)>,“__loader__”:<类“_frozen_importlib。BuiltinImporter”>,“__doc__”:没有一个}>>>bzipped<模块“reader.compressed.bzipped”从”。/读者/压缩/ bzipped.py”>>>>gzip<模块“reader.compressed.gzipped”从”。/读者/压缩/ gzipped.py”>
我们看到的是从reader.compressed导入*
进口的bzipped
而且gzip
的子模块压缩
直接打包到本地命名空间中。我们更喜欢这样进口*
只从每个模块中导入不同的“opener”函数,所以让我们更新一下压缩/ __init__ . py
做那件事:
#读者/压缩/ __init__.python从读者.压缩.bzipped进口开瓶器作为bz2_opener从读者.压缩.gzip进口开瓶器作为gzip_opener__all__=[“bz2_opener”,“gzip_opener”]
现在如果我们使用进口*
在reader.compressed
我们只导入“opener”函数,而不导入它们的模块:
>>>当地人(){“__package__”:没有一个,“__loader__”:<类“_frozen_importlib。BuiltinImporter”>,“__doc__ \':没有,'__builtins__': <模块'内置命令'(内置)>,'__name__“:”__main__ '}>>>从读者.压缩进口*>>>当地人(){“__name__”:“__main__ ',“__doc__”:没有一个,“__package__”:没有一个,“__loader__”:<类“_fro \zen_importlib.BuiltinImporter ' > ',__spec__':没有,'__annotations__': {}, '__builtins__”:\<模块“内置”(建-在)>,“bz2_opener”:<函数开放在0 x10efb9378>,“gzip_open \呃':<函数开放在0 x10f06b620>}>>>bz2_opener<函数开放在0 x1022300e0>>>>gzip_opener<函数开放在0 x10230a320>
的__all__
Module属性对于限制模块公开的名称是一个有用的工具。我们仍然不建议您使用进口*
在REPL中除了方便之外的语法,但是了解它是很好的__all__
因为你很可能在野外看到它。
名称空间包
前面我们说过,包被实现为包含a的目录__init__ . py
文件。这对于大多数情况都是正确的,但是在某些情况下,您希望能够将包拆分到多个目录。这很有用,例如,当一个逻辑包需要以多个部分交付时,就像在一些较大的Python项目中发生的那样。
已经实施了几种解决这一需求的方法,但它是在PEP420在2012年,一个官方的解决方案被内置到Python语言中。这个解决方案被称为名称空间包.命名空间包是分布在几个目录上的包,从程序员的角度来看,每个目录树都是一个逻辑包。
命名空间包与普通包的不同之处在于它们没有__init__ . py
文件。这很重要,因为这意味着名称空间包不能有包级初始化代码;导入包时,包不会执行任何操作。这种限制的主要原因是,当多个目录对一个包做出贡献时,它避免了初始化顺序的复杂问题。
但是如果命名空间包没有__init__ . py
文件,Python如何在导入过程中找到它们?答案是Python遵循一个相对简单的算法检测命名空间包。当被要求导入名称“foo”时,Python扫描中的每个条目sys.path
在秩序。如果在这些目录中找到一个名为“foo”的目录,其中包含__init__ . py
,则导入正常包。如果它没有找到任何正常的包做找到foo.py
或任何其他可以作为模块的文件6,则该模块将被导入。否则,导入机制会跟踪它找到的所有名为“foo”的目录。如果没有找到满足导入的正常包或模块,那么所有匹配的目录名都作为命名空间包的一部分。
命名空间包的示例
作为一个简单的例子,让我们看看如何将图形
打包到命名空间包中。我们不是将所有代码放在一个目录下,而是将两个独立的部分放在path1
而且path2
是这样的:
path1└──图形├──原语│├──__init__.py│├──线.py└──形状├──__init__.py└──三角形.pypath2└──图形└──场景└──__init__.py
这就把场景
包的其余部分。
现在导入图形
你需要确保两者都是path1
而且path2
都在你的sys.path
.我们可以在一个REPL中这样做:
>>>进口sys>>>sys.路径.扩展([”。/ path1 ',”。/ path2 '])>>>进口图形>>>图形.__path___NamespacePath([“path1 /图形。”,“path2 /图形。”])>>>进口图形.原语>>>进口图形.场景>>>图形.原语.__path__[”。/ path1 /图形/原语的]>>>图形.场景.__path__[”。/ path2 /图形/场景”]
我们把path1
而且path2
在最后sys.path
.当我们导入时图形
我们看到它__path__
包括两部分path1
而且path2
.当我们导入原语
而且场景
我们看到它们确实来自各自的目录。
还有更多的细节名称空间包,但这解决了你需要知道的大部分重要细节。事实上,在大多数情况下,您根本不可能需要开发自己的名称空间包。不过,如果你确实想了解更多关于它们的知识,你可以从阅读开始PEP 420.
可执行目录
开发包通常是因为它们实现了一些您想要执行的程序。构造这样的程序有许多方法,但最简单的方法之一是使用可执行目录.可执行目录允许您指定一个主入口点,该入口点在Python执行目录时运行。
当我们说Python“执行目录”是什么意思?我们的意思是在命令行上将目录名传递给Python,就像这样:
$ mkdir text_stats$ python3 text_stats
通常这是行不通的,Python会抱怨说它找不到__main__
模块:
$ python3 text_stats/usr/当地的/箱子/python3:可以找不到__main__模块中的text_stats”
但是,正如该错误消息所建议的那样,您可以将一个名为__main__.py
在目录中,Python将执行它。这个模块可以执行它想执行的任何代码,这意味着它可以调用您创建的模块来提供(例如)模块的用户界面。
为了说明这一点,让我们添加一个__main__.py
对我们的text_stats
目录中。这个程序将计算(粗略地!)在命令行中传入的单词和字符的数量:
# text_stats / __main__.py进口sys段=sys.argv[1:]full_text=' '.加入(段)输出=# words: {}, # chars: {}.格式(len(full_text.分裂()),总和(len(w)为w在full_text.分裂()))打印(输出)
现在如果我们通过这个text_stats
目录到Python,我们将看到我们的__main__.py
执行:
我坐好了在一个办公室,周围都是人头而且的身体.#单词:10,#字符:47
这很有趣,但是是这样使用的__main__.py
只不过是好奇而已。但是,正如我们很快将看到的那样,“可执行目录”的思想可以用来更好地组织可能在单个文件中蔓延的代码。
__main__.py
而且sys.path
当Python执行__main__.py
,它首先添加包含__main__.py
来sys.path
.这种方式__main__.py
可以轻松导入与其共享目录的任何其他模块。
如果你认为目录包含__main__.py
作为一个程序,你可以看到它是如何变化的sys.path
允许您以更好的方式组织代码。您可以为程序逻辑上不同的部分使用单独的模块。在我们的情况下text_stats
例如,将实际的计数逻辑移到一个单独的模块中是有意义的,因此让我们将该代码放入一个名为计数器
:
# text_stats / counter.pydef数(文本):单词=文本.分裂()返回(len(单词),总和(len(w)为w在单词))
然后更新我们的__main__.py
使用计数器
:
# text_stats / __main__.py进口sys进口计数器段=sys.argv[1:]full_text=' '.加入(段)输出=# words: {}, # chars: {}.格式(*计数器.数(full_text))打印(输出)
现在,计数单词和字母的逻辑在我们的小程序中与UI逻辑完全分离了。如果我们再次运行我们的目录,我们会看到它仍然可以工作:
$ python3 text_stats它是可能我已经对我的未来有了一些预感.#单词:11,#字符:50
压缩可执行目录
通过压缩目录,我们可以进一步实现可执行目录的思想。Python知道如何读取zip文件并将其视为目录,这意味着我们可以像创建可执行目录一样创建可执行的zip文件。
创建一个压缩文件text_stats
目录:
$ CD text_stats$邮政编码-r../text_stats.邮政编码*$ cd..
压缩文件应该包含内容您的可执行目录,而不是可执行目录本身。zip文件代替了目录。
现在我们可以告诉Python执行zip文件而不是目录:
$ python3 text_stats.邮政编码唱,女神,珀琉斯的儿子亚基利乌斯的怒气#单词:8,#字符:42
结合Python对的支持__main__.py
在某些情况下,它能够执行压缩文件,这为我们提供了一种便捷的方式来分发代码。如果您开发的程序由一个目录组成,其中包含一些模块和一个__main__.py
,您可以压缩目录的内容,与他人共享,他们将能够运行它,而不需要安装任何包到他们的Python安装。
当然,有时确实需要分发适当的包,而不是更多特别的模块的集合,我们来看看的角色__main__.py
接下来是包装。
可执行方案
在前一节中,我们看到了如何使用__main__.py
使目录直接可执行。您可以使用类似的技术来创建可执行方案.如果你把__main__.py
在包目录中,那么Python将在运行包时执行它Python3 -m package_name
.
为了证明这一点,让我们转换我们的text_stats
程序装入包中。创建一个空的__init__ . py
在text_stats
目录中。然后编辑text_stats / __main__.py
进口计数器
相对而言:
# text_stats / __main__.py进口sys从.计数器进口数段=sys.argv[1:]full_text=' '.加入(段)输出=# words: {}, # chars: {}.格式(*数(full_text))打印(输出)
在这些更改之后,我们看到不能再执行目录python3 text_stats
像之前:
$ python3 text_stats恐怖vacui回溯(最近一次通话):文件“/图书馆/框架/ Python.framework /版本/ 3.5 / lib / python3.6 / runpy.py”,行1\93,在_run_module_as_main“__main__”,mod_spec)文件“/图书馆/框架/ Python.framework /版本/ 3.5 / lib / python3.6 / runpy.py”,行8\5,在_run_code执行(代码,run_globals)文件“text_stats / __main__.py”,行5,在<模块>从.计数器进口数ImportError:尝试相对进口与未知父包
Python抱怨说__main__.py
不能导入计数器
具有相对的重要性。这似乎与我们的设计不一致:__main__.py
而且counter.py
显然是在同一个包裹里!
失败的原因是我们前面了解到的关于可执行目录的内容。当我们请求Python执行text_stats
目录,它首先添加text_stats
目录sys.path
.然后执行__main__.py
.关键的细节是sys.path
包含text_stats
本身,而不是包含text_stats
.因此,Python无法识别text_stats
作为一个包装;我们还没有告诉它去哪里找。
为了执行我们的包,我们需要告诉Python进行处理text_stats
作为一个模块7与- m
命令行标志:
python3美元-M text_stats菲亚特lux#单词:2,#字符:7
现在Python寻找模块text_stats.__main__
——也就是我们的text_stats / __main__.py
-并在治疗时执行它text_stats
作为一个包装。结果,__main__.py
能不能使用一个相对导入来导入计数器
.
两者之间的区别__init__ . py
而且__main__.py
正如您回忆的那样,Python执行__init__ . py
第一次导入包。你可能想知道为什么我们需要__main__.py
在所有。毕竟,我们不能只执行相同的代码__init__ . py
就像我们在__main__.py
?
简短的回答是“不”。当然,您可以放入任何您想要的代码__init__ . py
,但是Python不会执行一个包,除非它包含__main__.py
.要看到这一点,首先行动text_stats / __main__.py
的text_stats
并尝试再次运行该包:
python3美元-熟睡的巨人/usr/当地的/箱子/python3.6:没有名为text_stats的模块.__main__;“text_stats”是一个包裹\而且不能直接执行
现在移动__main__.py
回到原位并编辑text_stats / __init__ . py
打印一条小信息:
# text_stats / __init__ . py打印(“执行{}. __init__”.格式(__name__))
再次执行包:
python3美元-熟睡的巨人执行text_stats.__init__#单词:2,#字符:13
我们可以看到我们的包装__init__ . py
导入时确实会执行,但是,同样,Python不会让我们执行一个包,除非它包含__main__.py
.
推荐的布局
当我们结束本章时,让我们看看如何最好地组织你的项目。关于如何布局代码并没有严格的规则,但有些选项通常比其他选项要好。我们在这里展示的是一个良好的、通用的结构,它适用于您可能从事的几乎所有项目。
这是基本的项目布局:
project_name/设置.pyproject_name/__init__.pymore_source.pysubpackage1/__init__.py测验/test_code.py
在最顶层有一个带有项目名称的目录。这个目录是不一个包but是一个目录,包含您的包以及像您的包这样的支持文件setup . py
、许可详细信息和测试目录。
下一个目录是实际的包目录。它与顶级目录具有相同的名称。同样,没有规则规定必须如此,但这是一种常见的模式,在导航项目时可以很容易地识别您的位置。您的包包含所有生产代码,包括任何子包。
包和测试之间的分离
的测验
目录包含所有测试。这可能简单到几个Python文件,也可能复杂到多个单元、集成和端到端测试套件。出于多种原因,我们建议将测试放在包之外。
一般来说,测试代码和生产代码的目的非常不同,不应该不必要地耦合。由于您通常不需要将测试与包一起安装,因此这可以防止打包工具将它们捆绑在一起。此外,更奇特的是,这种安排确保某些工具不会意外地试图将您的测试视为生产代码。8
与所有事情一样,这个测试目录安排可能不适合您的需要。当然,您会找到一些包的例子,这些包将它们的测试包含为子包。9如果您发现需要将所有或部分测试包含在一个子包中,那么您绝对应该这样做。
务实的出发点
没有什么比这更重要的了。这是一个非常简单的结构,但它可以很好地满足大多数需求。它可以作为更复杂的项目结构的一个很好的起点,这是我们在开始新项目时通常使用的结构。
模块是单例的
单例模式是软件开发中最广为人知的模式之一,这在很大程度上是因为它非常简单,并且在某些方面提供了比可怕的全局变量更好的选择。单例模式的目的是将一个类型的实例数量限制为一个。例如,在整个应用程序中很容易访问的项目的单个注册表。10
如果你发现你在Python中需要一个单例,一个简单而有效的实现方法是作为一个模块级属性。因为模块只执行一次,这就保证了你的单例只被初始化一次。由于模块是按照定义良好的用户控制顺序进行初始化的,因此您可以对当你的单例将被初始化。这为实现实用的单例对象奠定了坚实的基础。
考虑一个简单的单例注册表,在registry.py
,来电者可留下姓名:
# registry.py_registry=[]def注册(的名字):_registry.附加(的名字)defregistered_names():返回iter(_registry)
调用者会这样使用它:
进口注册表注册表.注册(“我的名字”)为的名字在注册表.registered_names():打印(的名字)
第一次注册表
为导入,则_registry
列表已初始化。然后,每一个呼叫注册
而且registered_names
将在完全保证已正确初始化的情况下访问该列表。
您可以回想起在中的前导下划线
_registry
Python成语是否表明这一点_registry
是不应直接访问的实现详细信息。
这个简单的模式利用了Python健壮的模块语义,是一种以安全可靠的方式实现单例的有用方法。
总结
包在Python中是一个重要的概念,在本章中,我们已经介绍了与实现和使用包相关的大多数主要主题。让我们回顾一下之前学过的主题:
- 包:
- 包是一种特殊类型的模块
- 与普通模块不同,包可以包含其他模块,包括其他包。
- 包层次结构是组织相关代码的强大方法。
- 包有一个
__path__
成员,该序列指定从其中加载包的目录。 - 一个简单的标准项目结构包括一个存放非python文件的位置、项目的包和一个专用的测试子包。
sys.path
:sys.path
是Python搜索模块的目录列表。sys.path
是普通列表,可以像任何其他列表一样修改和查询。- 如果启动Python时不带参数,则在前面放置一个空字符串
sys.path
.这指示Python从当前目录导入模块。 - 将目录追加到
sys.path
在运行时允许从这些目录导入模块。
PYTHONPATH环境
:PYTHONPATH环境
包含目录列表的环境变量。- 的格式
PYTHONPATH环境
和for一样吗路径
在你的系统上。它在Windows上是一个分号分隔的列表,在Linux或Mac OS X上是一个冒号分隔的列表。 - 的内容
PYTHONPATH环境
作为条目添加到sys.path
.
__init__ . py
:- 普通包通过放置一个名为
__init__ . py
进入一个目录。 - 的
__init__ . py
导入包时执行包的文件。 __init__ . py
为了方便,文件可以将属性从子模块提升到更高的名称空间。
- 普通包通过放置一个名为
- 相对进口:
- 相对导入允许您在包中导入模块,而无需指定完整的模块路径。
- 的相对导入必须使用
从模块导入名称
进口形式。 - 相对导入的“from”部分至少以一个点开始。
- 相对导入中的每个点表示一个包含包。
- 相对导入中的第一个点表示“包含此模块的包”。
- 相对导入对于减少类型输入很有用。
- 在某些情况下,相对导入可以提高可修改性。
- 一般来说,最好避免相对导入,因为它们会使代码更难理解。
- 名称空间包:
- 命名空间包是一个跨多个目录的包。
- 命名空间包将在PEP420中进行描述。
- 命名空间包不使用
__init__ . py
文件。 - 当Python路径中的一个或多个目录与导入请求匹配,而没有正常的包或模块与该请求匹配时,将创建命名空间包。
- 贡献给命名空间包的每个目录都列在包的目录中
__path__
属性。
- 可执行文件目录:
- 创建可执行目录
__main__.py
文件在目录中。 - 通过在命令行上将目录传递给Python可执行文件,可以使用Python执行目录。
- 当
__main__.py
执行其__name__
属性设置为__main__
. - 当
__main__.py
执行时,它的父目录自动添加到sys.path
. - 的
如果__name__ == '__main__':
构造是冗余的__main__.py
文件。 - 可执行目录可以被压缩成同样可以执行的zip文件。
- 可执行目录和zip文件是分发Python程序的方便方式。
- 创建可执行目录
- 模块:
- 模块可以通过将它们传递给Python来执行
- m
论点。 - 的
__all__
属性是一个字符串列表,指定何时导出的名称从模块导入
使用。 - 模块级属性为实现单例提供了良好的机制。
- 模块具有定义良好的初始化语义。
- 模块可以通过将它们传递给Python来执行
- 杂项:
- 标准的
gzip
模块允许你使用GZIP压缩文件。 - 标准的
bz2
模块允许你处理使用BZ2压缩的文件。
- 标准的