前面介绍递归函数的时候用到了sys模块,介绍文件操作函数的时候用到了os模块,之前只是简单说了这两个模块下部分函数的用法,这里详细介绍一下对于模块、包和库的概念,以及一些常见的模块用法。
不需要记住每个模块下所有函数用法,但是平常看到python文件导入模块操作的时候,要大概知道这几个模块有什么作用。
1. 概念
1.1 模块(module)
函数可以理解为完成特定功能的一段程序,类是包含一组数据及操作这些数据或传递消息的函数的集合,而模块(module)是在函数和类的基础上,将一系列相关代码组织到一起的集合体。
在python中,扩展名为.py的源程序文件就是一个模块,这个和C语言的头文件以及JAVA的包是类似的。
python官方网站上可以查看当前标准库中的所有模块,点击这里。
1.2 包(package)
为了方便调用将一些功能相近的模块组织在一起,或是将一个较为复杂的模块拆分为多个组成部分,可以将 .py 源程序文件放在同一个文件夹下,按照 Python 的规则进行管理,这样的文件夹和其中的文件就称为包(package)。
包的目录下需要创建__init__.py 模块,可以是一个空文件,可以写一些初始化代码,其作用就是告诉 Python 要将该目录当成包来处理,让python认为你这是一个包而不是单纯的一个目录(否则会显示找不到包)。有的博客说python3.3版本之后不需要空的__init__.py 模块来声明这是一个包了,但是我在vscode和jupyter运行python3.10的时候发现还是需要__init__.py 模块声明的,这里先存疑,我保留自己的观点。
- 2022.12.3更新:准确来说,从包里导入模块需要__init__.py 声明;直接导入同目录下的模块不需要(3.3版本以后)
简单来说,包就是有层次地文件目录结构,里面装着各种扩展名.py的python源程序文件,包中也可以含有包。
1.3 库
库顾名思义则是功能相关联的包的集合。python的三大特色之一:强大的标准库,第三方库以及自定义模块。
2. 常用模块/库
python的三大特色对应三种类型的模块,标准库的内置模块,第三方库开源模块和自定义的模块,这里简单记录一下常用的模块/库。
模块名称 | 介绍 |
---|---|
内置模块 | |
os | 普遍的操作系统功能接口,包括前面介绍的文件操作函数 |
sys | 提供了一系列有关Python运行环境的变量和函数,sys.path.append() |
random | 生成随机数,random() 返回0<n<=1 |
time | 各种提供日期、时间功能的类和函数,time.time() 时间戳 |
datetime | 对time模块的一个高级封装 |
logging | 日志打印到了标准输出中 |
re | 可以直接调用来实现正则匹配,re.split() 分割字符串,格式化列表 |
pymysql | 连接数据库,并实现简单的增删改查 |
threading | 提供了更强大的多线程管理方案 |
json | 用于字符串和数据类型间进行转换json |
subprocess | 像linux一样创建运行子进程 |
shutil | 对压缩包的处理、对文件和文件夹的高级处理,os的补充 |
tkinter | Python的标准Tk GUI工具包的接口 |
第三方模块/库 | |
Requsests | python最有名的第三方HTTP客户端库 |
Scrapy | 屏幕抓取和web抓取框架,编写爬虫用到(上面的也可以) |
Pillow | 常用的图像处理库 |
Matplotlib | 绘制二维数据图的库,使用方式对标matlab |
NumPy | 提供大型矩阵计算公式,在很多领域都用到 |
Pandas | 基于Numpy 和 Matplotlib,和上面两个组成数据分析三剑客 |
Django | 开源的web开发框架 |
PyTorch | 开源的深度学习框架,各种张量操作、梯度计算,方便构建各种动态神经网络 |
TensorFlow | 也是机器学习库,张量的操作和运算,tensorboard可视化数据很强大 |
第三方库实在太多,这里只列举了我知道的比较常见的库;内置模块可以见1.1章节的官网链接,里面有所有内置模块的具体用法。接下来说说怎么导入模块和制作模块。
3. 导入包和模块
3.1 导入模块
制作模块要注意,自定义的模块名不能和系统内置的模块重名,否则被重名的系统模块无法被导入。
python中用关键字import引入某个模块,在调用模块中的函数时,需要以 模块名.函数名 的方式进行引用。自定义模块名中的函数是可以重名的,因为模块名不会相同(同一层目录下文件名不同),调用的时候可以进行区分,这很好理解。
3.2 导入包
有的时候我们只需要包里的某个模块或者模块里的某个函数,而不需要包或者模块里的全部内容,这个时候我们可以用关键词 from 包名/模块名 import 模块名/函数名 来进行调用。
举个例子,在如下的文件结构中,main.py作为主程序入口,test文件夹相当于一个包,里面有4个.py后缀的模块,分别定义了四则运算的函数,__init__.py 是个空文件(暂时不做处理),声明test文件夹是个python包而不是普通的目录。
1 | # add.py文件内容——定义加法运算 |
我现在要做的是,在main.py文件里,导入test包里四个模块,调用各自模块中对应的函数,有以下几种调用方式:
1 | # main.py的文件内容 |
4. 包和模块导入的思考
4.1 __init__.py的作用
在上面的例子中__init__.py 是个空文件,是声明test文件夹是python包所必须的(主程序和包的位置在同一个目录下)。然而我们在编写main.py的主程序文件的时候,仍然要在开头导入相当多的模块,比较繁琐,这个时候可以在__init__.py中批量导入我们所需要的模块(导入包其实就是导入__init__.py文件)。
1 | # 在__init__.py中添加如下内容 |
1 | # main.py相应的改为如下内容 |
可以看到上面的主程序代码量少了很多,起到简化代码的作用。
4.2 if __name__ == ‘__main__‘
首先来看一个现象,如果在add.py文件中不仅仅有定义函数的代码,还有编写代码时做的测试内容,如下:
1 | # add.py文件中最后一行对这个函数做了测试 |
其他文件全都不变,再次运行main.py,会发现输出结果为:
1 | 7 # add.py中测试内容也被输出 |
这显然不是我们想看到的,我们在导入add模块调用add函数的时候,并不想要其他无关的输出结果。
稍稍改变一下add.py内容
1 | # add.py文件内容 |
此时再次运行main.py则不会输出add.py中的测试内容。
首先要了解一个概念,在每个python文件创建的时候都有一个记录名称的变量__name__,当这个python文件作为脚本直接运行,那么__name__的值为‘”__main__“;当这个文件作为模块被导入其他文件中运行的时候,这个__name__的值为模块的名字,也就是说
当.py文件被直接运行时,if __name__ == ‘__main__‘ 之下的代码块将被运行
当.py文件以模块形式被导入时,if __name__ == ‘__main__‘ 之下的代码块不被运行
在导入的模块中有选择性地执行代码,这在实际开发应用中非常普遍。
4.3 导入模块在主程序的父目录下
前面的导入模块操作,导入模块要么在主程序的子目录下(加入__init__.py 声明这是一个包),要么和主程序在同一个目录(直接import),如果导入模块在主程序的父目录下,应该怎么导入呢?
首先,按照一般流程直接import导入和加入__init__.py声明都会报错找不到这个包,这里就不演示了。
其实这个问题在前面的笔记中有记录,点击这里。 当时是刚用vscode搭建python环境,对python调用一知半解都算不上,现在才有了初步的理解。
1 | # 两种解决办法 |
4.4 相对导入
前面3.2的例子中,包和模块的导入都是用的绝对导入(absolute import),导入时写明了工作环境中包的具体位置。
还有一种导入方式称为相对导入(relative import),还是用3.2的例子理解一下,在如下的文件结构中,主程序入口main.py和test包在同一层目录下,test包中有__init__.py(空文件),add.py和dev.py两个模块的内容如下:
1 | # add.py内容 |
上面的例子意思是,我现在要在test包的dev.py模块中用add.py模块的函数方法(同一个包中的模块相互引用,这在实际工程中很常见)。如果dev.py是主程序,我们可以直接import add
;但是我们这里main是主程序,代码如下:
1 | # main.py内容 |
主程序main.py功能是导入test包dev模块,打印dev模块的函数func1(1, 2)执行结果。
如果我们在dev.py中直接导入from add import add
(没有点,也就是不加当前目录),这个时候再运行main.py会报错找不到模块(因为main.py同目录下没有add.py模块)。这个时候就有两种导入方式,要么完善包名字,使用绝对导入from test.add import add
;要么使用相对导入from .add import add
。
这个相对导入就像是linux的文件操作方式,一个点代表当前目录,两个点代表父目录,还能用三个点表示linux无法做到的祖父目录,依此类推。
相对导入的优点就一目了然:就算改变了包的名字,这个时候调用也不会出错,也就是简化代码,方便迁移。