命令空间包
/ / / 阅读数:13002前言
我在解决用户遇到的一个 lyanna 问题时发现的一个之前不了解知识点,用本篇记录下来。
我学习 Python 的包内容时只有常规包,也就是以一个包含__init__.py
文件的目录形式实现。以一个包含__init__.py
文件的目录形式实现:
❯ tree regular regular ├── __init__.py ├── a │ └── __init__.py └── b └── __init__.py |
如果没有这个__init__.py
文件就会造成导入失败 (python 2):
❯ rm regular/__init__.py ❯ ipython2 Python 2.7.16 (default, Nov 9 2019, 05:55:08) In : import regular --------------------------------------------------------------------------- ImportError Traceback (most recent call last) <ipython-input-1-3dca75a44ca9> in <module>() ----> 1 import regular ImportError: No module named regular In : import regular.a --------------------------------------------------------------------------- ImportError Traceback (most recent call last) <ipython-input-2-2f312ff46378> in <module>() ----> 1 import regular.a ImportError: No module named regular.a |
这非常符合预期 (或者说,习惯了这种设定),不过本文说的是在 Python 3 中的效果:
❯ ipython3 Python 3.7.1 (default, Dec 13 2018, 22:28:16) In : import regular In : regular Out: <module 'regular' (namespace)> In : import regular.a In : regular.a Out: <module 'regular.a' from '/Users/dongwm/mp/2020-01-02/regular/a/__init__.py'> In : regular.a.DATA Out: 'a' In : regular.b.DATA --------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-8-2964870c96fb> in <module> ----> 1 regular.b.DATA AttributeError: module 'regular' has no attribute 'b' In : import regular.b In : regular.b.DATA Out: 'b' |
也就是说,在 Python 3 下即便没有__init__.py
也能正常 import 成功,不过模块会显示成<module 'XX' (namespace)>
这样,另外是对于其子包的使用不受影响。
那么 Python 是怎么做到的呢?
命名空间包 (Namespace package)
这个特性是 Python 3.3 时引入的,PEP 链接: PEP420 。
一个文件夹中没有定义__init__.py
也可以被导入的,只不过它不是以 Python 包的形式导入,而是以命名空间包 (Namespace package) 的形式被导入,所以显示成上面看到的<module 'XX' (namespace)>
这样。
不过,利用命名空间包的主要价值是能导入目录分散的代码。
通过豆瓣的用法来理解
豆瓣 开源了一些 Python 的项目,其中有一些内部版本还在广泛的在各项目中使用,不过我们可以拿开源的来体验一下问题,我们先安装 2 个包吧:
❯ virtualenv venv --python=python2.7 ❯ source venv/bin/activate ❯ git clone https://github.com/douban/douban-utils ❯ cd douban-utils/ ❯ python setup.py install ❯ cd ../ ❯ git clone https://github.com/douban/douban-sqlstore ❯ cd douban-sqlstore ❯ python setup.py install ❯ pip install mysqlclient # douban-sqlstore依赖的MySQL-python已经不再维护,换一个 ❯ cd .. |
现在看看怎么导入:
❯ pip install ipython==5.2 # IPython 6.X开始只支持Python 3了 ❯ venv/bin/ipython In : from douban.sqlstore import SqlStore In : from douban.utils import ptrans |
这 2 个导入语句的代码在不同的包中,但是 douban 是共用的空间。为什么用豆瓣这么个 namespace 呢?
这个在延伸阅读链接 2,也就是 Python Cookbook 里面被提到过。如果你所在公司或者团队有大量的代码,由不同的人来分散地维护,那么可以把其中不同的部分组织为文件目录,但好的实践是能用共同的包前缀将所有组件连接起来,不是将每一个部分作为独立的包来安装。
这样是不能用一开始提到的那个目录名字为 regular 的常规包,需要使用命名空间包
命名空间包的三种风格
本文的重点啦:
pkgutil 风格
所谓风格其实就是用了那个 Python 模块或者特性实现命名空间,pkgutil 风格就是在每个子包里面的__init__.py
里面添加如下的代码:
❯ cat pkgutil_style/a/__init__.py __path__ = __import__('pkgutil').extend_path(__path__, __name__) |
然后分别安装并进入交互模式:
❯ python pkgutil_style/a/setup.py install ❯ python pkgutil_style/b/setup.py install |
setup.py 非常简单,就是取了个不冲突的包名。然后体验一下:
❯ venv/bin/ipython In : from pkgutil_style.a import DATA In : DATA Out: 'aa' In : from pkgutil_style.b import DATA In : DATA Out: 'bb' |
pkg_resources 风格
它和 pkgutil 风格的区别就是子包里面的__init__.py
里面添加的是如下代码:
__import__('pkg_resources').declare_namespace(__name__) |
效果和上面一样。这种风格称为 setuptools-style。
上述 2 种风格在豆瓣项目中的已经体现了 (延伸阅读链接 3):
try: __import__('pkg_resources').declare_namespace(__name__) except ImportError: from pkgutil import extend_path __path__ = extend_path(__path__, __name__) |
naive 风格 (Python3.3+)
这是在 Python 3 时才可用的隐式的命名包的风格,也就是在命名空间下没有__init__.py
:
❯ tree naive_style -L 2 naive_style # 这里没有⬅️ ├── a │ ├── __init__.py │ └── setup.py └── b ├── __init__.py └── setup.py |
不过要注意,setup.py (除了明确使用 packages 列出包) 不能使用setuptools.find_packages()
,而是要用setuptools.find_namespace_packages()
:
❯ cat naive_style/a/setup.py from setuptools import setup, find_namespace_packages setup( name='pkg_3a', version='1', description='', long_description='', packages=find_namespace_packages(), zip_safe=False, ) |
怎么确认一个包是不是 naive 风格呢?如果__file__
属性为 None,那包是个命名空间:
In : import naive_style In : import naive_style.a In : naive_style Out: <module 'naive_style' (namespace)> In : naive_style.__file__ In : naive_style.a.__file__ Out: '/Users/dongweiming/mp/2020-01-02/naive_style/a/__init__.py' |
PS: 注意这里和 Python Cookbook 里面说的不一样.
代码目录
本文代码可以在 mp 项目 找到