前言
我在解决用户遇到的一个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成功,不过模块会显示成
'XX'
(
namespace
)
>
这样,另外是对于其子包的使用不受影响。
那么Python是怎么做到的呢?
命名空间包(Namespace package)
这个特性是Python 3.3时引入的,PEP链接: PEP420。
一个文件夹中没有定义
__init__
.
py
也可以被导入的,只不过它不是以Python包的形式导入,而是以命名空间包 (Namespace package) 的形式被导入,所以显示成上面看到的
'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 项目 找到
延伸阅读
-
https://www.python.org/dev/peps/pep-0420/
-
https://python3-cookbook.readthedocs.io/zh
CN/latest/c10/p05
separate
directories
import
by
namespace.html
-
https://github.com/douban/douban-sqlstore/blob/master/douban/
init
.py
-
https://packaging.python.org/guides/packaging-namespace-packages/