本文的撰写动因:最近在折腾zope WEB 框架,在研究依赖包安装的过程中偶然发现了setup.py中的namespace package这个概念,发现自己竟是一头雾水,百度了一遍发现并没有中文的详细讲解文章,自己平日里看其它大神的文章受益匪浅,而自己从未对他人做出过什么贡献,遂开通了一个CSDN博客,试着把这个问题说明白。
note 1:本文读者应具备python package、module以及python程序打包的的基本知识
note 2:若有任何错误请指出或叙述不轻的地方,请吐槽-_-,本人将第一时间更正
note 3:本文以python2.7为例,如果有读者使用的版本不同,有些目录名称可能不同
与java类似,PYTHON中也有package的概念,所谓package,就是包含一个或多个python module、package的集合,在系统中以文件夹的形式存在,每个package文件夹中还需要包含一个__init__.py
文件(若有子package,则每个子package目录中也要包含一个__init__.py
文件)
[root@localhost ~]# cd ~/test mkdir && package_test
[root@localhost package_test]# mkdir a
[root@localhost package_test]# echo > a/__init__.py
[root@localhost package_test]# echo 'print "module_1"' > a/module_1
为了演示方便,我们的工作目录可以通过pwd
命令查看:
[root@localhost package_test]# pwd
/root/test/package_test
由于python自动将工作目录加入到sys.path中(对sys.path陌生的请自行百度),
执行python
命令,测试package a
中的module_1
可以正常导入
[root@localhost package_test] python
>>> import a.module_1
>>> module_1
可以看到,package a
中的moduel_1正常import了
至此,一切都按照我们的设想正常进行
现在假设某位大神dashen想与我们合作,但又不想和我们共用一个目录,因而他提出让我们给他开一个帐号,名称为dashen,而他自己的代码就保存在/home/dashen 目录(家目录)下
于是,dashen兄做了以下操作:
[dashen@localhost ~]$ mkdir my_project && cd my_project
胃疼的是:由于dashen兄所负责的功能与我们package a
中的很相似,因此他也将自己的代码放到了package a
中:
[dashen@localhost my_project]$ mkdir a
[dashen@localhost my_project]$ echo 'print "moduel_2"' > a/module_2.py
这个时候,我们要对代码进行联调,所以将dashen兄的项目目录加入了sys.path中:
[root@localhost package_test]# python
>>> import sys
>>> sys.path += ['/home/dashen/my_project']
>>> sys.path
OK,dashen兄的目录已经加入了sys.path中
>>> sys.path
/usr/local/python2.7/lib/python2.7/site-packages/setuptools-18.4-py2.7.egg
/usr/local/python2.7/lib/python2.7/site-packages/Django-1.8.13-py2.7.egg
/usr/local/python2.7/lib/python2.7/site-packages/Twisted-15.5.0-py2.7-linux-x86_64.egg
…(此处略过不重要的path)
/home/dashen/my_project
我们接下来执行以下命令:
>>> import a.module_2
Traceback (most recent call last):
File "
ImportError: No module named module_2
WTF,这是什么鬼!!
dashen的module_2
无法import
不太科学,我们已经把dashen的工作目录加入了sys.path中
再看看我们的moduel_1
能否正常import:
>>> import a.module_1
>>> module_1
还好,我们的module_1没受影响
之所以会造成以上问题,主要由于python的packag查找逻辑
回顾下上面测试中sys.path所打印出来的结果:
>>> sys.path
/usr/local/python2.7/lib/python2.7/site-packages/setuptools-18.4-py2.7.egg
/usr/local/python2.7/lib/python2.7/site-packages/Django-1.8.13-py2.7.egg
/usr/local/python2.7/lib/python2.7/site-packages/Twisted-15.5.0-py2.7-linux-x86_64.egg
…(此处略过不重要的path)
/home/dashen/my_project
我们发现,在sys.path中出现了一个空行(第一行),这个空行实际上是”(sys.path实际上是字符串组成的list,每个字符串代表了系统中的一个路径),而”代表的是当前工作目录,也就是我们运行python
时所处的目录
python查找module的逻辑:
以import x.y.z
为例
附上目录结构:
root_dir
|–x
|–|__init__.py
|–|–y
|—–|–__init__.py
|—–|–z.py
y
中,而y
又在x
中,因而需要先找到 x
x
,找到之后,继续在该路径的x
目录中查找y
y
中找到z
在上述测试中,我们要找a.module_2
从第一个目录''
(也就是当前工作目录)开始查找 a
,由于当前目录已经包含了 a
这个package,因而python会天真的以为module_2
也在a
中,而dashen的目录/home/dashen/my_project
被默默地无视了,也就是说虽然dashen的目录也包含一个package a
,python也不会去管了
而module_1
只所以能import,大家应该知道原因了,因为我们当前工作目录中包含了a
,在a
中又包含了module_1.py
OK,现在问题的原因找到了:同一个package不能放在不同的位置,否则总有一些东西会丢失,实际上在python中,package也是一个特殊的module:
>>> type(a)
>>>
同样,如果sys.path中有两个或以上的目录包含同名文件,如module.py,那么位置靠前的目录中的那个会被import,其它的则会被无视
但是dashen不乐意了,他仍然坚持之前的做法,那现在怎么办呢?
请接着往下看
首先把两个概念简单拿出来说下:
module distribution VS package
python中很少看到library这个词,在其它很多语言中,library指的是:由一些代码逻辑组成的集合,翻译过来是库,一个库通常专门用于提供某一方面的功能。而在python中,官方文档将其称为module distribution,由于它的角色与library基本相同,这里我们索性也称之为python库,一个python库可以包含一个或多个module与package,而一个package又可以包含一个或多个module与package,在某个python库的根目录中,会包含一个setup.py文件,该文件用于安装该python库
实际上,同一个package中的内容是可以分散在不同目录的,比如zope.interface和zope.datatypes这两个python包下的moduel和子package都存在于名称为zope的namespace(命名空间)下
说到这,不得不提到setuptools,setuptools一个专门为python包开发者准备的python包发布管理工具,可以理解为python自带的distutils库的升级版(关于disutils,请百度)。
仍然用上面的例子来说明:
由于dashen的傲娇,他一意孤行的进行开发,而我们自然也按照原计划推进,到了最终发布python库的时候了,假设dashen的python库名为“dashen-lib”,而我们的库名为“our-lib”
our-lib与dashen-lib的setup.py文件分别如下(setup.py文件是啥?setup.py文件用于python库的打包制作,同时,用户安装python库的时候也会用到这个文件,关于setup.py扫盲请baidu,其他人已经把核心内容讲得很清楚了,在这儿又贴一遍实感觉没有必要):
our-lib:
from setuptools import setup, find_packages
setup(
name = 'our-lib',
version = '1.0',
packages = ['a']
)
dashen-lib:
from setuptools import setup, find_packages
setup(
name = 'dashen-lib',
version = '1.0',
packages = ['a']
)
假设我们与dashen通过以上两个setup.py各自制作了python库的安装包,用户利用安装包同时安装了这两个python库,那么在用户的机器上会存在两个目录:
(用户python安装路径)/lib/python2.7/site-packages/dashen-lib-1.0-py2.7.egg/dashen-lib
(用户python安装路径)/lib/python2.7/site-packages/our-lib-1.0-py2.7.egg/dashen-lib
以后,在每次用户运行python程序的时候,sys.path中就会包含以上两个目录,每个目录中都会包含名为a
的文件夹,这个时候我们预感到上面的package同名冲突又要重现了
经过和dashen协商,只要我们双方做以下两件事情
namespace_packages
这个参数在distutils库中不存在): namespace_packages = ['a']
a/__init__
文件只包含一行: __import__('pkg_resources').declare_namespace(__name__)
即可解决问题,最终的setup.py变成了下面这个样子:
our-lib:
from setuptools import setup, find_packages
setup(
name = 'our-lib',
version = '1.0',
packages = ['a']
)
dashen-lib:
from setuptools import setup, find_packages
setup(
name = 'dashen-lib',
version = '1.0',
packages = ['a']
)
经过我们的重新打包,用户使用就不会有任何问题了
除了我们往__init__.py
中加的那一行之外,namespace_packages这个参数那么究竟改变了什么呢?
让我们先看下在添加这个参数之前,用户安装了our-lib和dashen-lib之后这两个库安装目录下的文件内容:
our-lib
ls -l (用户python安装路径)/lib/python2.7/site-packages/our-lib-1.0-py2.7.egg/dashen-lib
-rw-r--r-- 1 root root 1 Jul 2 19:53 dependency_links.txt
-rw-r--r-- 1 root root 1 Jul 2 19:53 not-zip-safe
-rw-r--r-- 1 root root 189 Jul 2 19:53 PKG-INFO
-rw-r--r-- 1 root root 240 Jul 2 19:53 SOURCES.txt
-rw-r--r-- 1 root root 2 Jul 2 19:53 top_level.txt
dashen-lib
ls -l (用户python安装路径)/lib/python2.7/site-packages/dashen-lib-1.0-py2.7.egg/dashen-lib
-rw-r--r-- 1 root root 1 Jul 2 19:53 dependency_links.txt
-rw-r--r-- 1 root root 1 Jul 2 19:53 not-zip-safe
-rw-r--r-- 1 root root 189 Jul 2 19:53 PKG-INFO
-rw-r--r-- 1 root root 240 Jul 2 19:53 SOURCES.txt
-rw-r--r-- 1 root root 2 Jul 2 19:53 top_level.txt
添加参数之后:
our-lib
[root@localhost ~]# ls -l (用户python安装路径)/lib/python2.7/site-packages/our-lib-1.0-py2.7.egg/dashen-lib
-rw-r--r-- 1 root root 1 Jul 2 19:53 dependency_links.txt
-rw-r--r-- 1 root root 240 Jul 2 19:53 namespace_packages.txt
-rw-r--r-- 1 root root 1 Jul 2 19:53 not-zip-safe
-rw-r--r-- 1 root root 189 Jul 2 19:53 PKG-INFO
-rw-r--r-- 1 root root 240 Jul 2 19:53 SOURCES.txt
-rw-r--r-- 1 root root 2 Jul 2 19:53 top_level.txt
dashen-lib
[root@localhost ~]# ls -l (用户python安装路径)/lib/python2.7/site-packages/dashen-lib-1.0-py2.7.egg/dashen-lib
-rw-r--r-- 1 root root 1 Jul 2 19:53 dependency_links.txt
-rw-r--r-- 1 root root 240 Jul 2 19:53 namespace_packages.txt
-rw-r--r-- 1 root root 1 Jul 2 19:53 not-zip-safe
-rw-r--r-- 1 root root 189 Jul 2 19:53 PKG-INFO
-rw-r--r-- 1 root root 240 Jul 2 19:53 SOURCES.txt
-rw-r--r-- 1 root root 2 Jul 2 19:53 top_level.txt
我们可以发现,在两个库的安装目录下都出现了一个新文件:namespace_packages.txt,这个文件的内容其实非常简单:
[root@localhost ~]# cat (用户python安装路径)/lib/python2.7/site-packages/dashen-lib-1.0-py2.7.egg/our-lib/namespace_packages.txt
a
[root@localhost ~]# cat (用户python安装路径)/lib/python2.7/site-packages/dashen-lib-1.0-py2.7.egg/dashen-lib/namespace_packages.txt
a
可以得知:python就是通过namespace_packages.txt
中的内容与a
中__init__
我们新加的那一行来解决package名称冲突的,相当于把our-lib和dashen-lib的package a
合并成了一个统一的package a
参考文献:
1. Learning Python 5th Ed
2. http://setuptools.readthedocs.io/en/latest/setuptools.html?highlight=namespace#namespace-packages