一、构建一个模块的层级包

问题

将代码组织成由很多分层模块构成的包。


解决方案

封装成包很简单。在文件系统上组织你的代码,并确保每个目录都定义一个 __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.jpgimport 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-packagebar-package 都加到 python 模块路径:

>>> import sys
>>> sys.path.extend(['foo-package', 'bar-package'])
>>> import spam.blah
>>> import spam.grok
>>>

两个不同的包目录被合并到一起,你可以导入spam.blahspam.grok ,并能够正常工作。


讨论

包命名空间的关键是确保顶级目录中没有 __init__.py 文件来作为共同的命名空间。