起因
起初学 Go 语言的时候,对它的导包规则感到痛苦,——“那么麻烦干嘛呢!”。但最近接到一些新功能开发任务,于是 “啪嗒啪嗒” 写代码,结果更痛苦了。公司项目目录规划打一开始就不合理,以此为基础的 “繁荣发展” 导致更多混乱与麻烦。于是马上翻开《Python cookbook》解惑,有些心得,这里记之。
场景1
场景1:如何模块化代码,同时保证导入方式统一?
公司项目是老项目,因此代码祖传,常常一个 .py 文件里有上千行代码。恰巧现在重构(虽然并不是严格意义上的重构),同事们有意识地将代码拆分成多个文件。然后就有了这样的现象:
from onedir.twodir.file1 import func1
from onedir.twodir.three.file2 imort func2
from onedir.file3 import func3
目录结构如下:
Guan@Orchard:~/python/08/demo$ tree onedir/
onedir/
├── file3.py
├── __init__.py
└── twodir
├── file1.py
├── __init__.py
└── three
├── file2.py
└── __init__.py
2 directories, 6 files
很显然,要我记住哪个函数在哪个模块中这是很困难、又没意义的事。况且 onedir 里边的代码只有一个模块在使用,所以为什么不让调用方式统一一下呢?这里需要用到 Python 包中的 \_\_init\_\_.py 文件。
现在,修改 onedir 目录下的 init 文件,修改后变成下面这样:
# onedir/__init__.py
from .file3 import *
from .twodir.file1 import *
from .twodir.three.file2 import *
利用 init 文件加载子模块,使得子模块中的代码在导入父级包时就可以直接使用。示例如下:
"""原来的导入方式"""
# from onedir.twodir.file1 import func1
# from onedir.twodir.three.file2 import func2
# from onedir.file3 import func3
""""现在的导入方式"""
from onedir import func1, func2, func3
func1()
func2()
func3()
# 输出:
func1
func2
func3
这里额外补充一下相对路径导入模块的姿势。
我们都知道,在执行 import xxx
语句时,解释器会先在当前目录下找有没有这个模块,如果没有,就去 sys.path 中的路径里去找。当两个 .py 文件在同级目录时:
Guan@Orchard:~/python/08$ tree demo/
demo/
├── view1.py
└── view2.py
0 directories, 3 files
在 view2.py 文件中可以直接 import view1
,但是不可以 import .view1
。因为相对路径只允许 from ... import ... 这种导入方式。所以是不是 from . import view1 就可以了呢?也不行!
为控制篇幅,我主要是在这里抛结论,想获悉原理可以耐心去看看这篇文章:Python相对导入机制详解 。
结论:
- 在执行脚本所在的目录下,不能使用相对路径的导入方式。也就是说,如果我想在 demo 目录下运行 view2.py 文件,里边还有相对导入,那是绝对不可以的。当这个目录不是执行脚本所在目录时,就可以用相对导入了。
- 如果坚持直接运行 view2.py ,又想使用相对导入,请以
python -m demo.view2
这种方式运行脚本(这种方式下,Python2 要求 demo 目录中存在 init 文件,但 Python3 没有此要求)。
场景2
场景2:如何控制符号(函数,变量等)的导入?
我们现在已知的是,当一个函数名(变量、类同理)以下划线开头时,不允许 from xxx import *
的方式导入,但可以 from xxx import _xxx
这样导入。但不知道你们有没有觉得,下划线开头的符号看上去有点丑,我很不爱用,那么有没有其他控制方式呢?那当然是有的:__all__
。
在一个模块中定义变量 __all__
,并指明允许导出的符号名,from ... import *
就不能导入没有列出的那些符号了。用法如下:
def func1():
pass
def func2():
pass
__all__ = ["func1"] # 在其他模块中使用时,func1 可以被导入,func2 不行
当然,from xxx import func2
仍可以把 func2() 导进来。
场景3
场景3:如何让不同目录下的代码在统一的命名空间中导入?
目录结构如下:
Guan@Orchard:~/python/08/demo$ tree demo/
demo/
├── demo1
│ └── spam
│ └── a.py
└── demo2
└── spam
└── b.py
4 directories, 2 files
现在,需要使用 a.py 和 b.py 中的代码,寻常方式是:
import demo1.spam.a
import demo2.spam.b
尽管它们都在 spam 目录下,但因为顶级目录(demo1 和 demo2)不同,所以需要不同的入口。但其实,是可以统一 a.py 和 b.py 的入口的。解决方式如下:
import sys
sys.path.extend(["demo1", "demo2"])
import spam.a
import spam.b
事实上,此时的 spam 是一个命名空间包。现在打印 spam ,得到的结果会是:
...
In [5]: spam
Out[5]:<module 'spam' (namespace)>
In [6]: spam.__file__
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-6-b25cc88cc23e> in <module>()
----> 1 spam.__file__
AttributeError: module 'spam' has no attribute '__file__'
创建命名空间包的关键在于,统一命名空间的顶层目录中不能有 \_\_init_\_.py 文件。拿上面的例子来说,就是不能在 spam 目录下有这个文件。
你可以尝试导入一个目录,在两种情况下,一个是该目录下存在 init 文件,一个是不存在这个文件。然后打印这个目录,看看结果会有什么不同。
感谢
- 参考 《Python cookbook(第三版)》
- 参考 Python相对导入机制详解
还不快抢沙发