10

命令空间包

 4 years ago
source link: http://www.dongwm.com/post/namespace-packages/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前言

我在解决用户遇到的一个 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  # 这里没有:arrow_left:
├── 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 项目 找到

延伸阅读


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK