Python CookBook 学习笔记
第十章 模块与包¶
值得注意的小节:
- 10.11
- 10.12
10.11 10.12 未完成
模块与包是任何大型程序的核心,就连Python 安装程序本身也是一个包。本章重点涉及有关模块和包的常用编程技术,例如如何组织包、把大型模块分割成多个文件、创建命名空间包。同时,也给出了让你自定义导入语句的秘籍。
Table of Contents:
10.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/graphics/formats/__init__.py
将在文件 graphics/formats/jpg.py 导入之前导入。
注意这里的区别是 上层目录的 __init__.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
的其他常用用法包括将多个文件合并到一个逻辑命名空间,这将在10.4小节讨论。
敏锐的程序员会发现,即使没有 __init__.py
文件存在,python 仍然会导入包。如果你没有定义 __init__.py
时,实际上创建了一个所谓的“命名空间包”,这将在10.5小节讨论,但是最好还是创建一个 __init__.py
文件。
10.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。
10.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 这样使用绝对路径名的不利之处是这将顶层包名硬编码到你的源码中。如果你想重新组织它,会很不方便。同样,硬编码的名称会使移动代码变得困难。
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
10.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 文件来将每部分粘合在一起。
作为这一章节的延伸,将介绍延迟导入。如图所示,init.py文件一次导入所有必需的组件的。但是对于一个很大的模块,可能你只想组件在需要时被加载。 要做到这一点,init.py有细微的变化:
# __init__.py
def A():
from .a import A
return A()
def B():
from .b import B
return B()
在这个版本中,类 A 和类 B 被替换为在第一次访问时加载所需的类的函数。对于用户,这看起来不会有太大的不同。
延迟加载的主要缺点是继承和类型检查可能会中断
if isinstance(x, mymodule.A): # Error
if isinstance(x, mymodule.a.A): # Ok
10.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 的时候).
包命名空间的一个重要特点是任何人都可以用自己的代码来扩展命名空间。
一个包是否被作为一个包命名空间的主要方法是检查其 __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)>
10.7 运行目录或压缩文件¶
问题:有一个已成长为包含多个文件的应用,它已远不再是一个简单的脚本,你想向用户提供一些简单的方法运行这个程序。
解决方案:如果你的应用程序已经有多个文件,你可以把你的应用程序放进它自己的目录并添加一个 __main__.py
文件。
myapplication/
spam.py
bar.py
grok.py
__main__.py
如果 __main__.py
存在,你可以简单地在顶级目录运行 Python 解释器:
bash % python3 myapplication
解释器将执行 __main__.py
文件作为主程序。
如果你将你的代码打包成 zip 件,这种技术同样也适用
bash % ls
spam.py bar.py grok.py __main__.py
bash % zip -r myapp.zip *.py
bash % python3 myapp.zip
... output from __main__.py ...
讨论:创建一个目录或zip文件并添加main.py文件来将一个更大的Python应用打包是可行的。这和作为标准库被安装到Python库的代码包是有一点区别的。相反,这只是让别人执行的代码包。
由于目录和zip文件与正常文件有一点不同,你可能还需要增加一个 shell 脚本,使执行更加容易。例如,如果代码文件名为myapp.zip,你可以创建这样一个顶级脚本:
#!/usr/bin/env python3 /usr/local/bin/myapp.zip
10.8 读取位于包中的数据文件¶
问题:你的包中包含代码需要去读取的数据文件。你需要尽可能地用最便捷的方式来做这件事。
解决方案:假设你的包中的文件组织成如下:
mypackage/
__init__.py
somedata.dat
spam.py
现在假设 spam.py 文件需要读取 somedata.dat 文件中的内容。
# spam.py
import pkgutil
data = pkgutil.get_data(__package__, 'somedata.dat')
由此产生的变量是包含该文件的原始内容的字节字符串。
讨论:要读取数据文件,你可能会倾向于编写使用内置的I/O功能的代码,如open()。但是这种方法也有一些问题。
首先,一个包对解释器的当前工作目录几乎没有控制权。因此,编程时任何I/O操作都必须使用绝对文件名。由于每个模块包含有完整路径的file变量,这弄清楚它的路径不是不可能,但它很凌乱。
第二,包通常安装作为.zip或.egg文件,这些文件并不像在文件系统上的一个普通目录里那样被保存。因此,你试图用open()对一个包含数据文件的归档文件进行操作,它根本不会工作。
pkgutil.get_data() 函数是一个读取数据文件的高级工具,不用管包是如何安装以及安装在哪。它只是工作并将文件内容以字节字符串返回给你。
get_data()的第一个参数是包含包名的字符串。你可以直接使用包名,也可以使用特殊的变量,比如 __package__
。第二个参数是包内文件的相对名称。如果有必要,可以使用标准的Unix命名规范到不同的目录,只有最后的目录仍然位于包中。
10.9 将文件夹加入到sys.path¶
问题:你无法导入你的Python代码因为它所在的目录不在 sys.path 里。你想将添加新目录到Python路径,但是不想硬链接到你的代码。
解决方案:有两种常用的方式将新目录添加到sys.path。第一种,你可以使用PYTHONPATH环境变量来添加。
bash % env PYTHONPATH=/some/dir:/other/dir python3
Python 3.3.0 (default, Oct 4 2012, 10:17:33)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.path
['', '/some/dir', '/other/dir', ...]
在自定义应用程序中,这样的环境变量可在程序启动时设置或通过 shell 脚本。
第二种方法是创建一个.pth文件,将目录列举出来
# myapplication.pth
/some/dir
/other/dir
这个 .pth 文件需要放在某个 Python 的 site-packages 目录,通常位于 /usr/local/lib/python3.3/site-packages 或者 ~/.local/lib/python3.3/sitepackages。当解释器启动时,.pth 文件里列举出来的存在于文件系统的目录将被添加到 sys.path。安装一个 .pth 文件可能需要管理员权限,如果它被添加到系统级的 Python 解释器。
讨论:比起费力地找文件,你可能会倾向于写一个代码手动调节 sys.path 的值,但这并不是一个很好的选择
import sys
sys.path.insert(0, '/some/dir')
sys.path.insert(0, '/other/dir')
虽然这能“工作”,它是在实践中极为脆弱,应尽量避免使用。这种方法的问题是,它将目录名硬编码到了你的源代码。如果你的代码被移到一个新的位置,这会导致维护问题。更好的做法是在不修改源代码的情况下,将path配置到其他地方。如果您使用模块级的变量来精心构造一个适当的绝对路径,有时你可以解决硬编码目录的问题,比如 __file__
。举个例子:
import sys
from os.path import abspath, join, dirname
sys.path.insert(0, join(abspath(dirname('__file__')), 'src'))
这将 src 目录添加到 path 里,和执行插入步骤的代码在同一个目录里。
site-packages 目录是第三方包和模块安装的目录。如果你手动安装你的代码,它将被安装到 site-packages 目录。虽然用于配置 path 的 .pth 文件必须放置在 site-packages 里,但它配置的路径可以是系统上任何你希望的目录。因此,你可以把你的代码放在一系列不同的目录,只要那些目录包含在 .pth 文件里。
10.10 通过字符串名导入模块¶
问题:你想导入一个模块,但是模块的名字在字符串里。你想对字符串调用导入命令。
解决方案:使用 importlib.import_module() 函数来手动导入名字为字符串给出的一个模块或者包的一部分。
>>> import importlib
>>> math = importlib.import_module('math')
>>> math.sin(2)
0.9092974268256817
>>> mod = importlib.import_module('urllib.request')
>>> u = mod.urlopen('http://www.python.org')
import_module 只是简单地执行和 import 相同的步骤,但是返回生成的模块对象。你只需要将其存储在一个变量,然后像正常的模块一样使用。
import_module() 也可用于相对导入。但是,你需要给它一个额外的参数。
import importlib
# Same as 'from . import b'
b = importlib.import_module('.b', __package__)
讨论:使用 import_module ()手动导入模块的问题通常出现在以某种方式编写修改或覆盖模块的代码时候。例如,也许你正在执行某种自定义导入机制,需要通过名称来加载一个模块,通过补丁加载代码。
在旧的代码,有时你会看到用于导入的内建函数 __import__()
。尽管它能工作,但是 importlib.import_module() 通常更容易使用。
10.13 安装私有的包¶
问题:想要安装一个第三方包,但是没有权限将它安装到系统 Python 库中去。或者,你可能想要安装一个供自己使用的包,而不是系统上面所有用户。
解决方案:Python 有一个用户安装目录,通常类似 ”~/.local/lib/python3.3/site-packages”。 要强制在这个目录中安装包,可使用安装选项 “–user”。
python3 setup.py install --user
或者
pip install --user packagename
在 sys.path 中用户的 “site-packages” 目录位于系统的 “site-packages” 目录之前。 因此,你安装在里面的包就比系统已安装的包优先级高 (尽管并不总是这样,要取决于第三方包管理器,比如 distribute 或pip)。
讨论:通常包会被安装到系统的 site-packages 目录中去,路径类似 “/usr/local/lib/python3.3/site-packages”。 不过,这样做需要有管理员权限并且使用 sudo 命令。
安装包到用户目录中通常是一个有效的方案,它允许你创建一个自定义安装。
另外,你还可以创建一个虚拟环境。
10.14 创建新的 Python 环境¶
问题:你想创建一个新的 Python 环境,用来安装模块和包。 不过,你不想安装一个新的 Python 克隆,也不想对系统 Python 环境产生影响。
解决方案:你可以使用 pyvenv
命令创建一个新的“虚拟”环境。 这个命令被安装在 Python 解释器同一目录,或 Windows 上面的 Scripts 目录中。下面是一个例子:
bash % pyvenv Spam
传给 pyvenv
命令的名字是将要被创建的目录名。当被创建后,Span 目录像下面这样:
bash % ls
bin include lib pyvenv.cfg
在 bin 目录中,你会找到一个可以使用的 Python 解释器:
bash % Spam/bin/python3
这个解释器的特点就是他的 site-packages
目录被设置为新创建的环境。 如果你要安装第三方包,它们会被安装在那里,而不是通常系统的 site-packages
目录。
讨论:有了一个新的虚拟环境,下一步就是安装一个包管理器,比如 distribute
或 pip
。 但安装这样的工具和包的时候,你需要确保你使用的是虚拟环境的解释器。 它会将包安装到新创建的 site-packages
目录中去。
尽管一个虚拟环境看上去是 Python 安装的一个复制, 不过它实际上只包含了少量几个文件和一些符号链接。 所有标准库函文件和可执行解释器都来自原来的 Python 安装。 因此,创建这样的环境是很容易的,并且几乎不会消耗机器资源。
默认情况下,虚拟环境是空的,不包含任何额外的第三方库。如果想将一个已经安装的包作为虚拟环境的一部分, 可以使用 “–system-site-packages”
选项来创建虚拟环境
bash % pyvenv --system-site-packages Spam
10.15 分发包¶
问题:你已经编写了一个有用的库,想将它分享给其他人。
解决方案:如果你想分发你的代码,第一件事就是给它一个唯一的名字,并且清理它的目录结构。 例如,一个典型的函数库包会类似下面这样:
projectname/
README.txt
Doc/
documentation.txt
projectname/
__init__.py
foo.py
bar.py
utils/
__init__.py
spam.py
grok.py
examples/
helloworld.py
...
要让你的包可以发布出去,首先你要编写一个 setup.py
,类似下面这样:
# setup.py
from distutils.core import setup
setup(name='projectname',
version='1.0',
author='Your Name',
author_email='you@youraddress.com',
url='http://www.you.com/projectname',
packages=['projectname', 'projectname.utils'],
)
下一步,就是创建一个 MANIFEST.in
文件,列出所有在你的包中需要包含进来的非源码文件:
# MANIFEST.in
include *.txt
recursive-include examples *
recursive-include Doc *
确保 setup.py
和 MANIFEST.in
文件放在你的包的最顶级目录中。 一旦你已经做了这些,你就可以像下面这样执行命令来创建一个源码分发包了:
% bash python3 setup.py sdist
它会创建一个文件比如 ”projectname-1.0.zip”
或 “projectname-1.0.tar.gz”
, 具体依赖于你的系统平台。如果一切正常,这个文件就可以发送给别人使用或者上传至 Python Package Index。
讨论:对于纯 Python 代码,编写一个普通的 setup.py
文件通常很简单。 一个可能的问题是你必须手动列出所有构成包源码的子目录。 一个常见错误就是仅仅只列出一个包的最顶级目录,忘记了包含包的子组件。 这也是为什么在 setup.py
中对于包的说明包含了列表 packages=['projectname', 'projectname.utils']
。
大部分 Python 程序员都知道,有很多第三方包管理器供选择,包括 setuptools、distribute 等等。 有些是为了替代标准库中的 distutils。注意如果你依赖这些包, 用户可能不能安装你的软件,除非他们已经事先安装过所需要的包管理器。 正因如此,你更应该时刻记住越简单越好的道理。 最好让你的代码使用标准的 Python 3 安装。 如果其他包也需要的话,可以通过一个可选项来支持。
对于涉及到 C 扩展的代码打包与分发就更复杂点了。