Python项目中的模块引用问题,是一个比较复杂的问题,无非是绝对引用,相对引用,
看起来似乎很简单,但在实践中,总是会出现一些“莫名其妙”的错误,但解决起来倒也方便,
import 语句的写法多试验几次也就可以搞定了,关于这方面,很少有文章全面深入的讨论,
经过多次吃亏后,决定认真研究一下。
本文所使用的示例在python3.7环境下顺利通过。
入口脚本
首先明确一点,一个项目的入口脚本,或者说启动脚本,必须放在项目的根目录下,
启动脚本所在的目录,将被加入到 sys.path 里,而在脚本里使用 import 引入模块时,
会根据 sys.path 里的目录逐个进行查找,这一点很关键,后面会用到。
pypackage/
├── __init__.py
├── log.py
├── run.py
├── utils
│ ├── fileutil.py
│ └── __init__.py
└── view
├── __init__.py
├── one.py
├── two.py
└── view2
├── four.py
├── __ini__.py
└── three.py
import log
受此启发,fileutil 也可以引入 four 模块。
from view.view2 import four
from view import two # 绝对引用
from . import two # 相对引用
如果想在 one.py 里引用 fileutil 模块,下面两种方法都可行。
from utils import fileutil # 绝对引用
from ..utils import fileutil # 相对引用 实测不可行
一个 . 表达式当前目录, 两个 .. 表示上一级目录,那么 from ..utils import fileutil 本应该也可行,
但实测却发现无法引用,所以,遇到这种莫名其妙的问题,建议使用绝对引用,避免使用相对引用。
app/
__init__.py
sub1/
__init__.py
mod1.py
string.py
sub2/
__init__.py
mod2.py
从程序的输出会发现,它引用的是 app/sub1/string.py 中的 lower() 方法。
显然解释器 默认先从当前目录下搜索对应的模块,当搜到 string.py 的时候便停止搜索进行动态加载。
那么如果要使用 Python 自带的 string 模块中的方法,该怎么实现呢?
这就涉及 absolute import 和 relative import 相关的话题了。
在 Python 2.4 以前默认为隐式的 relative import ,
局部范围的模块将覆盖重名的全局范围的模块。
如果要使用标注库中同名的模块,需要深入考察 sys.modules 。
显然这并不是一种非常友好的做法。
import sys
len(sys.modules)
932
Python 2.5 后虽然默认的仍然是 relative import ,
但它为 absolute import 提供了一种新的机制,
在模块中使用 from _future_ import absolute_import 语句进行说明后再进行导入。
同时它还通过点号提供了一种显式进行 relative import 的方法,
. 表示当前目录, .. 表示当前目录的上一层目录。
例如想在 modl.py 中导入 string.py ,
可以使用 from.import string 。
其中 modi 所在的包层次结构为 app.sub1.modi 。
但事情是不是就此结束了呢?远不止,
使用显式 relative import 之后再运行程序可能遇到这种错误:
ValueError: Attempted relative import in non-package
这是什么原因呢?这个问题产生的原因在于 relative import 使用模块的 __name__ 属性来决定当前模块在包层次结构中的位置。
如果当前的模块名称中不包含任何包的信息,
那么它将默认为模块在包的顶层位置,而不管模块在文件系统中的实际位置。
而在 relative import 的情形下, __name__ 会随着文件加载方式的不同而发生改变,
上例中如在目录 app/subl/ 下运行 Python rnodl.py ,会发现模块的 _name_ 为 _main_ ,
但如果在目录 app/subl/ 下运行 Python -m modl.py ,会发现 _name_ 变为 modi 。
其中 -m 的作用是使得一个模块像脚本一样运行。
而无论以何种方式加载,当在包的内部运行脚本的时候,包相关的结构信息都会丟失,
默认当前脚本所在的位置为模块在包中的顶层位置,因此便会抛出异常。
如果确实需要将模块当作脚本一样运行,解决方法之一是在包的顶层目录中加入参数 -m 运行该脚本,
上例中如果要运行脚本 modl_py 可以在 app 所在的目录的位置输入 Python -m app.subI.modI 。
另一个解决这个问题的方法是利用Python2.6在模块中引入的 _package_ 属性,设置 _package_ 之后,
解释器会根据 _package_ 和 _name_ 的值来确定包的层次结构。
上面的例子中如果将 modl.py 修改为以下形式便不会出现在包结构内运行模块对应的脚本时出错的情况了。
if __name__ == "__main__" and __package__ is None:
import sys
import os.path
sys.path[0] = os. path. abspath('./../../')
print (sys.path[0])
import app.sub1
_package_ = str('app.sub1')
from . import string
相比于 absolute import , relative import 在实际应用中反馈的问题较多,
因此推荐优先使用 absolute import 。
absolute import 可读性和出现问题后的可跟踪性都更好。
当项目的包层次结构较为复杂的时候,显式 relative import 也是可以接受的,
由于命名冲突的原因以及语义模糊等原因,不推荐使用隐式的 relative import ,
并且它在 Python3 中已经被移除。