Python导入路径:不同文件夹中同名包的处理
我正在同时为多个客户开发几个Python项目。我的项目文件夹结构简单来说大概是这样的:
/path/
to/
projects/
cust1/
proj1/
pack1/
__init__.py
mod1.py
proj2/
pack2/
__init__.py
mod2.py
cust2/
proj3/
pack3/
__init__.py
mod3.py
比如,当我想使用proj1
中的功能时,我会通过/path/to/projects/cust1/proj1
来扩展sys.path
(例如,通过设置PYTHONPATH
,或者在site_packages
文件夹中添加一个.pth
文件,甚至直接修改sys.path
),然后像这样导入模块:
>>> from pack1.mod1 import something
随着我进行的项目越来越多,不同的项目可能会有相同的包名:
/path/
to/
projects/
cust3/
proj4/
pack1/ <-- same package name as in cust1/proj1 above
__init__.py
mod4.py
如果我现在简单地通过/path/to/projects/cust3/proj4
来扩展sys.path
,我仍然可以从proj1
中导入,但却无法从proj4
中导入:
>>> from pack1.mod1 import something
>>> from pack1.mod4 import something_else
ImportError: No module named mod4
我认为第二次导入失败的原因是Python只会在sys.path
中的第一个文件夹里查找pack1
包,如果找不到mod4
模块就会放弃。我之前问过这个问题,详情可以查看导入同名的Python模块,但内部的细节我还是不太明白。
无论如何,显而易见的解决办法是通过将项目目录变成超级包来增加一个命名空间的层次:在每个proj*
文件夹中添加__init__.py
文件,并从扩展sys.path
的路径中移除这些文件夹,例如:
$ export PYTHONPATH=/path/to/projects/cust1:/path/to/projects/cust3
$ touch /path/to/projects/cust1/proj1/__init__.py
$ touch /path/to/projects/cust3/proj4/__init__.py
$ python
>>> from proj1.pack1.mod1 import something
>>> from proj4.pack1.mod4 import something_else
现在我遇到了一个情况,不同客户的不同项目有相同的名称,例如:
/path/
to/
projects/
cust3/
proj1/ <-- same project name as for cust1 above
__init__.py
pack4/
__init__.py
mod4.py
尝试从mod4
导入不再有效,原因和之前一样:
>>> from proj1.pack4.mod4 import yet_something_else
ImportError: No module named pack4.mod4
按照之前解决这个问题的方法,我会再增加一个包/命名空间层,把客户文件夹变成超级超级包。
然而,这与我对项目文件夹结构的其他要求发生了冲突,例如:
- 开发/发布结构以维护多个代码线
- 其他类型的源代码,比如JavaScript、SQL等
- 除了源文件之外的其他文件,比如文档或数据。
一个更复杂、更贴近实际的项目文件夹结构大概是这样的:
/path/
to/
projects/
cust1/
proj1/
Development/
code/
javascript/
...
python/
pack1/
__init__.py
mod1.py
doc/
...
Release/
...
proj2/
Development/
code/
python/
pack2/
__init__.py
mod2.py
我看不出如何同时满足Python解释器对文件夹结构的要求和我自己的要求。也许我可以创建一个额外的文件夹结构并使用一些符号链接,然后在sys.path
中使用它,但考虑到我已经付出的努力,我感觉我的整个方法可能有根本性的错误。顺便说一下,我也很难相信Python真的会限制我选择源代码文件夹名称的自由,似乎在这种情况下确实是这样。
我该如何设置我的项目文件夹和sys.path
,以便在项目和包名相同的情况下,能够以一致的方式从所有项目中导入?
3 个回答
如果你不小心把一个客户或项目的代码导入到另一个项目里,而且没有注意到,会发生什么呢?当你交付的时候,几乎肯定会出问题。我建议你每次只设置一个项目的PYTHONPATH,不要试图让你写过的所有代码都能同时被导入。
你可以为每个项目使用一个包装脚本来设置PYTHONPATH并启动python,或者在切换项目时使用脚本来切换环境。
当然,有些项目可能会依赖其他项目(就是你提到的那些库),但如果你希望客户能够同时导入多个项目,那你就得确保这些项目的名字不会冲突。只有当你在PYTHONPATH上有多个本不该一起使用的项目时,才会遇到这个问题。
你应该使用非常好用的 virtualenv 和 virtualenvwrapper 工具。
这个是我问题的解决办法,虽然一开始可能不太明显。
在我的项目中,我现在为每个客户引入了一个命名空间的约定。每个客户的文件夹(比如 cust1
、cust2
等)里都有一个 __init__.py
文件,里面写着以下代码:
import pkgutil
__path__ = pkgutil.extend_path(__path__, __name__)
我其他包里的 __init__.py
文件都是空的(主要是因为我还没时间去研究它们该怎么用)。
正如 这里 解释的那样,extend_path
确保 Python 知道一个包里有多个子包,这些子包可能在不同的地方。根据我的理解,当解释器在 sys.path
中找到第一个包路径时,如果找不到模块,它不会停止搜索,而是会继续在 __path__
中的所有路径里查找。
现在,我可以在所有项目之间以一致的方式访问所有代码,比如:
from cust1.proj1.pack1.mod1 import something
from cust3.proj4.pack1.mod4 import something_else
from cust3.proj1.pack4.mod4 import yet_something_else
不过,有个缺点是,我不得不创建一个更深的项目文件夹结构:
/path/
to/
projects/
cust1/
proj1/
Development/
code/
python/
cust1/
__init__.py <--- contains code as described above
proj1/
__init__.py <--- empty
pack1/
__init__.py <--- empty
mod1.py
但这对我来说似乎是可以接受的,尤其是考虑到我维护这个约定所需的努力非常少。对于这个项目,sys.path
被扩展为 /path/to/projects/cust1/proj1/Development/code/python
。
顺便提一下,我注意到,对于同一个客户的所有 __init__.py
文件,sys.path
中第一个出现的路径里的那个文件会被执行,无论我从哪个项目导入东西。