模块化编程是使用Python这门语言进行较大型开发的基本方式,在谈这个话题之前,我先来介绍一下我自己的学Python的一个进化背景。
在刚开始学习Python这门语言,主要是在读研究生期间用它对气象类数据进行处理分析和可视化,最开始使用的方式也简单粗暴:直接在一个脚本文件里写上大段大段的代码(甚至都不使用函数),保存以后去终端调用python命令执行,这是第一阶段。
后来根据一些教材的建议以及对实际复制粘贴的厌恶,开始使用函数以解决代码重用问题,这是第二阶段。在这一阶段,我把几乎所有的代码都写在一个脚本文件里,使得这个文件极其冗长和繁杂。为了使程序变得清晰,我就在处理新问题的时候新建一个新的脚本文件去写新的代码,这时候还是会遇到代码重用的问题:我需要从旧脚本中复制粘贴复用函数到新的脚本。
为了进一步解决代码重用问题,我进入了第三个阶段:导入自定义函数包,在这一阶段,我的方式也非常简单粗暴,我会把一些复用性较高的函数统一放在一个命名为tools.py的脚本里,然后在它的同级目录下新建其他脚本文件,在新脚本中用import tools as tl来导入该脚本,然后调用其中的函数。
在研究生的三年中,我基本上就保持在这第三阶段,在模块化编程上没有更进一步。
后来,我发现这种把所有函数集中在一个脚本文件的方式会让这个tools.py文件变得越来越庞大和丑陋。在GitHub上研习了其他一些代码库以后,我才知道别人到底是怎么优雅地使用Python的,由此我也开始恶补模块化编程,努力向社区看起。
什么是包(package)?什么是模块(module)?
以前我并不是很重视包(package)和模块(module)之间的区别,甚至就把它们当成一个东西,比如上文我所说的“导入自定义函数包”这句话,其实就是把模块当作了包。然而包和模块是完全不同的两个东西。简单来讲,模块就是单个脚本文件,每一个以.py结尾的脚本文件,就是一个模块,在本文中“脚本”与“模块”等同。而包是一种多个模块的结构化集合。它是对多个模块的一种聚类整理,但它不是模块的简单组合。
参阅:
为什么模块结尾会有if __name__ == ‘__main__’代码块?
在很多脚本的结尾会有类似于这样的代码块:
if __name__ == '__main__':
main()
这段代码是干什么用的呢?
简单来说,这段代码是根据模块调用方式来选择性执行代码块的。
因为每一个模块都有两种执行(调用)方式,即作为主函数执行和作为模块执行。
其中作为主函数执行,就是在终端直接用python命令调用,比如说我自己写了一个叫mymodule.py的模块,那么在终端执行python mymodule.py就是将该模块作为主函数执行,这样会执行该模块中的所有代码。
假如这个mymodule.py需要在其他模块中以import的方式导入使用的话,就是以模块形式执行。在作为模块执行的时候,被导入的模块里面的一些代码是不需要甚至是要避免执行的,如果存在if __name__ == '__main__'判断,那么该判断内的代码块会被忽略。
举个例子:
比如说,我写了一个名为loadjs.py的模块,它里面仅仅包含一个简化读取json文件的函数。
# coding : utf-8
import json as js
def read(path):
'''
easily read js file
'''
with open(path) as file_obj:
content = js.load(file_obj)
return content
# 不含if __name__ == '__main__'
PATH = './test.json'
content = read(PATH)
print(content)
我在该模块同级目录下准备好了test.json的文件,其内容如下:
{
"message":"testing",
}
现在在终端执行python loadjs.py,得到的结果如下
$ python loadjs.py
{'message': 'testing'}
作为主函数执行一切正常对吧,好了,我现在想试试用import形式执行。先准备好另一个json文件:aboutme.json,它的内容如下:
{
"author":"Clarmy Lee",
"gender":"male",
"age":26,
"single": true
}
在Python的交互式命令行中调用
>>> import loadjs
{'message': 'testing'}
>>> loadjs.read('./aboutme.json')
{'author': 'Clarmy Lee', 'gender': 'male', 'age': '26', 'single': True}
等等,发生了什么?我只想读取aboutme.json的内容,为什么导入模块后它打印了之前的测试信息?
这是因为我们在导入该模块的时候,import语句完完整整地把该模块中所有的代码全部执行了。
那么现在我们希望执行import loadjs语句时不要打印出测试的内容,仅在终端执行python loadjs.py时正常打印测试结果,这时候就需要使用if __name__ == '__main__'代码块了。
把loadjs.py模块改写一下
# coding : utf-8
import json as js
def read(path):
'''
read js file
'''
with open(path) as file_obj:
content = js.load(file_obj)
return content
if __name__ == '__main__':
PATH = './test.json'
content = read(PATH)
print(content)
其实也就是把只在主函数执行的那部分代码放在if __name__ == '__main__'代码块中,这样一来,用python loadjs.py调用的时候,这部分代码会被执行,而用import loadjs的方式调用模块,这部分代码会被忽略。
用流程图来显示
__name__是什么?
为了说清楚这个问题,我先创建一个名为mymodule.py的模块。里面只有一条语句:print('__name__:'+__name__),它是用来打印__name__这个变量的。
然后我在终端执行该模块
$ python mymodule.py
__name__:__main__
看到了吗?__name__的值为__main__。
我再用import的方式执行该模块:
>>> import mymodule
__name__:mymodule
看到了吗?__name__的值为mymodule,所以我们可以看出用不同的方式调用模块,__name__变量就会被赋予不同的值,用主函数调用的时候它就会被赋值为__main__,以模块调用的时候它会被赋值为模块名。
那么__name__究竟是什么?在问这个问题之前,我们需要思考另一个问题:模块究竟是什么?
接着刚才的命令行,检查一下模块的类型:
>>> type(mymodule)
这么看来,模块是类的一种,也即是说,模块是对象。这当然应验了Python中一切皆对象的说法。
既然是对象,那__name__就是模块对象的一个属性。在我们使用import语句的时候,Python就会建立模块对象(实例化),在实例化的过程中,Python会对模块对象进行初始化,对__name__的赋值便是其对象初始化的结果。
为什么要用if __name__ == ‘__main__’?
可能有人在前面的loadjs.py例子中有这样一个疑问:我在调试完成以后直接把与调试有关的代码删除只保留函数不就行了?何必非要用if __name__ == '__main__'呢?当然,如果主函数调用只用来调试,那么调试完删除是可以的,无需额外增加判断,但是这样做太浪费了,如果我们想要让模块变得更通用一些,就应该充分利用if __name__ == '__main__'代码块。拿loadjs.py这个例子来说,假如我希望用主函数调用的方式来实现对指定json文件的读取。可能需要进行这样的改写
# coding : utf-8
import json as js
from sys import argv
def read(path):
'''
read js file
'''
with open(path) as file_obj:
content = js.load(file_obj)
return content
def main(path):
content = read(path)
print(content)
if __name__ == '__main__':
PATH = argv[1] #获取终端传入的文件路径参数
main(PATH)
这样这个脚本就可以根据用户的需要打印用户指定的json文件了
$ python loadjs.py ./test.json
{'message': 'testing'}
$ python loadjs.py ./aboutme.json
{'author': 'Clarmy Lee', 'gender': 'male', 'age': '26', 'single': True}
此外,在实际编程的时候,调用情景会比这复杂得多,一个脚本有时候需要扮演主程序的角色,有时候需要作为模块导入。合理规划if __name__ == '__main__'可以有效提高代码的复用效率。
如何建立模块包目录结构?
前面所举的例子,所有的import都发生在同级目录下,如果我们要开发一个稍微大一点的程序,把所有模块都一股脑堆在一个文件夹里显然是不合适的,这样会使模块难以管理。就像管理人一样,但凡是人数比较多的组织,必然会有层级管理。
在Python2中,我们可以把脚本放进文件夹,然后在每个文件夹里建立一个名为__init__.py的文件,该文件内容可以为空,但是必须要有;在Python3中,直接把模块放进文件夹里即可,__init__.py并非必须。
调用的时候直接按照层级名称即可访问,例如我把loadjs.py放进了一个名为pkg的文件夹里,那么我在与pkg同级的文件内要调用loadjs.py时直接使用import pkg.loadjs即可。