My SOP of Python 包编程
背景
前几天转发了一篇 Python类、模块、包的区别 重在理清我正准备要将一个有 500 行核心代码的 .py 文件拆分出来,是称为拆分成包,还是叫模块。
当然,看了博文之后,了解到,实际上在python 中,我们将 .py 文件不作为 __main__
运行,实际上就是当作 “模块” 使用了。也就是说,一个 .py 文件对应的是一个模块(module),一个 dir/
文件夹,对应的就是一个 包 (package)
;1
在我拆分一个 .py 文件成包的时候,出现了一点儿问题,所以有必要总结一下。当然,做成 SOP,照做就行,因为有些东西我们无需深究,应当把精力放在我们需要关注的地方,而非这类 编程gong语言ju 的细枝末节。
1 SOP
为了能够使示例根据自释性,我就直接使用明显的命名了。
all_in.py <========================<- +--------------------------+
| \ | many import |
\/ | |
pro-root/ | GLOBAL_VARS |
|---- package/ | |
| |-------- __ini__.py | def utils_func(): ... |
| |-------- utils.py | |
| |-------- functional1_module.py | class GRAND():... |
| |-------- functional2_module.py | |
| `------- others.py | class PARENT(GRAND):.. |
|---- setup.py | def main(argc, argv):... |
|---- LICENCE, README, etc... |if __name__=='__main__': |
`--- main.py +--------------------------+
package/ 内的文件内容基本上是这样的:
#########################################################################
# package/utils.py | # main.py
GLOBAL_VARS | from package.utils import *
| from package.functional1_module import GRAND
def utils_func():... | from package.functional2_module import PARENT
| def main(argc, argv):
| ...
| if __name__ == '__main__':
| main(len(sys.argv), sys.argv)
|
##########################################################################
# package/functional1_module.py | # package/functional2_module.py
from .utils import * | from .utils import *
import ... | from .functional1_mdule import GRAND
|
class GRAND(): | class PARENT(GRAND):
... | ....
|
###########################################################################
注意到上面有一些差异:
- main.py 使用的是
from package.functional1_module import GRAND
来导入;
functional2_module.py 使用的是from .functional1_module import GRAND
来导入。functional2_module.py 导入的是本包内的功能模块,所以使用相对导入,这样做是有原因的:
在 package/ 未来
$ python setup.py install
的时候,这样不用修改包内文件的任何内容
严格要求自己在 package/ 开发的时候,就不要将开发测试写在 package/functional1_module.py 这样的文件里面了。要写在上层的 main.py 文件中。
main.py 不使用相对导入,package/ 在 install 之后它也不用调整内容。
from . import module
(from .module import class
) 使用相对导入 - 相对导入知识点:
2 Note
2.1 递归导入(向前引用原则)陷阱注意
这里就跟 C 语言的 头文件要注意使用 #ifndef __XX_H #define __XX_H ... #endif
类似地要考虑这种问题。
但是在 python 中,原因有所不同。
C 语言中,不像上述那样处理头文件的话,在递归导入的情况中会显示重复定义。
而 python 中的递归导入,往往是“没有定义”。
我是从书上学习来的,书上讲述地也很好理解了,我不觉得自己能解释地更清楚,所以针对这个问题,我就直接将 Learning Python 上相关的知识点和解释贴出来,以供参考。
最后一点补充
“因为导入会从头到尾执行一个文件的语句”
如果对这句话有不清楚的地方,实际上 Learning Python 一书对此有单独的一节解释和示例。
只不过这个特性也就是比较显而易见了,在你需要了解到自己制作 “包” 这个问题上的时候,即使你不清楚上述这句话的概念,但是也应该注意到过这个现象了。所以我不会对这个内容多做说明。有一个值得一提的是,上面我的示例中
package/utils.py
中的 GLOBAL_VARS 就是使用此特性,因此其它模块在有必要使用这类全局变量的时候,可以通过from .utils import GLOBAL_VARS
这样的语句使用来自 utils.py 中的全局变量(严格意义上,这里是希望它作为一个常量)。
关于 Python 的“常量“ 也是一个值得有所了解的话题,但是并非本文主题;众所周知,元组是个定义不可变变量好内置类型。
但是如果你想给不可变常量带上一个名称,而非使用ALL_CONST[0], ALL_CONST[1]
这样的方式来使用常量,那么标准库的collections.namedtuple
是个好的 solution。4
3 2019/03/06 更新
在写了更多的 Python 代码,创建/实践更复杂的 Python 项目之后,终于理解了 Python 中属于项目一部分的模块文件该如何处理了!
首先假设有一个 Python 项目,整个项目文件夹在如下位置:
$ pwd
/home/jos/project
即,这里整个项目文件夹,我为了方便就称之为
project
那么如果你使用:
$ python
>>> import sys
>>> sys.path
['', ......, ......, .......]
你可以自己运行一下试试
注意到开始第一个 ''
。这说明说,“当前的目录”已经属于 Python 解释器导入包的时候索引路径的其中之一。
3.1 顶层文件导入模块与包内模块相对导入
也就是说,如果项目文件夹内部的文件像这样:
project/
|---- main.py
|---- cli.py
|---- package1/
| |---- __init__.py
| `---- module_a.py
`---- module_2.py
顶层文件直接导入模块/包
那么在 main.py
中可以这么直接使用:
import cli
import package1
from package1.module_a import Class_I
import module_2
from module_2 import Class_II
...
以上用法都是没有问题的。
包内模块使用相对导入
在 package1
中,因为这个定位是一个“包”,作为一个包,我们总是希望它是提供一个尽可能独立的功能。所以最好是它只依赖它自己本身的代码。
在 package1
文件夹中的文件可以使用相对导入来导入属于这个包本身范围的 py 文件:
#### this is pacake1/module_a.py file
from . import *
上面 line 2 代码会导入
package1/__init__.py
中所有(全局)变量和函数和类。
当然,也有相反的做法,
__init__.py
导入module_a.py
。
也就是说,如果还有module_b.py
等,也被__init__.py
导入,由__init__.py
统一提供该package1
提供的功能。
区别只是在于“作者”如何设计而已,都是可行的。
但是尽最大可能不要相互导入。