专栏名称: Python之美
《Python web开发实战》作者的公众号。发现Python之美,主要包含Web开发、Python进阶、架构设计、Python开发招聘信息等方面内容
目录
相关文章推荐
51好读  ›  专栏  ›  Python之美

命名空间包

Python之美  · 公众号  · Python  · 2020-01-02 18:38

正文

请到「今天看啥」查看全文


前言

我在解决用户遇到的一个lyanna问题时发现的一个之前不了解知识点,用本篇记录下来。

我学习Python的包内容时只有常规包,也就是以一个包含 __init__ . py 文件的目录形式实现。以一个包含 __init__ . py 文件的目录形式实现:

  1. tree regular

  2. regular

  3. ├── __init__.py

  4. ├── a

  5.    └── __init__.py

  6. └── b

  7. └── __init__.py

如果没有这个 __init__ . py 文件就会造成导入失败(python 2):

  1. rm regular/__init__.py


  2. ipython2

  3. Python 2.7.16 (default, Nov 9 2019, 05:55 :08)


  4. In : import regular

  5. ---------------------------------------------------------------------------

  6. ImportError Traceback (most recent call last)

  7. <ipython-input-1-3dca75a44ca9> in <module>()

  8. ----> 1 import regular


  9. ImportError: No module named regular


  10. In : import regular.a

  11. ---------------------------------------------------------------------------

  12. ImportError Traceback (most recent call last)

  13. <ipython-input-2-2f312ff46378> in <module>()

  14. ----> 1 import regular.a


  15. ImportError: No module named regular.a

这非常符合预期(或者说,习惯了这种设定),不过本文说的是在Python 3中的效果:

  1. ipython3

  2. Python 3.7.1 (default, Dec 13 2018, 22:28:16)


  3. In : import regular


  4. In : regular

  5. Out: <module 'regular' (namespace)>


  6. In : import regular.a


  7. In : regular.a

  8. Out: <module 'regular.a' from '/Users/dongwm/mp/2020-01-02/regular/a/__init__.py'>


  9. In : regular. a.DATA

  10. Out: 'a'


  11. In : regular.b.DATA

  12. ---------------------------------------------------------------------------

  13. AttributeError Traceback (most recent call last)

  14. <ipython-input-8-2964870c96fb> in <module>

  15. ----> 1 regular.b.DATA


  16. AttributeError: module 'regular' has no attribute 'b'


  17. In : import regular.b


  18. In : regular.b.DATA

  19. 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个包吧:

  1. virtualenv venv --python=python2.7

  2. source venv/bin/activate

  3. git clone https://github.com/douban/douban-utils

  4. cd douban-utils/

  5. python setup.py install

  6. cd ../

  7. git clone https://github. com/douban/douban-sqlstore

  8. cd douban-sqlstore

  9. python setup.py install

  10. pip install mysqlclient # douban-sqlstore依赖的MySQL-python已经不再维护,换一个

  11. cd ..

现在看看怎么导入:

  1. pip install ipython==5.2 # IPython 6.X开始只支持Python 3了

  2. venv/bin/ipython

  3. In : from douban.sqlstore import SqlStore


  4. In : from douban.utils import ptrans

这2个导入语句的代码在不同的包中,但是douban是共用的空间。为什么用豆瓣这么个namespace呢?

这个在延伸阅读链接2,也就是Python Cookbook里面被提到过。如果你所在公司或者团队有大量的代码,由不同的人来分散地维护,那么可以把其中不同的部分组织为文件目录,但好的实践是能用共同的包前缀将所有组件连接起来,不是将每一个部分作为独立的包来安装。

这样是不能用一开始提到的那个目录名字为regular的常规包,需要使用命名空间包

命名空间包的三种风格

本文的重点啦:

pkgutil风格

所谓风格其实就是用了那个Python模块或者特性实现命名空间,pkgutil风格就是在每个子包里面的 __init__ . py 里面添加如下的代码:

  1. cat pkgutil_style/a/__init__.py

  2. __path__ = __import__('pkgutil').extend_path(__path__, __name__)

然后分别安装并进入交互模式:

  1. python pkgutil_style/a/setup.py install

  2. python pkgutil_style/b/setup.py install

setup.py非常简单,就是取了个不冲突的包名。然后体验一下:

  1. venv/bin/ipython

  2. In : from pkgutil_style.a import DATA


  3. In : DATA

  4. Out: 'aa'


  5. In : from pkgutil_style.b import DATA


  6. In : DATA

  7. Out: 'bb'

pkg_resources风格

它和pkgutil风格的区别就是子包里面的 __init__ . py 里面添加的是如下代码:

  1. __import__('pkg_resources').declare_namespace(__name__)

效果和上面一样。这种风格称为setuptools-style。

上述2种风格在豆瓣项目中的已经体现了(延伸阅读链接3):

  1. try:

  2. __import__('pkg_resources').declare_namespace(__name__)

  3. except ImportError:

  4. from pkgutil import extend_path

  5. __path__ = extend_path(__path__, __name__)

naive风格(Python3.3+)

这是在Python 3时才可用的隐式的命名包的风格,也就是在命名空间下没有 __init__ . py :

  1. tree naive_style -L 2

  2. naive_style # 这里没有⬅️

  3. ├── a

  4.    ├── __init__.py

  5.    └── setup.py

  6. └── b

  7. ├── __init__.py

  8. └── setup.py

不过要注意,setup.py(除了明确使用packages列出包)不能使用 setuptools . find_packages () ,而是要用 setuptools . find_namespace_packages () :

  1. cat naive_style/a/setup.py

  2. from setuptools import setup, find_namespace_packages


  3. setup(

  4. name='pkg_3a',

  5. version='1',

  6. description='',

  7. long_description='',

  8. packages=find_namespace_packages(),

  9. zip_safe=False,

  10. )

怎么确认一个包是不是naive风格呢?如果 __file__ 属性为None,那包是个命名空间:

  1. In : import naive_style


  2. In : import naive_style.a


  3. In : naive_style

  4. Out: <module 'naive_style' (namespace)>


  5. In : naive_style.__file__


  6. In : naive_style.a.__file__

  7. Out: '/Users/dongweiming/mp/2020-01-02/naive_style/a/__init__.py'

PS: 注意这里和Python Cookbook里面说的不一样.

代码目录

本文代码可以在 mp 项目 找到

延伸阅读

  1. https://www.python.org/dev/peps/pep-0420/

  2. https://python3-cookbook.readthedocs.io/zh CN/latest/c10/p05 separate directories import by namespace.html

  3. https://github.com/douban/douban-sqlstore/blob/master/douban/ init .py

  4. https://packaging.python.org/guides/packaging-namespace-packages/








请到「今天看啥」查看全文