第10章 模块和包
10.1 把模块按层次结构组织成包
- 把代码在文件系统上进行组织,确保每一个目录下定义一个__init__.py文件
- 当模块被导入时,它所属的包的__init__.py就会被触发执行
- init.py可以留空,也可以用来进行子模块的加载
10.2 对所有符号的导入进行精确控制
- 使用from module import * 可以导入模块中的所有符号
- 可以在模块中定义__all__变量(列表),就可以控制可被导入的符号,例如:
- 在一个模块中:
def foo():
print("Hello World")
def bar():
print("Bye World")
__all__ = ["foo"]
- 在另一个模块中使用*导入:
from test import *
bar()
- 报错:
NameError: name 'bar' is not defined
- 注意:只有在使用“*”导入时,__all__才能起到控制作用,如果直接以名称导入,是可以正常执行的:
# 以下代码正常执行
from test import bar
bar()
10.3 用相对名称来导入包中的子模块
- 想从一个子模块中导入另一个子模块,又不想硬编码包的名称。
- 例如,有下面的包:
- top
- __init__.py
- testA
- __init__.py
- foo.py
- bar.py
- testB
- __init__.py
- hi.py
- ho.py
- 相同目录下的导入,例如在foo.py中导入bar.py:
from . import bar
- 不同目录中的导入,例如在hi.py中导入bar.py:
from ..testA import bar
- 上面的代码中使用了相对名称,没有包含顶层包top
10.4 将模块分解成多个文件
- 例如有一个模块test.py是这样的:
class A:
def __init__(self):
print("Hey, I'm class A")
class B(A):
def __init__(self):
print("Hey, I'm class B")
- 想将A和B类放到不同模块中定义,但是此时已经在其他模块中导入了它们,不想修改这些代码。
- 可以把test改成这样:
- test
- __init__.py
- a.py
- b.py
- init.py:
from .a import A
from .b import B
- 在模块a.py和b.py中分别就是A和B类的定义
- 由此我们发现,之前对模块test的导入是不必作任何修改的:
from test import A, B
import test.A
import test.B
...
- 惰性导入:在__init__.py中,我们一次性加载了所有子模块, 但如果模块很大的话,我们往往希望只有在实际需要的时候才加载,因此可以把__init__.py修改成:
def A():
from .a import A
return A()
def B():
from .b import B
return B()
10.5 让各个目录下的代码在统一的命名空间下导入
- 一个代码库中不同的包是由不同开发人员开发维护的,我们希望把这些包用一个统一的前缀命名
- 例如,在顶层目录下有两个包:
- foo/
- spam/
- hello.py
- bar/
- spam/
- hi.py
- 注意到在spam下面都没有定义__init__.py
- 下面把foo和bar都添加到Python模块查询路径中:
import sys
sys.path.extend(['foo', 'bar'])
- 然后就可以对spam执行导入了:
from spam import hello, hi
-
不同包中的模块可以通过同一条语句导入
-
这种特性叫“命名控件包”,对于不同包中的代码起到了合并的作用,在统一的命名空间下进行管理。
-
这样任何人创建的包下只要包含了“spam”, 并且把这个包添加到sys.path中,就可以用类似以上的方式导入这个包的内容
-
检查一个包是否为命名空间包:
- 打印一个包的字符串表达:
>>> spam <module 'spam' (namespace)>
- 有namespace字样,就是命名空间包
- 查看它的__file__属性,如果缺失,那么也代表它是命名空间包
10.6 重新加载模块
- 重新加载一个已加载果的模块,使用importlib.reload()实现(书中介绍的imp.reload()已过时):
>>> import importlib
>>> importlib.reload(a)
- 这种方法多用于开发和调试阶段,应尽量避免在生产环境中重新加载模块
- reload模块会擦除模块的__dict__中的内容,并重新执行模块的源代码
- 如果使用from module import name这样的语句,reload不会生效;
- 只有import module.name 的语句reload才有效
10.7 让目录或zip文件成为可运行的脚本
- 如果项目规模庞大,涉及多个目录,可以使用__main__.py, Python解释器会把这个文件当成主程序来执行:
- app
- foo.py
- bar.py
- hello.py
- __main__.py
- 在顶层目录,执行:
python3 app
- 就可以运行整个应用
- 如果将一个应用放到压缩包.zip文件中,上述方法依然可行:
python3 app.zip
10.8 读取包中的数据文件
- 有两个问题:
- 使用内建的IO函数(如open())无法读取保存在归档(archive)中的数据文件,因为包通常被安装为.zip或.egg文件
- 对open()函数,我们要提供包的绝对路径,或是相对解释器的路径,我们不想硬编码绝对路径,也无法控制解释器的所在路径,这么做使代码的可移植性较低。
- pkgutil.get_data()函数接收一个包含包名的字符串,第二个参数是要获取的数据文件相对于包的名称
10.9 添加目录到sys.path中
- Python解释器会在sys.path中查找我们导入的包,一些包没有在其中列出,因此想在代码中把它们添加进去
- 以下列出了各种方法:
# 1. 设置系统的环境变量:PYTHONPATH
# 对于MacOS,在~/.bash_profile 中修改并使之生效
# 2. 创建一个.pth文件, 放到site-packages目录下
# 3. 代码中动态添加
import sys
sys.path.insert(0, '/some/dir')
- 前两种方法在运行程序前就设置,第3种方法在程序运行时设置并且用了硬编码路径的方式
- 应当使用一些策略避免使用这种硬编码的方式,如:
import sys
from os.path import abspath, join, dirname
sys.path.insert(0, abspath(dirname('__file__'), 'src'))
10.10 使用字符串中给定的名称来导入模块
- 使用importlib.import_module():
import importlib
math = importlib.import_module("math")
math.sin(2)
10.13 安装只为自己所用的包
- 有以下两种情形:
- 想安装一个第三方的包,但是没有权限在系统Python中安装其他的包
- 安装一个给自己使用的包,而不是给系统中其他用户使用
- Python有一个用户级的目录,仅属于当前用户所有。要把包强制安装到这个目录下,只要在安装命令后添加–user:
pip install --user some-package
python3 setup.py install -user
- 一般而言,包会被安装在系统级的site-package目录下,利用–user选项可以把包安装到自己的目录中
- 使用虚拟环境也可以完成类似的事情
10.15 发布自己定义的包
- 要发布一个自定义的包,要经过以下几个步骤:
- 为自己的库起一个独一无二的名字,清理目录结构:
- project/ - README.txt - Doc/ - doc.txt - project/ - __init__.py - foo.py - bar.py - utils/ - __init__.py - spam.py - grok.py - examples/ - hello.py ...
- 编写一个setup文件,放置于顶层目录
from setuptools import setup, find_packages setup( name = "test", version = "1.0", keywords = ("test", "xxx"), description = "eds sdk", long_description = "eds sdk for python", license = "MIT Licence", url = "http://test.com", author = "test", author_email = "test@gmail.com", packages = find_packages(), include_package_data = True, platforms = "any", install_requires = [], scripts = [], entry_points = { 'console_scripts': [ 'test = test.help:main' ] } )
- 创建一个MANIFEST.in文件,在其中列出希望被包含在包中的非源码文件,放置于顶层目录:
# MANIFEST.in include *.txt recursive-include examples * recursive-include Doc *
- 创建一个源码级的分发包(生成一个压缩文件):
python3 setup.py sdist
- 在PyPi上发布!