一、构建一个模块的层级包
问题
将代码组织成由很多分层模块构成的包。
解决方案
封装成包很简单。在文件系统上组织你的代码,并确保每个目录都定义一个 __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
空着就好。但有些情况可能包含代码。
# graphics/formats/__init__.py
from . import jpg
from . import png
像这样的文件,用户可以通过 import grahpics.formats
代替 import graphics.formats.jpg
和 import graphics.formats.png
。
二、模块的全部导入
问题
使用 from module import *
时,对从模块或包导出的符号进行精确控制。
解决方案
在模块中定义一个变量 __all__
来列出要导出的内容。
# somemodule.py
def spam():
pass
def grok():
pass
blah = 42
# 只导出'spam'和'grok'
__all__ = ['spam', 'grok']
讨论
尽管强烈反对使用 from module import *
,但是在定义了大量变量名的时候还是会使用。因为这样的导入,将导入所有不以下划线开头的。
如果将 __all__
定义为空列表,没有东西将被导入。
如果 __all__
包含未定义的名字,引起 AttributeError(属性错误)。
三、模块的相对路径导入
问题
将代码组织成包,用 import 语句从另一个包中导入子模块。
解决方案
使用包的相对导入。将一个模块导入同一个包的另一个模块。
mypackage/
__init__.py
A/
__init__.py
spam.py
grok.py
B/
__init__.py
bar.py
模块 mypackage.A.spam
要导入同目录下的 grok
。
# mypackage/A/spam.py
from . import grok
模块 mypackage.A.spam
要导入不同目录下的 B.bar
。
# mypackage/A/spam.py
from ..B import bar
.
为当前目录,..B
为目录 ../B
。这种语法只适用于 import。
两个 import 语句都没包含顶层包名,而是使用了 spam.py 的相对路径。
讨论
在包内,既可以使用相对路径,也可以使用绝对路径。
# mypackage/A/spam.py
from mypackage.A import grok # 正确
from . import grok # 正确
import grok # 错误
绝对路径的缺点是将顶层包名硬编码到你的源码中。
相对导入只适用于包中的模块。而在顶层脚本的简单模块,它们不起作用。如果包的部分被作为脚本直接执行,它们将不起作用。
% python mypackage/A/spam.py # 相对导入失败
如果使用 python -m
执行先前脚本,相对导入正确运行。
% python -m mypackage/A/spam.py # 相对导入成功
四、将模块分割成多个文件
问题
将一个模块分割成多个文件。
解决方案
程序模块通过变成包来分割成多个独立的文件。
# mymodule.py
class A:
def spam(self):
print('A.spam')
class B:
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
from mymodule import A, B
对后者而言,让 mymodule 成为一个大的源文件更常见。
第四节展示了如何将多个文件合并成单一的逻辑命名空间。步骤的关键是创建一个包目录,使用 __init__.py
文件将每部分粘合在一起。
作为这一节的延伸,下面介绍延迟导入。之前 __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): # 错误
...
if isinstance(x, mymodule.a.A): # 正确
...
五、利用命名空间导入目录分散的代码
问题
当代码由不同的人来维护,你希望用共同的包前缀将所有组件连接起来。
解决方案
定义一个顶级 Python 包,作为一个大集合分开维护子包。
在不同的目录里统一相同的命名空间,但是要删除用来将组件联合起来的 __init__.py
文件。
foo-package/
spam/
blah.py
bar-package/
spam
grok.py
在上面的两个目录,有共同的命名空间 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
文件来作为共同的命名空间。