1 构建一个模块的层级包
封装成包是很简单的。在文件系统上组织你的代码,并确保每个目录都定义了一个__init__.py
文件。 例如:
graphics/
__init__.py
primitive/
__init__.py
line.py
fill.py
text.py
formats/
__init__.py
png.py
jpg.py
一旦你做到了这一点,你应该能够执行各种import语句,如下:
import graphics.primitive.line
from graphics.primitive import line
import graphics.formats.jpg as jpg
讨论
定义模块的层次结构就像在文件系统上建立目录结构一样容易。 文件__init__.py
的目的是要包含不同运行级别的包的可选的初始化代码。 举个例子,如果你执行了语句import graphics
, 文件graphics/__init__.py
将被导入,建立graphics
命名空间的内容。像import graphics.format.jpg
这样导入,文件graphics/__init__.py
和文件graphics/formats/__init__.py
将在文件graphics/formats/jpg.py
导入之前导入。
绝大部分时候让__init__.py
空着就好,但是有些情况下可能包含代码。 举个例子,__init__.py
能够用来自动加载子模块:
# graphics/formats/__init__.py
from . import jpg
from . import png
像这样一个文件,用户可以仅仅通过import grahpics.formats
来代替import graphics.formats.jpg
以及import graphics.formats.png
。
__init__.py
的其他常用用法包括将多个文件合并到一个逻辑命名空间,这将在4小节讨论。
敏锐的程序员会发现,即使没有__init__.py
文件存在,python仍然会导入包。如果你没有定义__init__.py
时,实际上创建了一个所谓的“命名空间包”,这将在5小节讨论。如果你着手创建一个新的包的话,包含一个__init__.py
文件吧。
2 控制模块被全部导入的内容
当使用from module import *
语句时,希望对从模块或包导出的符号进行限制,即只导出部分内容。
可以定义一个变量__all__
来明确地列出需要导出的内容。
举个例子:
# somemodule.py
def spam():
pass
def grok():
pass
blah = 42
# Only export 'spam' and 'grok'
__all__ = ['spam', 'grok']
讨论
尽管强烈反对使用from module import *
,但是在定义了大量变量名的模块中频繁使用。如果你不做任何事, 这样的导入将会导入所有不以下划线开头的。 另一方面,如果定义了__all__
, 那么只有被列举出的东西会被导出。
如果你将__all__
定义成一个空列表,没有东西将被导入。 如果__all__
包含未定义的名字, 在导入时引起AttributeError
。
3 使用相对路径名导入包中子模块
将代码组织成包,想用import语句从另一个包名没有硬编码过的包中导入子模块。
使用包的相对导入,使一个模块导入同一个包的另一个模块。举个例子,假设文件组织如下:
mypackage/
__init__.py
A/
__init__.py
spam.py
grok.py
B/
__init__.py
bar.py
如果模块mypackage.A.spam
要导入同目录下的模块grok
,它应该包括的import语句如下:
# mypackage/A/spam.py
from . import grok
如果模块mypackage.A.spam
要导入不同目录下的模块B.bar,它应该使用的import语句如下:
# mypackage/A/spam.py
from ..B import bar
两个import语句都没包含顶层包名,而是使用了spam.py的相对路径。
讨论
在包内,既可以使用相对路径也可以使用绝对路径来导入。 举个例子:
# mypackage/A/spam.py
from mypackage.A import grok # OK
from . import grok # OK
import grok # Error (not found)
**像mypackage.A这样使用绝对路径名的不利之处是这将顶层包名硬编码到你的源码中。**如果你想重新组织它,你的代码将更脆,很难工作。 举个例子,如果你改变了包名,你就必须检查所有文件来修正源码。 同样,硬编码的名称会使移动代码变得困难。举个例子,也许有人想安装两个不同版本的软件包,只通过名称区分它们。 如果使用相对导入,那一切都ok,然而使用绝对路径名很可能会出问题。
import语句的 .
和 ..
看起来很滑稽, 但它指定目录名.为当前目录,..B为目录../B。这种语法只适用于import。 举个例子:
from . import grok # OK
import .grok # ERROR
尽管使用相对导入看起来像是浏览文件系统,但是不能到定义包的目录之外。也就是说,使用点的这种模式从不是包的目录中导入将会引发错误。
另外,只能导入顶层模块下的内部模块,不能导入和顶层模块相同目录的模块,如果包的部分被作为脚本直接执行,那相对路径的导入这种方式可能会不起作用。
例如:
% python3 mypackage/A/spam.py # Relative imports fail
另一方面,如果你使用Python的-m选项来执行先前的脚本,相对导入将会正确运行。 例如:
% python3 -m mypackage.A.spam # Relative imports work
也就是说包中的文件(模块)不能单独作为主函数运行。(如果一个模块被直接运行,则此时它自己就是顶层模块,不存在层次结构,所以找不到和其同层的模块和上层模块,但可以使用绝对路径;如果非要使用相对路径,把需要导入的模块放到执行模块的内部模块)
-
from package_A import module_B/package_B
中,先执行的是package_A
中的__init__.py
文件,再执行后面的module_B
文件或者package_B
中的__init__.py
文件。 -
在顶层模块内部的各模块,可以相对导入;这些模块对于顶层模块同目录或者更上层目录则不能相对导入。
最后,不推荐使用相对路径。
4 将模块分割成多个文件
你想将一个模块分割成多个文件。但是你不想将分离的文件统一成一个逻辑模块时使已有的代码遭到破坏。
程序模块可以通过变成包来分割成多个独立的文件。考虑下下面简单的模块:
# mymodule.py
class A:
def spam(self):
print('A.spam')
class B(A):
def bar(self):
print('B.bar')
假设你想mymodule.py分为两个文件,每个定义的一个类。要做到这一点,首先用mymodule目录来替换文件mymodule.py。 这这个目录下,创建以下文件:
mymodule/
__init__.py
a.py
b.py
在a.py文件中插入以下代码:
# a.py
class A:
def spam(self):
print('A.spam')
在b.py文件中插入以下代码:
# b.py
from .a import A
class B(A):
def bar(self):
print('B.bar')
最后,在 __init__.py
中,将2个文件粘合在一起:
# __init__.py
from .a import A
from .b import B
如果按照这些步骤,所产生的包MyModule将作为一个单一的逻辑模块:
>>> import mymodule
>>> a = mymodule.A()
>>> a.spam()
A.spam
>>> b = mymodule.B()
>>> b.bar()
B.bar
>>>
讨论
在这个章节中的主要问题是一个设计问题,不管你是否希望用户使用很多小模块或只是一个模块。举个例子,在一个大型的代码库中,你可以将这一切都分割成独立的文件,让用户使用大量的import语句,就像这样:
from mymodule.a import A
from mymodule.b import B
...
这样能工作,但这让用户承受更多的负担,用户要知道不同的部分位于何处。通常情况下,将这些统一起来,使用一条import将更加容易,就像这样:
from mymodule import A, B
对后者而言,让mymodule成为一个大的源文件是最常见的。但是,这一章节展示了如何合并多个文件合并成一个单一的逻辑命名空间。 这样做的关键是创建一个包目录,使用 __init__.py
文件来将每部分粘合在一起。
当一个模块被分割,你需要特别注意交叉引用的文件名。举个例子,在这一章节中,B类需要访问A类作为基类。用包的相对导入 from .a import A
来获取。
整个章节都使用包的相对导入来避免将顶层模块名硬编码到源代码中。这使得重命名模块或者将它移动到别的位置更容易。(见3小节)
作为这一章节的延伸,将介绍延迟导入。如图所示,__init__.py
文件一次导入所有必需的组件的。但是对于一个很大的模块,可能你只想组件在需要时被加载。 要做到这一点,__init__.py
有细微的变化:
# __init__.py
def A():
from .a import A
return A()
def B():
from .b import B
return B()
在这个版本中,类A和类B被替换为在第一次访问时加载所需的类的函数。对于用户,这看起来不会有太大的不同。 例如:
>>> import mymodule
>>> a = mymodule.A()
>>> a.spam()
A.spam
>>>
延迟加载的主要缺点是继承和类型检查可能会中断。你可能会稍微改变你的代码,例如:
if isinstance(x, mymodule.A): # Error
...
if isinstance(x, mymodule.a.A): # Ok
...
延迟加载的真实例子, 见标准库 multiprocessing/__init__.py
的源码.
5 利用命名空间导入目录分散的代码
你可能有大量的代码,由不同的人来分散地维护。每个部分被组织为文件目录,如一个包。然而,你希望能用共同的包前缀将所有组件连接起来,不是将每一个部分作为独立的包来安装。
从本质上讲,你要定义一个顶级Python包,作为一个大集合分开维护子包的命名空间。这个问题经常出现在大的应用框架中,框架开发者希望鼓励用户发布插件或附加包。
在统一不同的目录里统一相同的命名空间,但是要删去用来将组件联合起来的__init__.py
文件。假设你有Python代码的两个不同的目录如下:
foo-package/
spam/
blah.py
bar-package/
spam/
grok.py
在这2个目录里,都有着共同的命名空间spam。在任何一个目录里都没有__init__.py
文件。
让我们看看,如果将foo-package和bar-package都加到python模块路径并尝试导入会发生什么
>>> import sys
>>> sys.path.extend(['foo-package', 'bar-package'])
>>> import spam.blah
>>> import spam.grok
>>>
两个不同的包目录被合并到一起,你可以导入spam.blah和spam.grok,并且它们能够工作。
讨论
在这里工作的机制被称为“包命名空间”的一个特征。从本质上讲,包命名空间是一种特殊的封装设计,为合并不同的目录的代码到一个共同的命名空间。对于大的框架,这可能是有用的,因为它允许一个框架的部分被单独地安装下载。它也使人们能够轻松地为这样的框架编写第三方附加组件和其他扩展。
包命名空间的关键是确保顶级目录中没有__init__.py
文件来作为共同的命名空间。缺失__init__.py
文件使得在导入包的时候会发生有趣的事情:这并没有产生错误,解释器创建了一个由所有包含匹配包名的目录组成的列表。特殊的包命名空间模块被创建,只读的目录列表副本被存储在其__path__
变量中。 举个例子:
>>> import spam
>>> spam.__path__
_NamespacePath(['foo-package/spam', 'bar-package/spam'])
>>>
在定位包的子组件时,目录__path__
将被用到(例如, 当导入spam.grok或者spam.blah的时候).
包命名空间的一个重要特点是任何人都可以用自己的代码来扩展命名空间。举个例子,假设你自己的代码目录像这样:
my-package/
spam/
custom.py
如果你将你的代码目录和其他包一起添加到sys.path,这将无缝地合并到别的spam包目录中:
>>> import spam.custom
>>> import spam.grok
>>> import spam.blah
>>>
一个包是否被作为一个包命名空间的主要方法是检查其__file__属性。如果没有,那包是个命名空间。这也可以由其字符表现形式中的“namespace”这个词体现出来。
>>> spam.__file__
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'module' object has no attribute '__file__'
>>> spam
<module 'spam' (namespace)>
>>>
更多的包命名空间信息可以查看 [PEP 420](https://www.python.org/dev/peps/pep-0420/。