• 微信公众号:美女很有趣。 工作之余,放松一下,关注即送10G+美女照片!

【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】

互联网 diligentman 2小时前 2次浏览

为什么写这篇文章

我从2021年6月13号写下第一篇Python的系列专栏算起,陆续更新了二十七篇Python系列文章。在此感谢读者朋友们的支持和阅读,特别感谢一键三连的小伙伴

本专栏起名【Python从入门到精通】,主要分为基础知识和项目实战两个部分,目前基础知识部分已经完全介绍完毕。下一阶段就是写Python项目实战以及爬虫相关的知识点。

为了对前期学习的Python基础知识做一个总结归纳,以帮助读者朋友们更好的学习下一部分的实战知识点,故在此我写下此文,共勉,同进

同时为了方便大家交流学习,我这边还建立了一个Python的学习群。群里都是一群热爱学习的小伙伴,不乏一些牛逼的大佬。大佬带飞,我相信进入群里的小伙伴一定会走的更快,飞的更高。 欢迎扫码进群。
【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】

本专栏写了什么

下面就通过一个思维导图,展示本专栏Python基础知识部分的总览图。
【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】
本专栏从零基础出发,从环境的搭建到高级知识点的学习,一步步走来,相信各位读者朋友们早已掌握相关的知识点。接下来就做一个详细的回顾。

0.何为Python

Python是一门开源免费的,通用型的脚本编程语言。它需要在运行时将代码一行行解析成CPU能识别的机器码。它是一门解析型的语言,何为解析型语言呢?就是在运行时通过解析器将源代码一行行解析成机器码。而像C语言,C++等则是编译型的语言,即通过编译器将所有的源代码一次性编译成二进制指令,生成一个可执行的程序。解析型语言相对于编译型语言的好处就是天然具有跨平台的特点,一次编码,到处运行。

1. 开发环境配置

  1. 下载Python解释器
    如同Java需要安装JDK 编译器一样,Python也需要安装解释器来运行Python程序。
    官方的下载网址是: https://www.python.org/downloads/,映入眼帘的是最新的发布版本,如果想下载其他版本的话,可以下来找到如下图所示的信息,当前的最新版本是 python 3.9.5 版本。根据你开发电脑的系统选择不同系统的安装包。
    【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】
    【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】
    安装包下载之后双击运行进行安装。需要注意的是在Window下安装需要勾选 Add Python 3.8 to PATH,如下图1.2所示
    【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】
    安装完成之后在命令行中输入python3 验证安装的结果,如果出现如下结果就表明安装Python编译器安装成功了。
    【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】
    详细内容可以查看【Python从入门到精通】(一)就简单看看Python吧

2. 工具安装

2.1. 安装PyCharm开发工具

工欲善其事必先利其器,在实际开发中我们都是通过IDE(集成开发环境)来进行编码,为啥要使用IDE呢?这是因为IDE集成了语言编辑器,自动建立工具,除错器等等工具可以极大方便我们快速的进行开发。打个比方 我们可以将集成开发环境想象成一个台式机。虽然只需要主机就能运行起来,但是,还是需要显示器,键盘等才能用的爽。
PyCharm就是这样一款让人用的很爽的IDE开发工具。下面就简单介绍一下它的安装过程

下载安装包

点击链接 https://www.jetbrains.com/pycharm/download/
进入下来页面,PyCharm 有专业版和社区版。其中,专业版需要购买才能使用,而社区版是免费的。社区版对于日常的Python开发完全够用了。所以我们选择PyCharm的社区版进行下载安装。点击如下图所示的按钮进行安装包的下载。

【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】

安装

安装包下载好之后,我们双击安装包即可进行安装,安装过程比较简单,基本只需要安装默认的设置每一步点击Next按钮即可,不过当出现下图的窗口时需要设置一下。
【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】
设置好之后点击 Next 进入下一步的安装,知道所有的安装都完成。

使用

这里使用只需要注意一点,就是设置解释器,默认的话在Project Interpreter的选择框中是提示的是 No interpreter,即没有选中解释器,所以,我们需要手动添加。【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】
所以需要点击设置按钮设置解释器,这里选择 Add Local 设置本地的解释器。
【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】
打开解释器的设置页面之后,默认选中的是Virtualenv Environment 这个tab页,
这里Location是用来设置项目的虚拟环境,具体可以参考pycharm的使用小结,设置虚拟环境,更换镜像源
Base interpreter 用来设置解释器的路径。
【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】
至此,开发Python的脚手架已经弄好,接下来就是编码了。
如下创建了一个名为demo_1.py的文件,然后在文件中写入了如下几行代码

print("你好,世界")
a = 120
b = 230
print(a + b)

运行这些代码只需要简单的右键选中 Run ‘demo_1’ 或者 Debug ‘demo_1’ ,这两者的区别是Run demo_1是以普通模式运行代码,而 Debug ‘demo_1’ 是以调试模式运行代码。
【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】
运行结果就是:
【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】
详细内容可以查看【Python从入门到精通】(二)怎么运行Python呢?有哪些好的开发工具

3. 编码规范&注释

3.1.注释

首先介绍的是Python的注释,Python的注释分为两种:单行注释和多行注释。

  1. 单行注释
    Python使用 # 号作为单行注释的符号,其语法格式为:#注释内容 从#号开始直到这行结束为止的所有内容都是注释。例如:
# 这是单行注释
  1. 多行注释
    多行注释指一次注释程序中多行的内容(包含一行) ,Python使用三个连续的 单引号’’’ 或者三个连续的双引号""" 注释多行内容。其语法格式是如下:
'''
三个连续的单引号的多行注释
注释多行内容
'''

或者

"""
三个连续的双引号的多行注释
注释多行内容
"""

多行注释通常用来为Python文件、模块、类或者函数等添加版权或者功能描述信息(即文档注释)

3.2.缩进规则

不同于其他编程语言(如Java,或者C)采用大括号{}分割代码块,Python采用代码缩进冒号 : 来区分代码块之间的层次。如下面的代码所示:

a = -100                     
if a >= 0:
    print("输出正数" + str(a))
    print('测试')
else:
    print("输出负数" + str(a))

其中第一行代码a = -100和第二行代码if a >= 0:是在同一作用域(也就是作用范围相同),所以这两行代码并排。而第三行代码print("输出正数" + str(a)) 的作用范围是在第二行代码里面,所以需要缩进。第五行代码也是同理。第二行代码通过冒号和第三行代码的缩进来区分这两个代码块。
Python的缩进量可以使用空格或者Tab键来实现缩进,通常情况下都是采用4个空格长度作为一个缩进量的
这里需要注意的是同一个作用域的代码的缩进量要相同,不然会导致IndentationError异常错误,提示缩进量不对,如下面代码所示:第二行代码print("输出正数" + str(a)) 缩进了4个空格,而第三行代码print('测试')只缩进了2个空格。

if a >= 0:
    print("输出正数" + str(a))
  print('测试')

在Python中,对于类定义,函数定义,流程控制语句就像前面的if a>=0:,异常处理语句等,行尾的冒号和下一行缩进,表示下一个代码块的开始,而缩进的结束则表示此代码的结束。
详细内容可以查看【Python从入门到精通】(三)Python的编码规范,标识符知多少?

4. 数据类型

4.1.各种数据类型总览

【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】

4.2.整数(int)

Python3中的整数是不分类型,也就是说没有长整数类型(Long)或者短整数类型(short)之分,它的取值范围是是无限的,即不管多大或者多小的数字,Python都能轻松的应对。如下就是两个极大或者极小的整数。

>>> 100000-0000000000000000000000000000000000000000
1000000000000000000000000000000000000000000000
>>> print(-1000000000000000000000000000000000000000000000)
-1000000000000000000000000000000000000000000000

可以看出再大或者再小的数字都不会出现溢出的情况,这说明了Python对整数的处理能力非常强。

整数的不同进制

Python中可以用多种进制的来表示整数。

  1. 十进制形式
    我们平时常见的整数就是十进制形式,它由 0~9 共十个数字排列组合而成。
    注意,使用十进制形式的整数不能以 0 作为开头,除非这个数值本身就是 0。
  2. 二进制形式
    由 0 和 1 两个数字组成,书写时以0b或0B开头。例如,1001对应十进制数是 9。
  3. 八进制形式
    八进制整数由 0~7 共八个数字组成,以0o或0O开头。注意,第一个符号是数字 0,第二个符号是大写或小写的字母 O。
  4. 十六进制形式
    由 0~9 十个数字以及 A~F(或 a~f)六个字母组成,书写时以0x或0X开头。
# 二进制
a=0b1001
print('a=',a)
# 八进制
b=0o207
print('b=',b)
# 十六进制
c=0x45
print('c=',c)

运行结果是:

a= 9
b= 135
c= 69

Python 3.x允许使用下划线_作为数字(包括整数和小数)的分隔符,通常每隔三个数字添加一个下划线,比如:click = 1_301_547

4.3. 浮点数/小数(float)

在编程语言中,小数通常以浮点数的形式存储,浮点数和定点数是相对的;小数在存储过程中如果小数点发生移动,就称为浮点数;如果小数点不动,就称为定点数。

小数的书写形式

Python中的小数有两种书写形式:

  1. 十进制形式
    这就是我们经常看到的小数形式,比如101.1;234.5;0.23
  2. 指数形式
    Python小数点指数形式的写法为:aEn或aen
    a为尾数部分,是一个十进制,n为指数部分,是一个十进制,E或者e是固定的字符,用于分割尾数部分和指数部分,真的表达式是 a×10n。
    举个栗子:
    2.3E5=2.3×10的5次方
    依然还举个栗子:
x=10.01
print('x=',x)
y=-0.031
print('y=',y)
z=2.3E10
print('z=',z)
w=-0.00000001
print('w=',w)

运行结果是:

x= 10.01
y= -0.031
z= 23000000000.0
w= -1e-08

4.4.布尔类型(bool)

布尔类型用来表示真(对)或假(错),比如常见的3>2 比较算式,这个是正确的,Python中使用True来代表;再比如2>3 比较算式,这个是错误的,用False来代表。

print(3>2)
print(2>3)
print('True==1的结果是:',True==1)
print('False==0的结果是:',False==0)

运行结果是:

True
False
True==1的结果是: True
False==0的结果是: True

详细内容可以查看【Python从入门到精通】(四)Python的内置数据类型有哪些呢?数字了解一下

5. 序列

序列(sequence)指的是一块可存放多个元素的内存空间,这些元素按照一定的顺序排列。每个元素都有自己的位置(索引),可以通过这些位置(索引)来找到指定的元素。如果将序列想象成一个酒店,那么酒店里的每个房间就相当于序列中的每个元素,房间的编号就相当于元素的索引,可以通过编号(索引)找到指定的房间(元素)。

5.1.有哪些序列类型呢?

了解完了序列的基本概念,那么在Python中一共有哪些序列类型呢?如下图所示:
【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】
从图中可以看出在Python中共有7种序列类型,分别是文本序列类型(str);二进制序列类型 bytes和bytearray;列表(list);元组(tuple);集合类型(set和frozenset);范围类型(range)以及字典类型(dict)。

5.2. 按照能存储的元素划分

按照能存储的元素可以将序列类型划分为两大类:分别是:容器序列和扁平序列
容器序列:即可容纳不同数据类型的元素的序列;有 list;tuple;set;dict
举个栗子:

list=['runoob',786,2.23,'john',70.2]

这里的list保存的元素有多种数据类型,既有字符串,也有小数和整数。
扁平序列:即只能容纳相同数据类型的元素的序列;有bytes;str;bytearray,以str为例,同一个str只能都存储字符。

5.2. 按照是否可变划分

按照序列是否可变,又可分为可变序列和不可变序列。
这里的可变的意思是:序列创建成功之后,还能不能进行修改操作,比如插入,修改等等,如果可以的话则是可变的序列,如果不可以的话则是不可变序列。
可变序列有列表( list);字典(dict)等,
不可变的序列有元祖(tuple),后面的文章会详细的对这些数据类型做详细介绍。

5.3. 序列的索引

在介绍序列概念的时候,说到了序列中元素的索引,那么什么是序列的索引呢?其实就是位置的下标。 如果对C语言中的数组有所了解的话,我们知道数组的索引下标都是从0开始依次递增的正数,即第一个元素的索引下标是0,第n个元素的索引下标是n-1。序列的索引也是同理,默认情况下都是从左向右记录索引,索引值从0开始递增,即第一个元素的元素的索引值是0,第n个元素的索引值是n-1。如下图所示:
【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】
当然与C语言中数组不同的是,Python还支持索引值是负数,该类的索引是从右向左计数。换句话说,就是从最后一个元素开始计数,从索引值-1开始递减,即第n个元素的索引值是-1,第1个元素的索引值是-n,如下图所示:
【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】

5.4.序列切片

切片操作是访问序列元素的另一种方式,它可以访问一定范围内的元素,通过切片操作,可以生成一个新的序列。切片操作的语法格式是:

sname[start : end : step]

各个参数的含义分别是:

  1. sname: 表示序列的名称
  2. start:表示切片的开始索引位置(包括该位置),此参数也可以不指定,不指定的情况下会默认为0,也就是从序列的开头开始切片。
  3. end:表示切片的结束索引位置(不包括该位置),如果不指定,则默认为序列的长度。
  4. step: 表示步长,即在切片过程中,隔几个存储位置(包括当前位置)取一次元素,也就是说,如果step的值大于1,比如step为3时,则在切片取元素时,会隔2个位置去取下一个元素。
    还是举个栗子说明下吧:
str1='好好学习,天天向上'
# 取出索引下标为7的值
print(str1[7])
# 从下标0开始取值,一直取到下标为7(不包括)的索引值
print(str1[0:7])
# 从下标1开始取值,一直取到下标为4(不包括)的索引值,因为step等于2,所以会隔1个元素取值
print(str1[1:4:2])
# 取出最后一个元素
print(str1[-1])
# 从下标-9开始取值,一直取到下标为-2(不包括)的索引值
print(str1[-9:-2])

运行的结果是:

向
好好学习,天天
好习
上
好好学习,天天

5.5.序列相加

Python支持类型相同的两个序列使用"+"运算符做想加操作,它会将两个序列进行连接,但是不会去除重复的元素,即只做一个简单的拼接。

str='他叫小明'
str1='他很聪明'
print(str+str1)

运行结果是:他叫小明他很聪明

5.6.序列相乘

Python支持使用数字n乘以一个序列,其会生成一个新的序列,新序列的内容是原序列被重复了n次的结果。

str2='你好呀'
print(str2*3)

运行结果是:你好呀你好呀你好呀 ,原序列的内容重复了3次。

5.7.检查元素是否包含在序列中

Python中可以使用in关键字检查某个元素是否为序列中的成员,其语法格式为:

value in sequence

其中,value表示要检查的元素,sequence表示指定的序列。
举个栗子:查找字是否在字符串str1中。

str1='好好学习,天天向上'
print('天' in str1)

运行结果是:True

6. 字符串

*由若干个字符组成的集合就是一个字符串(str)**,Python中的字符串必须由双引号""或者单引号’'包围。其语法格式是:

"字符串内容"
'字符串内容'

如果字符串中包含了单引号需要做特殊处理。比如现在有这样一个字符串
str4='I'm a greate coder' 直接这样写有问题的。
处理的方式有两种:

  1. 对引号进行转义,通过转义符号进行转义即可:
str4='I'm a greate coder'
  1. 使用不同的引号包围字符串
str4="I'm a greate coder"

这里外层用双引号,包裹字符串里的单引号。

6.1.字符串拼接

通过+运算符
现有字符串码农飞哥好,,要求将字符串码农飞哥牛逼拼接到其后面,生成新的字符串码农飞哥好,码农飞哥牛逼
举个例子:

str6 = '码农飞哥好,'
# 使用+ 运算符号
print('+运算符拼接的结果=',(str6 + '码农飞哥牛逼'))

运行结果是:

+运算符拼接的结果= 码农飞哥好,码农飞哥牛逼

6.2.字符串截取(字符串切片)

切片操作是访问字符串的另一种方式,它可以访问一定范围内的元素,通过切片操作,可以生成一个新的字符串。切片操作的语法格式是:

sname[start : end : step]

各个参数的含义分别是:

  1. sname: 表示字符串的名称
  2. start:表示切片的开始索引位置(包括该位置),此参数也可以不指定,不指定的情况下会默认为0,也就是从序列的开头开始切片。
  3. end:表示切片的结束索引位置(不包括该位置),如果不指定,则默认为序列的长度。
  4. step: 表示步长,即在切片过程中,隔几个存储位置(包括当前位置)取一次元素,也就是说,如果step的值大于1,比如step为3时,则在切片取元素时,会隔2个位置去取下一个元素。
    还是举个栗子说明下吧:
str1='好好学习,天天向上'
# 取出索引下标为7的值
print(str1[7])
# 从下标0开始取值,一直取到下标为7(不包括)的索引值
print(str1[0:7])
# 从下标1开始取值,一直取到下标为4(不包括)的索引值,因为step等于2,所以会隔1个元素取值
print(str1[1:4:2])
# 取出最后一个元素
print(str1[-1])
# 从下标-9开始取值,一直取到下标为-2(不包括)的索引值
print(str1[-9:-2])

运行的结果是:

向
好好学习,天天
好习
上
好好学习,天天

6.3.分割字符串

Python提供了split()方法用于分割字符串,split() 方法可以实现将一个字符串按照指定的分隔符切分成多个子串,这些子串会被保存到列表中(不包含分隔符),作为方法的返回值反馈回来。该方法的基本语法格式如下:

str.split(sep,maxsplit)

此方法中各部分参数的含义分别是:

  1. str: 表示要进行分割的字符串
  2. sep: 用于指定分隔符,可以包含多个字符,此参数默认为None,表示所有空字符,包括空格,换行符"n"、制表符"t"等
  3. maxsplit: 可选参数,用于指定分割的次数,最后列表中子串的个数最多为maxsplit+1,如果不指定或者指定为-1,则表示分割次数没有限制。
    在 split 方法中,如果不指定 sep 参数,那么也不能指定 maxsplit 参数。
    举例说明下:
str = 'https://feige.blog.csdn.net/'
print('不指定分割次数', str.split('.'))
print('指定分割次数为2次',str.split('.',2))

运行结果是:

不指定分割次数 ['https://feige', 'blog', 'csdn', 'net/']
指定分割次数为2次 ['https://feige', 'blog', 'csdn.net/']

6.4.合并字符串

合并字符串与split的作用刚刚相反,Python提供了join() 方法来将列表(或元组)中包含的多个字符串连接成一个字符串。其语法结构是:

newstr = str.join(iterable)

此方法各部分的参数含义是:

  1. newstr: 表示合并后生成的新字符串
  2. str: 用于指定合并时的分隔符
  3. iterable: 做合并操作的源字符串数据,允许以列表、元组等形式提供。
    依然是举例说明:
list = ['码农飞哥', '好好学习', '非常棒']
print('通过.来拼接', '.'.join(list))
print('通过-来拼接', '-'.join(list))

运行结果是:

通过.来拼接 码农飞哥.好好学习.非常棒
通过-来拼接 码农飞哥-好好学习-非常棒

6.5.检索字符串是否以指定字符串开头(startswith())

startswith()方法用于检索字符串是否以指定字符串开头,如果是返回True;反之返回False。其语法结构是:

str.startswith(sub[,start[,end]])

此方法各个参数的含义是:

  1. str: 表示原字符串
  2. sub: 要检索的子串‘
  3. start: 指定检索开始的起始位置索引,如果不指定,则默认从头开始检索
  4. end: 指定检索的结束位置索引,如果不指定,则默认一直检索到结束。
    举个栗子说明下:
str1 = 'https://feige.blog.csdn.net/'
print('是否是以https开头', str1.startswith('https'))
print('是否是以feige开头', str1.startswith('feige', 0, 20))

运行结果是:

是否是以https开头 True
是否是以feige开头 False

6.6.检索字符串是否以指定字符串结尾(endswith())

endswith()方法用于检索字符串是否以指定字符串结尾,如果是则返回True,反之则返回False。其语法结构是:

str.endswith(sub[,start[,end]])

此方法各个参数的含义与startswith方法相同,再此就不在赘述了。

6.7.字符串大小写转换(3种)函数及用法

Python中提供了3种方法用于字符串大小写转换

  1. title()方法用于将字符串中每个单词的首字母转成大写,其他字母全部转为小写。转换完成后,此方法会返回转换得到的字符串。如果字符串中没有需要被转换的字符,此方法会将字符串原封不动地返回。其语法结构是str.title()
  2. lower()用于将字符串中的所有大写字母转换成小写字母,转换完成后,该方法会返回新得到的子串。如果字符串中原本就都是小写字母,则该方法会返回原字符串。 其语法结构是str.lower()
  3. upper()用于将字符串中的所有小写字母转换成大写字母,如果转换成功,则返回新字符串;反之,则返回原字符串。其语法结构是:str.upper()
    举例说明下吧:
str = 'feiGe勇敢飞'
print('首字母大写', str.title())
print('全部小写', str.lower())
print('全部大写', str.upper())

运行结果是:

首字母大写 Feige勇敢飞
全部小写 feige勇敢飞
全部大写 FEIGE勇敢飞

6.8.去除字符串中空格(删除特殊字符)的3种方法

Python中提供了三种方法去除字符串中空格(删除特殊字符)的3种方法,这里的特殊字符,指的是指表符(t)、回车符(r),换行符(n)等。

  1. strip(): 删除字符串前后(左右两侧)的空格或特殊字符
  2. lstrip():删除字符串前面(左边)的空格或特殊字符
  3. rstrip():删除字符串后面(右边)的空格或特殊字符
    Python的str是不可变的,因此这三个方法只是返回字符串前面或者后面空白被删除之后的副本,并不会改变字符串本身
    举个例子说明下:
str = 'n码农飞哥勇敢飞 '
print('去除前后空格(特殊字符串)', str.strip())
print('去除左边空格(特殊字符串)', str.lstrip())
print('去除右边空格(特殊字符串)', str.rstrip())

运行结果是:

去除前后空格(特殊字符串) 码农飞哥勇敢飞
去除左边空格(特殊字符串) 码农飞哥勇敢飞 
去除右边空格(特殊字符串) 
码农飞哥勇敢飞

6.9.encode()和decode()方法:字符串编码转换

最早的字符串编码是ASCll编码,它仅仅对10个数字,26个大小写英文字母以及一些特殊字符进行了编码,ASCII码最多只能表示256个字符,每个字符只需要占用1个字节。为了兼容各国的文字,相继出现了GBK,GB2312,UTF-8编码等,UTF-8是国际通用的编码格式,它包含了全世界所有国家需要用到的字符,其规定英文字符占用1个字节,中文字符占用3个字节。

  1. encode() 方法为字符串类型(str)提供的方法,用于将 str 类型转换成 bytes 类型,这个过程也称为“编码”。其语法结构是:str.encode([encoding="utf-8"][,errors="strict"])
  2. 将bytes类型的二进制数据转换成str类型。这个过程也称为"解码",其语法结构是:bytes.decode([encoding="utf-8"][,errors="strict"])
    举个例子说明下:
str = '码农飞哥加油'
bytes = str.encode()
print('编码', bytes)
print('解码', bytes.decode())

运行结果是:

编码 b'xe7xa0x81xe5x86x9cxe9xa3x9exe5x93xa5xe5x8axa0xe6xb2xb9'
解码 码农飞哥加油

默认的编码格式是UTF-8,编码和解码的格式要相同,不然会解码失败。

6.9.序列化和反序列化

在实际工作中我们经常要将一个数据对象序列化成字符串,也会将一个字符串反序列化成一个数据对象。Python自带的序列化模块是json模块。

  1. json.dumps() 方法是将Python对象转成字符串
  2. json.loads()方法是将已编码的 JSON 字符串解码为 Python 对象
    举个例子说明下:
import json

dict = {'学号': 1001, 'name': "张三", 'score': [{'语文': 90, '数学': 100}]}
str = json.dumps(dict,ensure_ascii=False)
print('序列化成字符串', str, type(str))
dict2 = json.loads(str)
print('反序列化成对象', dict2, type(dict2))

运行结果是:

序列化成字符串 {"name": "张三", "score": [{"数学": 100, "语文": 90}], "学号": 1001} <class 'str'>
反序列化成对象 {'name': '张三', 'score': [{'数学': 100, '语文': 90}], '学号': 1001} <class 'dict'>

详细内容可以查看
【Python从入门到精通】(五)Python内置的数据类型-序列和字符串,没有女友,不是保姆,只有拿来就能用的干货
【Python从入门到精通】(九)Python中字符串的各种骚操作你已经烂熟于心了么?【收藏下来就挺好的】

7. 列表&元组

7.1.列表(list)的介绍

列表作为Python序列类型中的一种,其也是用于存储多个元素的一块内存空间,这些元素按照一定的顺序排列。其数据结构是:

[element1, element2, element3, ..., elementn]

element1~elementn表示列表中的元素,元素的数据格式没有限制,只要是Python支持的数据格式都可以往里面方。同时因为列表支持自动扩容,所以它可变序列,即可以动态的修改列表,即可以修改,新增,删除列表元素。看个爽图吧!

7.2.列表的操作

首先介绍的是对列表的操作:包括列表的创建,列表的删除等!其中创建一个列表的方式有两种:
第一种方式:
通过[]包裹列表中的元素,每个元素之间通过逗号,分割。元素类型不限并且同一列表中的每个元素的类型可以不相同,但是不建议这样做,因为如果每个元素的数据类型都不同的话则非常不方便对列表进行遍历解析。所以建议一个列表只存同一种类型的元素

   list=[element1, element2, element3, ..., elementn]

例如:test_list = ['测试', 2, ['码农飞哥', '小伟'], (12, 23)]
PS: 空列表的定义是list=[]
第二种方式:
通过list(iterable)函数来创建列表,list函数是Python内置的函数。该函数传入的参数必须是可迭代的序列,比如字符串,列表,元组等等,如果iterable传入为空,则会创建一个空的列表。iterable不能只传一个数字。

classmates1 = list('码农飞哥')
print(classmates1)

生成的列表是:['码', '农', '飞', '哥']

7.3. 向列表中新增元素

向列表中新增元素的方法有四种,分别是:
第一种: 使用**+运算符将多个列表**连接起来。相当于在第一个列表的末尾添加上另一个列表。其语法格式是listname1+listname2

name_list = ['码农飞哥', '小伟', '小小伟']
name_list2 = ['python', 'java']
print(name_list + name_list2)

输出结果是:['码农飞哥', '小伟', '小小伟', 'python', 'java'],可以看出将name_list2中的每个元素都添加到了name_list的末尾。
第二种:使用append()方法添加元素
append()方法用于向列表末尾添加元素,其语法格式是:listname.append(p_object)其中listname表示要添加元素的列表,p_object表示要添加到列表末尾的元素,可以是字符串,数字,也可以是一个序列。举个栗子:

name_list.append('Adam')
print(name_list)
name_list.append(['test', 'test1'])
print(name_list)

运行结果是:

['码农飞哥', '小伟', '小小伟', 'Adam']
['码农飞哥', '小伟', '小小伟', 'Adam', ['test', 'test1']]

可以看出待添加的元素都成功的添加到了原列表的末尾处。并且当添加的元素是一个序列时,则会将该序列当成一个整体。
第三种:使用extend()方法
extend()方法跟append()方法的用法相同,同样是向列表末尾添加元素。元素的类型只需要Python支持的数据类型即可。不过与append()方法不同的是,当添加的元素是序列时,extend()方法不会将列表当成一个整体,而是将每个元素添加到列表末尾。还是上面的那个例子:

name_list = ['码农飞哥', '小伟', '小小伟']
name_list.extend('Adam')
print(name_list)
name_list.extend(['test', 'test1'])
print(name_list)

运行结果是:

['码农飞哥', '小伟', '小小伟', 'A', 'd', 'a', 'm']
['码农飞哥', '小伟', '小小伟', 'A', 'd', 'a', 'm', 'test', 'test1']

从结果看出,当添加字符串时会将字符串中的每个字符作为一个元素添加到列表的末尾处,当添加的列表时会将列表中的每个元素添加到末尾处。
第四种:使用insert()方法
前面介绍的几种插入方法,都只能向列表的末尾处插入元素,如果想在列表指定位置插入元素则无能为力。insert()方法正式用于处理这种问题而来的。其语法结构是listname.insert(index, p_object) 其中index表示指定位置的索引值,insert()会将p_object插入到listname列表第index个元素的位置。与append()方法相同的是,如果待添加的元素的是序列,则insert()会将该序列当成一个整体插入到列表的指定位置处。举个栗子:

name_list = ['码农飞哥', '小伟', '小小伟']
name_list.insert(1, 'Jack')
print(name_list)
name_list.insert(2, ['test', 'test1'])
print(name_list)

运行结果是:

['码农飞哥', 'Jack', '小伟', '小小伟']
['码农飞哥', 'Jack', ['test', 'test1'], '小伟', '小小伟']

7.4. 修改列表中的元素

说完了列表中元素新增的方法,接着让我们来看看修改列表中的元素相关的方法。修改列表元素的方法有两种:
第一种:修改单个元素:
修改单个元素的方法就是对某个索引上的元素进行重新赋值。其语法结构是:listname[index]=newValue,就是将列表listname中索引值为index位置上的元素替换成newValue。
举个栗子:

name_list = ['码农飞哥', '小伟', '小小伟']
name_list[1] = 'Sarah'
print(name_list)

运行结果:['码农飞哥', 'Sarah', '小小伟'] 从结果可以看出索引为1处的元素值被成功修改成了Sarch。
第二种:通过切片语法修改一组元素
通过切片语法可以修改一组元素,其语法结构是:listname[start:end:step],其中,listname表示列表名称,start表示起始位置,end表示结束位置(不包括),step表示步长,如果不指定步长,Python就不要求新赋值的元素个数与原来的元素个数相同,这意味着,该操作可以为列表添加元素,也可以为列表删除元素。举个栗子:

name_list = ['码农飞哥', '小伟', '小小伟']
name_list[0:1] = ['飞哥', '牛逼']
print(name_list)

运行结果是:['飞哥', '牛逼', '小伟', '小小伟'] ,从结果可以看出将原列表中索引为0处的元素值已经被替换为飞哥,并且插入了牛逼 这个元素。

7.5. 删除列表中的元素

删除列表中元素的方法共有四种。
第一种:根据索引值删除元素的del关键字
根据索引值删除元素的del关键字有两种形式,一种是删除单个元素,del listname[index],一种是根据切片删除多个元素del listname[start : end],其中,listname表示列表名称,start表示起始索引,end表示结束索引,del会删除从索引start到end之间的元素,但是不包括end位置的元素。还是举个栗子:

name_list = ['码农飞哥', '小伟', '小小伟', '超人']
name_list2 = name_list
print('原始的name_list={0}'.format(name_list))
print('原始的name_list2={0}'.format(name_list2))
# 删除索引0到2之间的元素,即删除索引0和索引1两个位置的元素
del name_list[0:2]
print('使用del删除元素后name_list={0}'.format(name_list))
print('使用del删除元素后name_list2={0}'.format(name_list2))
del name_list
print('使用del删除列表后name_list2={0}'.format(name_list2))

运行结果是:

原始的name_list=['码农飞哥', '小伟', '小小伟', '超人']
原始的name_list2=['码农飞哥', '小伟', '小小伟', '超人']
使用del删除元素后name_list=['小小伟', '超人']
使用del删除元素后name_list2=['小小伟', '超人']
使用del删除列表后name_list2=['小小伟', '超人']

可以看出用del删除列表元素时是真实的删除了内存数据的,但是用del删除列表时,则只是删除了变量,name_list2所指向的内存数据还是存在的。
第二种:根据索引值删除元素的pop()方法
根据索引值删除元素的pop()方法的语法结构是:listname.pop(index),其中,listname表示列表名称,index表示索引值,如果不写index参数,默认会删除列表中最后一个元素,类似于数据结构中的出栈操作。举个例子:

name_list = ['码农飞哥', '小伟', '小小伟', '超人']
# 删除list末尾的元素
name_list.pop()
print(name_list)
# 删除指定位置的元素,用pop(i)方法,其中i是索引位置
name_list.pop(1)
print(name_list)

运行结果是:

['码农飞哥', '小伟', '小小伟']
['码农飞哥', '小小伟']

第三种:根据元素值进行删除的remove()方法
根据元素值进行删除的remove()方法,其语法结构是:listname.remove(object),其中listname表示列表的名称,object表示待删除的元素名称。需要注意的是:如果元素在列表中不存在则会报ValueError的错误。举个栗子:

name_list = ['码农飞哥', '小伟', '小小伟', '超人']
name_list.remove('小小伟')
print(name_list)

运行结果是:['码农飞哥', '小伟', '超人']
第四种:删除列表中的所有元素clear()方法
通过clear()方法可以删除掉列表中的所有元素,其语法结构是:listname.clear(),其中listname表示列表的名称。还是举个栗子吧:

name_list = ['码农飞哥', '小伟', '小小伟', '超人']
name_list.clear()
print(name_list)

运行结果是:[],可以看出列表中元素被全部清空了。

7.6.列表中元素的查找以及访问

说完了第五浅列表元素的删除,略感疲惫。接着进行第六浅吧!看看列表中元素的查找以及访问。看完这个之后,列表相关的内容也就告一段落了。

访问列表中的元素

访问列表中的元素有两种方式,分别是通过索引定位访问单个元素,通过切片访问多个元素。
第一种:通过索引定位访问单个元素,其语法结构是:listname[index] ,其中listname表示列表的名字,index表示要查找元素的索引值。
第二种:通过切片的方式访问多个元素,其语法结构是:listname[start:end:step]。其中,listname表示列表的名字,start表示开始索引,end表示结束索引(不包括end位置),step表示步长。同样是举个栗子:

list2 = ['码农飞哥', '小伟', '小小伟',123]
print(list2[0])  # 输出列表的第一个元素
print(list2[1:3])  # 输出第二个至第三个元素
print(list2[2:])  # 输出从第三个开始至列表末尾的所有元素

运行结果是:

码农飞哥
['小伟', '小小伟']
['小小伟', 123]

查找某个元素在列表中出现的位置 index()

indext()方法用来查找某个元素在列表中出现的位置(也就是索引),如果该元素在列表中不存在,则会报ValueError错误。其语法结构是:listname.index(object, start, end) 其中listname表示列表的名字,object表示要查找的元素,start表示起始索引,end表示结束索引(不包括)。

name_list = ['码农飞哥', '小伟', '小小伟', '超人']
print(name_list.index('小伟', 0, 2))

运行结果是:1

7.8. Python新增元素中各个方法的区别

前面介绍了使用+运算符,使用append方法,使用extend方法都可以新增元素,那么他们到底有啥区别呢?还是举例说明吧;

name_list = ['码农飞哥', '小伟', '小小伟', '超人']
name_list2 = ['牛魔王']
name_list3 = name_list + name_list2
print("原始的name_list的值={0};内存地址={1}".format(name_list, id(name_list)))
print("使用+运算符后name_list3的值={0};内存地址={1}".format(name_list3, id(name_list3)))
print("使用+运算符后name_list的值{0};内存地址={1}".format(name_list, id(name_list)))
name_list4 = name_list.append('牛魔王')
print('使用append方法后name_list4的值={0};内存地址={1}'.format(name_list4, id(name_list4)))
print("使用append方法后name_list的值{0};内存地址={1}".format(name_list, id(name_list)))
name_list5 = name_list.extend('牛魔王')
print('使用extend方法后name_list5的值={0};内存地址={1}'.format(name_list4, id(name_list4)))
print("使用extend方法后name_list的值{0};内存地址={1}".format(name_list, id(name_list)))

运行结果是:

原始的name_list的值=['码农飞哥', '小伟', '小小伟', '超人'];内存地址=2069467533448
使用+运算符后name_list3的值=['码农飞哥', '小伟', '小小伟', '超人', '牛魔王'];内存地址=2069467533896
使用+运算符后name_list的值['码农飞哥', '小伟', '小小伟', '超人'];内存地址=2069467533448
使用append方法后name_list4的值=None;内存地址=2012521616
使用append方法后name_list的值['码农飞哥', '小伟', '小小伟', '超人', '牛魔王'];内存地址=2069467533448
使用extend方法后name_list5的值=None;内存地址=2012521616
使用extend方法后name_list的值['码农飞哥', '小伟', '小小伟', '超人', '牛魔王', '牛', '魔', '王'];内存地址=2069467533448

从运行结果可以看出如下几点:

  1. 使用+运算符是创建一个新的列表,新列表的地址与原列表的地址不相同,并且原始列表的内容不会改变。
  2. append方法和extend方法都是修改原始列表的内容,并且都没有返回值,所以两者都不能使用链式表达式。
  3. 当待添加的元素是列表时,append方法会将列表当成一个整体,而extend不会。

8. 元组(tuple)

8.1. 元组(tuple)的介绍

说完了列表,接着让我们来看看另外一个重要的序列–元组(tuple),和列表类似,元组也是由一系列按特定书序排序的元素组成,与列表最重要的区别是,元组属于不可变序列,即元组一旦被创建,它的元素就不可更改了。

8.2.元组的创建方式

第一种:使用()直接创建
使用()创建元组的语法结构是tuplename=(element1,element2,....,elementn),其中tuplename表示元组的变量名,element1~elementn表示元组中的元素。小括号不是必须的,只要将元素用逗号分隔,Python就会将其视为元组。还是举个栗子:

#创建元组
tuple_name = ('码农飞哥', '小伟', '小小伟', '超人')
print(tuple_name)
#去掉小括号创建元组
tuple2 = '码农飞哥', '小伟', '小小伟', '超人'
print(type(tuple2))

运行结果是:

('码农飞哥', '小伟', '小小伟', '超人')
<class 'tuple'>

第二种:使用tuple()函数创建
与列表类似的,我们可以通过tuple(iterable)函数来创建元组,如果iterable传入为空,则创建一个空的元组,iterable 参数必须是可迭代的序列,比如字符串,列表,元组等。同样的iterable不能传入一个数字。举个栗子:

name_list = ['码农飞哥', '小伟', '小小伟', '超人']
print(tuple(name_list))
print(tuple('码农飞哥'))

运行结果是:

('码农飞哥', '小伟', '小小伟', '超人')
('码', '农', '飞', '哥')

由于元组是不可变序列,所以没有修改元素相关的方法,只能对元组中的元素进行查看。查看元素的方式也与列表类似,共两种方式:
第一种:通过索引(index)访问元组中的元素,其语法结构是tuplename[index]
第二种:通过切片的方式访问,其语法结构是:tuplename[start:end:step]
相关参数的描述在此不再赘述了。依然是举例说明:

tuple_name = ('码农飞哥', '小伟', '小小伟', '超人')
# 获取索引为1的元素值
print(tuple_name[1])
#获取索引为1到索引为2之间的元素值,不包括索引2本身
print(tuple_name[0:2])

运行结果是:

小伟
('码农飞哥', '小伟')

元组中的元素不能修改,不过可以通过 + 来生成一个新的元组。

详细内容可以查看【Python从入门到精通】(六)Python内置的数据类型-列表(list)和元组(tuple),九浅一深,十个章节,不信你用不到

9.字典

9.1.创建一个字典

创建字典的方式有很多种,下面罗列几种比较常见的方法。
第一种:使用 {} 符号来创建字典,其语法结构是dictname={'key1':'value1', 'key2':'value2', ..., 'keyn':valuen}
第二种:使用fromkeys方法,其语法结构是dictname = dict.fromkeys(list,value=None), 其中,list参数表示字典中所有键的列表(list),value参数表示默认值,如果不写则为所有的值都为空值None。
第三种:使用dict方法,其分为四种情况:

  1. dict() -> 创建一个空字典
  2. dict(mapping) -> 创建一个字典,初始化时其键值分别来自于mapping中的key,value。
  3. dict(iterable) -> 创建一个字典,初始化时会遍历iterable得到其键值。
for k, v in iterable:
                d[k] = v
  1. dict(**kwargs) -> **kwargs 是可变函数,其调用的语法格式是:dict(key1=value1,key2=value2,...keyn=valuen),例如:dict(name='码农飞哥', age=17, weight=63)
    这三种创建字典的方式都介绍完了,下面就来看看示例说明吧:
#1. 创建字典
d = {'name': '码农飞哥', 'age': 18, 'height': 185}
print(d)
list = ['name', 'age', 'height']
# 2. fromkeys方法
dict_demo = dict.fromkeys(list)
dict_demo1 = dict.fromkeys(list, '测试')
print(dict_demo)
print(dict_demo1)
# 通过dict()映射创建字典,传入列表或者元组
demo = [('name', '码农飞哥'), ('age', 19)]
dict_demo2 = dict(demo)
print(dict_demo2)
dict_demo21 = dict(name='码农飞哥', age=17, weight=63)
print(dict_demo21)

运行结果是:

{'name': '码农飞哥', 'age': 18, 'height': 185}
{'name': None, 'age': None, 'height': None}
{'name': '测试', 'age': '测试', 'height': '测试'}
{'name': '码农飞哥', 'age': 19}
{'name': '码农飞哥', 'age': 17, 'weight': 63}

9.2.字典的访问

说完了字典的创建之后,接着就让我们来看看字典的访问。字典不同于列表和元组,字典中的元素不是依次存储在内存区域中的;所以,字典中的元素不能通过索引来访问,只能是通过键来查找对应的值。 ,其有两种不同的写法。

  1. 第一种方式的语法格式是dictname[key] ,其中dictname表示字典的名称,key表示指定的键。如果指定的键不存在的话,则会报KeyError 错误
  2. 第二种方式的语法格式是dictname.get(key),其中dictname表示字典的名称,key表示指定的键。如果指定的键不存在的话,则会返回None。
    举个栗子说明下吧,下面代码的意思是根据键名为name 查找其对应的值。
dict_demo5 = {'name': '码农飞哥', 'age': 18, 'height': 185}
print(dict_demo5['name'])
print(dict_demo5.get('name'))
print('键不存在的情况返回结果=',dict_demo5.get('test'))

运行结果是:

码农飞哥
码农飞哥
键不存在的情况返回结果= None

9.3.添加和修改键值对

添加键值对的方法很简单,其语法结构是dictname[key]=value,如果key在字典中不存在的话,则会新增一个键值对。如果key在字典中存在的话,则会更新原来键所对应的值。依然是举例说明下:本例中代码的结果是增加键值对 sex='男',把键height对应的值改成了190

# 添加键值对
dict_demo6 = {'name': '码农飞哥', 'age': 18, 'height': 185}
dict_demo6['sex'] = '男'
print('新增键值对的结果={0}'.format(dict_demo6))
# 修改键值对
dict_demo6['height'] = 190
print('修改键值对的结果={0}'.format(dict_demo6))

运行结果是:

新增键值对的结果={'age': 18, 'name': '码农飞哥', 'height': 185, 'sex': '男'}
修改键值对的结果={'age': 18, 'name': '码农飞哥', 'height': 190, 'sex': '男'}

当然修改和删除键值对也可以通过update方法来实现,其具体的语法格式是:dictname.update(dict) ,其中,dictname为字典的名称,dict为要修改的字典的值。该方法既可以新增键值对,也可以修改键值对。 该方法没有返回值,即是在原字典上修改元素的。下面例子中就是将键name的值改成了飞飞1024,键age对应的值改成了25。并新增了键值对 like=学习

# update方法
dict_demo7 = {'name': '码农飞哥', 'age': 18, 'height': 185, 'width': 100}
dict_demo7.update({'name': '飞飞1024', 'age': 25, 'like': '学习'})
print('update方法返回结果={}', dict_demo7)

运行结果为:

update方法返回结果={} {'height': 185, 'like': '学习', 'width': 100, 'name': '飞飞1024', 'age': 25}

9.4.删除键值对

删除键值对的方法有三种:

  1. 第一种是del dictname[key],使用del关键字,其中dictname为字典的名称,key为要删除的键。如果键不存在的话则会报KeyError错误。
  2. 第二种方式是通过pop方法,其语法结构是:dictname.pop(key)。该方法是用于删除指定键值对,没有返回值,如果key不存在的话不会报错。
  3. 第三种方式是通过popitem方法,其语法结构是:dictname.popitem()。该方法用于删除字典中最后一个键值对。举例说明下吧:
dict_demo10 = {'name': '码农飞哥', 'age': 18, 'height': 185, 'width': 100}
# 删除键值对
del dict_demo6['height']
print('删除键height对之后的结果=', dict_demo6)
# pop()方法和popitem()方法
dict_demo10.pop('width')
print('pop方法调用删除键width之后结果=', dict_demo10)
dict_demo10 = {'name': '码农飞哥', 'age': 18, 'height': 185, 'width': 100}
dict_demo10.popitem()
print('popitem方法调用之后结果=', dict_demo10)

运行结果是:

删除键height对之后的结果= {'name': '码农飞哥', 'sex': '男', 'age': 18}
pop方法调用删除键width之后结果= {'name': '码农飞哥', 'height': 185, 'age': 18}
popitem方法调用之后结果= {'name': '码农飞哥', 'age': 18, 'height': 185}

可以看出popitem方法删除的键是最后一个键width。
详细内容可以查看【Python从入门到精通】(七)Python字典(dict)让人人都能找到自己的另一半(键值对,成双成对)

10. 推导式&生成器

10.1.range快速生成列表推导式

列表推导式的语法格式是

[表达式 for 迭代变量 in 可迭代对象 [if 条件表达式] ]

此格式中,[if 条件表达式]不是必须的,可以使用,也可以省略。下面就是输出1~10的列表的乘积的一个例子:

L = [x * x for x in range(1, 11)]
print(L)

此表达式相当于

L = []
for x in range(1, 11):
    L.append(x * x)
print(L)

运行结果是:

[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

来点复杂的吧,下面就是输出

print([x for x in range(1, 11) if x % 2 == 0])

运行结果是[2, 4, 6, 8, 10]
再来点复杂的,使用多个循环,生成推导式。

d_list = [(x, y) for x in range(5) for y in range(4)]
print(d_list)

运行结果是:

[(0, 0), (0, 1), (0, 2), (0, 3), (1, 0), (1, 1), (1, 2), (1, 3), (2, 0), (2, 1), (2, 2), (2, 3), (3, 0), (3, 1), (3, 2), (3, 3), (4, 0), (4, 1), (4, 2), (4, 3)]

上面代码,x是遍历range(5)的迭代变量(计数器),因此该x可迭代5次,y是遍历range(4)的计数器,因此该y可迭代4次。因此,该(x,y)表达式一共迭代了20次。它相当于下面这样一个嵌套表达式。

dd_list = []
for x in range(5):
    for y in range(4):
        dd_list.append((x, y))
print(dd_list)

10.2.range快速生成元组推导式

元组推导式与列表推导式类似,其语法结构是:

(表达式 for 迭代变量 in 可迭代对象 [if 条件表达式] )

此格式中,[if 条件表达式]不是必须的,可以使用,也可以省略。下面就是输出1~10的元组的乘积的一个例子:

d_tuple = (x * x for x in range(1, 11))
print(d_tuple)

运行结果是:

<generator object <genexpr> at 0x103322e08>

从上面的执行结果可以看出,使用元组推导式生成的结果并不是一个元组,而是一个生成器对象。
使用tuple()函数,可以直接将生成器对象转换成元组。例如:

d_tuple = (x * x for x in range(1, 11))
print(tuple(d_tuple))

输出结果是(1, 4, 9, 16, 25, 36, 49, 64, 81, 100)

10.4. 字典推导式

字典推导式的语法结构是:

{表达式 for 迭代变量 in 可迭代对象 [if 条件表达式]}

其中[if 条件表达式]可以使用,也可以省略。举个例子:

key_list = ['姓名:码农飞哥', '年龄:18', '爱好:写博客']
test_dict = {key.split(':')[0]: key.split(':')[1] for key in key_list}
print(test_dict)

运行结果是:

{'爱好': '写博客', '年龄': '18', '姓名': '码农飞哥'}

10.5.生成器另外一种方式 yield

通过yield关键字配合循环可以做一个生成器,就像下面这样

def generate():
    a = 2
    while True:
        a += 1
        yield a

b = generate()
print(b)
print(next(b))
print(next(b))
print(next(b))

运行结果

<generator object generate at 0x7fc01af19b30>
3
4
5

这里generate方法返回的就是一个生成器对象generator,因为它内部使用了yield关键字,
调用一次next()方法返回一个生成器中的结果。这就很像单例模式中的懒汉模式,他并并不像饿汉模式一样事先将列表数据生成好。

11. 判断以及循环

11.1. 流程控制

流程控制有三种结构,一种是顺序结构,一种是选择(分支)结构,一种是循环结构。
顺序结构:就是让程序按照从头到尾的顺序执行代码,不重复执行任何一行代码,也不跳过任何一行代码。一步一个脚印表示的就是这个意思。
选择(分支)结构:就是让程序根据不同的条件执行不同的代码,比如:根据年龄判断某个人是否是成年人。
循环结构: 就是让程序循环执行某一段代码。顺序的流程这里不做介绍了。

11.2. 选择结构(if,else):

if语句

只使用if语句是Python中最简单的形式。如果满足条件则执行表达式。则跳过表达式的执行。其伪代码是:

if 条件为真:
   代码块

如果if 后面的条件为真则执行代码块。否则则跳过代码的执行。
其流程图是:
【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】
就是说只使用if的话,则表达式成立的话执行代码块,不成立的话就结束。
下面就是一个简单的例子,如果满足a==1这个条件则打印a,否则跳过该语句。

a = 1
if a == 1:
    print(a)

if else语句

if else语句是if的变体,如果满足条件的话则执行代码块1,否则则执行代码块2。其伪代码是:

if 条件为真:
   代码块1
else 
   代码块2

流程图是:
【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】
同时使用if和else的话,则表达式成立的话执行一个代码块,表达式不成立的话则执行另一个代码块。
举个简单的例子吧。

age = 3
if age >= 18:
    print('your age is', age)
    print('adult')
else:
    print("your age is", age)
    print('kid')

根据输入的年龄判断某人是否是成年人。如果age大于等于18岁,则输出adult,否则输出kid。

if elif else语句

if elif else语句针对的就是多个条件判断的情况,如果if条件不满足则执行elif的条件,如果elif的条件也不满足的话,则执行else里面的表达式。其伪代码是:

if 条件为真:
   表达式a
elif 条件为真:
   表达式b
....
elif 条件为真:
   表达是n-1
else 
   表达式n

其中elif可以有多个,但是elif不能单独使用,必须搭配if和else一起使用
需要注意的是if,elif和else后面的代码块一定要缩进,而且缩进量要大于if,elif和else本身,建议的缩进量是4个空格。同一代码中所有语句都要有相同的缩进。 依然是举例说明:

bmi = 80.5 / (1.75 * 1.75)
if bmi < 18.5:
    print('过轻')
elif 18.5 <= bmi < 25:
    print('正常')
elif 25 <= bmi < 28:
    print('过重')
elif 28 <= bmi < 32:
    print('肥胖')
else:
    print('严重肥胖')
pass

下面就是根据bmi标准来判断一个人是过轻,正常还是肥胖。pass是Python中的关键字,用来让解释器跳过此处,什么都不做。

11.3. while循环语句详解

while是作为循环的一个关键字。其伪代码是:

while 条件表达式:
    代码块

一定要保证循环条件有变成假的时候,否则这个循环将成为一个死循环,即该循环无法结束。 其流程图是:
【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】
如果while中的表达式成立的话则执行循环体,否则的话则直接结束。
举个栗子:计算从1~100的求和,这就是一个经典的运用循环的场景

sum = 0
n = 1
while n <= 100:
    sum = sum + n
    n = n + 1
print('sum=', sum)

运行结果是sum= 5050,这个循环的结束条件是n>100,也就是说当n>100是会跳出循环。

11.4.for循环

在介绍range函数时用到了for关键字,这里介绍一下for关键字的使用。其语法结构是:

for 迭代变量 in 字符串|列表|元组|字典|集合:
    代码块

字符串,列表,元祖,字典,集合都可以还用for来迭代。其流程图是:
【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】

for 循环就是:首先根据in 关键字判断序列中是否有项,如果有的话则取下一项,接着执行循环体。如果没有的话则直接结束循环。

详细内容可以查看【Python从入门到精通】(十)Python流程控制的关键字该怎么用呢?列表推导式,生成器【收藏下来,常看常新】

12. 函数

12.1. 函数定义

函数是按照固定格式封装组织的可以重复使用的代码段。它能提高应用的模块性和代码的重复利用率。
函数定义的语法格式是:

def  函数名(参数列表):
	 代码块
	[return [返回值]]

函数名:其实就是一个符合Python语法的标识符,函数名最好能体现该函数的功能,比如: save_user。
形参列表:设置该函数可以接收多少个参数,多个参数之间用逗号(,)分割。需要注意的是没有参数的话,也需要留一对空的()
[return[返回值]]:整体作为函数的可选参数,用于设置该函数的返回值。也就是说,一个函数,
可以有返回值,也可以没有返回值。

12.2 函数调用

调用函数的语法格式是:

[返回值]=函数名([形参值])

函数名即指的是要调用的函数的名称,形参值指的是当初创建函数时要求传入的各个形参的值。
如果该函数有返回值,我们可以通过一个变量来接收该值,当然也可以不接收。需要注意的是,函数有多少个形参,那么调用的时候就需要传入多少个值
且顺序必须和创建函数时一致。即便该函数没有参数,函数名后的小括号也不能省略。
举个栗子吧:

def my_abs(x):
    """
    返回绝对值
    :param x:
    :return:
    """
    if not isinstance(x, (int, float)):
        raise TypeError('传入的数据类型不对')
    if x >= 0:
        return x
    else:
        return -x

调用代码是:

    x = my_abs(-3)
    print(x)
    print(my_abs.__doc__)

运行结果是:

3

    返回绝对值
    :param x:
    :return:

这是一个获取绝对值的函数,其函数名是my_abs,通过函数名可以让人大致明白函数的作用。形式参数是x。通过__doc__可以查看函数的说明文档。其返回值是处理后的值。

12.3 函数值传递和引用传递(形参和实参的介绍)

介绍函数值传递和引用传递之前首先需要了解两个概念。

  1. 形式参数(简称形参):在定义函数时,函数名后面括号中的参数就是形式参数,可以将形参想象成剧本中的角色。
  2. 实际参数(简称实参):在调用函数时,函数名后面括号中的参数称为实际参数,也就是函数的调用者给函数的参数,可以将实参想象成演角色的演员。
    函数参数传递方式分为两种:分别是值传递和引用传递:
  3. 值传递:适用于实参类型为不可变类型(字符串,数字,元组)
  4. 引用(地址)传递:适用于实参类型为可变类型(列表,字典)
    值传递和引用传递的区别是:函数参数进行值传递时,若形参发生改变,不会影响实参的值。而应用传递的话,改变形参的值,实参的值也会一同改变。依然是举例说明:
    函数param_test会将形参obj变成 obj+obj。如果是值传递则调用函数param_test之后,实参的值不变。如果是引用传递的话则调用param_test之后,实参的值也会变成 obj+obj。
def param_test(obj):
    obj += obj
    print('形参值为:', obj)


print('*******值传递*****')
a = '码农飞哥'
print('a的值为:', a)
param_test(a)
print('实参的值为:', a)

print("*******引用传递*****")
b = [1, 2, 3]
print('b的值为:', b)
param_test(b)
print('实参的值为:', b)

运行结果是:

*******值传递*****
a的值为: 码农飞哥
形参值为: 码农飞哥码农飞哥
实参的值为: 码农飞哥
*******引用传递*****
b的值为: [1, 2, 3]
形参值为: [1, 2, 3, 1, 2, 3]
实参的值为: [1, 2, 3, 1, 2, 3]

12.4. Python位置参数

位置参数,有时也被称为必备参数,指的是必须按照正确的顺序将实参传到函数中,换句话说,调用函数时传入实参的数量和位置必须和定义函数时保持一致。如果不一致的话,则在程序运行时Python解释器会报TypeError异常。举个例子,下面演示调用函数事参数传入的数量不对的情况。

def girth(width , height):
    return 2 * width+height
#调用函数时,必须传递 2 个参数,否则会引发错误
print(girth(3))

运行之后直接报Traceback错误。

Traceback (most recent call last):
  File "/Volumes/Develop/Python_learn/PycharmProjects/python_demo_1/demo/function/locate_fun.py", line 6, in <module>
    print(girth(3))
TypeError: girth() missing 1 required positional argument: 'height'

传入参数的位置不对的情况,本例中本想传入name的值为码农飞哥,age的值为18。结果入参顺序不对导致得到的结果不对。

def print_info(name, age):
    print('姓名=' + name + " 年龄=" + str(age))
    
print_info(18,'码农飞哥')

那么怎么处理这种情况呢?有两种方式:

  1. 严格按照形参的数量和位置入参。
  2. 按照关键字参数入参,所谓的关键字参数就是指使用形参的名字来确定输入的参数值。通过此方式制定函数实参时,不再需要与形参的位置完全一致,只要将参数名写正确即可。还是以上面的函数为例:
    利用关键字参数来调用函数的话则是这样写:
def print_info(name, age):
    print('姓名=' + name + " 年龄=" + str(age))
print_info(age=18,name='码农飞哥')

运行结果是:

姓名=码农飞哥 年龄=18

可以看出关键字参数入参时,不需要保证入参的顺序跟形参的顺序保持一致。
【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】

12.5. 默认参数设置

前面介绍的位置参数,就是说调用函数时必须要传入该参数。但是有些场景下我们并不想传入所有的参数。这种情况下就可以使用默认参数了。不过需要注意的是:指定有默认值的形式参数必须在所有没默认值的参数的最后,否则会产生语法错误。其语法格式是:

def 函数名(...,形参名,形参名=默认值):
    代码块

下面给出一个示例,该函数是记录学生的信息,有两个有默认值的参数,分别是age和city。它们都被置于函数的形参列表最后处。

def enroll(name, gender, age=6, city='Beijing'):
    print('name:', name)
    print("gender:", gender)
    print("age:", age)
    print("city:", city)

print(enroll('张三', '一年级'))
print('************************** ')
print(enroll('李四', '二年级', 7))

运行结果是:

name: 张三
gender: 一年级
age: 6
city: Beijing
None
************************** 
name: 李四
gender: 二年级
age: 7
city: Beijing
None

从上面代码可以看出:1. 可以不用传入有默认值的参数。2. 如果传入默认的参数,则会覆盖掉默认值。

12.5. 可变参数

Python函数可变参数(*args,**kwargs),又称为不定长参数,即传入函数中的实际参数可以是任意多个,Python定义可以变参数,主要有以下两种形式:

  1. 在形参前添加一个*,格式是*args。表示创建一个名为args的空元组,该元组可以接受任意多个外界传入的非关键字实参。必须以非关键字参数的形式给普通参数传值,否则Python解释器会把所有参数都优先传给可变参数。
  2. **kwargs表示创建一个名为kwargs的空字典,该字典可以接受任意多个以关键字参数赋值的实参。举个🌰,下面就是根据传入的值求和。

def calc(*numbers):
    sum = 0
    for n in numbers:
        sum = sum + n
    return sum

print(calc(10, 9))

运行的结果是:19
再举个例子呗:

def record(str, **kwargs):
    print('str=', str)
    print('kwargs=', kwargs)


record('测试', name='码农飞哥', age=20)
record('测试2')

运行结果是:

str= 测试
kwargs= {'age': 20, 'name': '码农飞哥'}
str= 测试2
kwargs= {}

从上面代码可以看出,可变参数可以不用传入,不传的话则会创建一个空元组或者空字典。

12.6.逆向参数收集

Python不仅有可变参数,将多个参数打包到一个元组或者字典中,还支持逆向参数收集,即直接将列表,元组,字典作为函数参数。不过调用函数时要对实参加上*。就像下面这样:

def param_test(name, age):
    print('name=', name)
    print('age=', age)

data = ['码农飞哥', 18]
param_test(*data)

运行结果是:

name= 码农飞哥
age= 18

12.7. return函数返回值

一个函数可以有返回值,也可以没有返回值,有返回值的语法结构是:

return [返回值]

返回值可以指定,也可以省略不写。如果不写的话就默认为是None,即空值。

12.8. Python函数返回多个值的方法

通常情况下,一个函数只有一个返回值,实际上Python也是如此,
只不过Python函数能以返回列表或元组的方式,将要返回的多个值保存到序列中,从而间接实现返回多个值的目的。

  1. 在函数中,提前将要返回的多个值存储到一个列表或元组中,然后函数返回该列表或元组
  2. 函数直接返回多个值,之间用逗号(,)分隔,Python会自动将多个值封装到一个元组中,其返回值仍是一个元组。下面就举例说明下:
def multi_return():
    return_tuple = ('张三', 12)
    return return_tuple


def multi_return2():
    return '张三', 12


print(multi_return())

result = multi_return2()
print('multi_return2返回值是=,类型是=', result, type(result))

运行结果是

('张三', 12)
multi_return2返回值是=,类型是= ('张三', 12) <class 'tuple'>

12.9 Python函数参数传递机制

Python函数参数传递机制有两种:分别是值传递和引用传递。那么这两种方式有啥区别呢?各自具体的参数传递机制又是啥呢?这个章节就将来解答这两个问题。首先来看看值传递。如下代码定义了一个swap函数,有两个入参a,b。这个函数的工作就是交换入参a,b的值。

def swap(a, b):
    a, b = b, a
    print("形参a=", a, 'b=', b)
    return a, b

a, b = '码农飞哥', '加油'
print("调用函数前实参的a=", a, 'b=', b)
swap(a, b)
print("调用函数后实参的a=", a, 'b=', b)

运行结果是:

调用函数前实参的a= 码农飞哥 b= 加油
形参a= 加油 b= 码农飞哥
调用函数后实参的a= 码农飞哥 b= 加油

可以看出形参被成功的改变了,但是并没有影响到实参。这到底是为啥呢?这其实是由于swap函数中形参a,b的值分别是实参a,b值的副本,也就是说在调用swap之后python会对入参a,b分别copy一份给swap函数的形参。对副本的改变当然不影响原来的数值啦。 语言的描述是空洞的,画个图说明下吧:在Python中一个方法对应一个栈帧,栈是一种后进先出的结构。上面说的过程可以用下面的调用图来表示:
【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】
可以看出当执行a, b = '码农飞哥', '加油' 代码是,Python会在main函数栈中初始化a,b的值。当调用swap函数时,又把main函数中a,b的值分别copy一份传给swap函数栈。当swap函数对a,b的值进行交换时,也就只影响到a,b的副本了,而对a,b本身没影响。
但是对于列表,字典这两的数据类型的话,由于数据是存储在堆中,栈中只存储了引用,所以在修改形参数据时实参会改变。。如下代码演示:

def swap(dw):
    # 下面代码实现dw的a、b两个元素的值交换
    dw['a'], dw['b'] = dw['b'], dw['a']
    print("swap函数里,a =", dw['a'], " b =", dw['b'])


dw = {'a': '码农飞哥', 'b': '加油'}
print("调用函数前外部 dw 字典中,a =", dw['a'], " b =", dw['b'])
swap(dw)
print("调用函数后外部 dw 字典中,a =", dw['a'], " b =", dw['b'])

运行结果是:

调用函数前外部 dw 字典中,a = 码农飞哥  b = 加油
swap函数里,a = 加油  b = 码农飞哥
调用函数后外部 dw 字典中,a = 加油  b = 码农飞哥

可以清晰的看出调用函数之后传入的实参dw的值确实改变了。这说明他是引用传递的,那么引用传递与值传递有啥区别呢?
【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】
从上图可以看出字典的数据是存储在堆中的,在main函数的栈中通过引用来指向字典存储的内存区域,当调用swap函数时,python会将dw的引用复制一份给形参,当然复制的引用指向的是同一个字典存储的内存区域。当通过副本引用来操作字典时,字典的数据当然也改变。综上所述:引用传递本质上也是值传递,只不过这个值是指引用指针本身,而不是引用所指向的值。 为了验证这个结论我们可以稍微改造下上面的代码:

def swap(dw):
    # 下面代码实现dw的a、b两个元素的值交换
    dw['a'], dw['b'] = dw['b'], dw['a']
    print("swap函数里,a =", dw['a'], " b =", dw['b'])
    dw = None
    print("删除形参对字典的引用",dw)

dw = {'a': '码农飞哥', 'b': '加油'}
print("调用函数前外部 dw 字典中,a =", dw['a'], " b =", dw['b'])
swap(dw)
print("调用函数后外部 dw 字典中,a =", dw['a'], " b =", dw['b'])

运行的结果是:

调用函数前外部 dw 字典中,a = 码农飞哥  b = 加油
swap函数里,a = 加油  b = 码农飞哥
删除形参对字典的引用
调用函数后外部 dw 字典中,a = 加油  b = 码农飞哥

删除了形参对字典的引用后,实参还是能获取到字典的值。这就充分说明了传给形参的是实参的引用的副本。
详细内容可以查看:【Python从入门到精通】(十一)Python的函数的方方面面【收藏下来保证有用!!!】
【Python从入门到精通】(十二)Python函数的高级知识点,更深入的吸收知识,不做知识的牙签(不浅尝辄止)【收藏下来保证有用!!!】

13.面向对象的开发

13.1. 面向对象的概念

面向对象(Object-oriented Programming,简称 OOP)的思想其本质上是一种对事物抽象封装的思想。
封装就是将隐藏具体的细节内容,就好像用户使用电脑,只需要简单的操作键盘和鼠标就可以实现一些功能。而无需了解计算机内部是如何实现的。
而在Python语言中的面向对象的封装是将描述特性的数据(属性)和描述行为(方法)封装在一起。
比如现在要描述一个人的话,我们首先会从两方面描述:

  1. 从表面特征描述:例如: 肤色,身高,体重,年龄
  2. 从所具有的行为描述:例如:会吃饭,会走路,会说话。
    如果通过代码来实现的话,其表面特征可以用变量来表示,其行为可以用各种方法来表示。
class Person:
    # 肤色
    colour = '黄色'
    # 身高
    height = '180cm'
    # 体重
    weight = '60KG'
    # 年龄
    age = 18
    
    def eat(self):
        print('吃饭')

    def walk(self):
        print('走路')

    def speak(self):
        print('说话')

zhangsan=Person()
lisi=Person()
wanger=Person()

通过构建一个Person类就可以将人的特性和行为都封装起来。人都是有共性的,也就是说可以通过这个类来创建出各种不同的对象。这里不得不说面向对象里的几个核心概念。

13.2. 类

类可以理解成可以理解成是一个模板,根据这个模板可以创建出许许多多的具体对象,可以把类想象成是一个模子亦或者是一张图纸。

13.3.对象

类并不能直接直接被使用, 通过类创建出来的具体的实例(又称为对象)才能被使用。这就像汽车的图纸与车本身的关系。大多数情况下,图纸并不能被普通人所使用,但是通过图纸创建出的一辆辆汽车能被使用。就像上面通过Person类可以创造出张三,李四,王二麻子。

zhangsan=Person()
lisi=Person()
wanger=Person()

13.4.属性

类中所有的变量都是类的属性,比如上面的身高height,体重weight等属性。

13.5.方法

类中的所有函数都被称为方法,不过,和函数有所不同的是类方法至少要包含一个 self 参数(后续会做详细介绍)。就像上面Person类的eat方法,walk方法。另外,就是类中的方法不能单独使用,必须通过类创建的对象调用。

13.5. 类的定义

类的定义必须要通过class关键字修饰,类名的命名规范建议是每个单词的首字母大写,其余字母小写。

class 类名:
      多个(>=0)类属性
	  多个(>=0)类的方法

就像上面的Person类,其有属性height,weight等几个属性。也有eat,walk等多个方法。

13.6.构造方法

不过这里有个隐藏的方法,那就是类构造方法__init__()。该方法是一个特殊的类实例方法。Python是通过该方法来创建类的实例。该方法的方法名是固定的,开头和结尾各有2个下划线并且中间不能有空格。
在创建类之后,Python会创建一个默认的构造方法__init__()。如果没有手动添加__init__()方法的话则使用默认的构造方法创建类的实例(即对象)。 __init__()方法的语法结构是:

def __init__(self,...):
     代码块

__init__()方法可以有多个参数,但必须包含一个名为self的参数,并且self的参数必须为第一个参数,如果没有其他参数则是默认构造方法。以上面的Person类为例,可以手动添加一个带其他参数的__init__()方法。

    def __init__(self, head):
        self.hand = head

如果手动添加了其他的__init__()方法,则会覆盖掉默认的构造方法。创建对象时会使用你手动添加的__init__()方法。

13.7. 类对象的创建和使用

创建类对象的过程被称为类的实例化,其语法结构是类名(参数), 当使用的是默认构造方法时或者添加的__init__()方法中仅有一个self参数,则创建类对象时参数可以不写。
但是如果__init__()方法中除了self参数还有其他参数的话,则参数必传。不然,就会报类似于下面的错误。

Traceback (most recent call last):
  File "/Python_learn/PycharmProjects/python_demo_1/demo/oop/person.py", line 15, in <module>
    person = Person()
TypeError: __init__() missing 1 required positional argument: 'head'

这里就必须要传入head参数。

person = Person('头')
person.eat()

13.8. self参数的详解

前面几个章节我们多次提到了self参数,那么这个参数到底有啥作用呢?我们知道类是一个模板,通过类这个模板可以创建出许许多多的对象。那么该如何区分这些对象呢?就像每个人都有自己的名字一样,实例化的对象在Python中也有属于自己的代号(自身的引用)。而python正是通过self参数来绑定调用对象本身。说白了就是通过self参数可以知道当前的方法是被谁调用了。专业一点的解释是,当某个对象调用类方法时,该方法会把自身的引用作为第一个参数自动传给该方法。

class Person:
    def __init__(self):
        print('正在执行构造方法')

    def eat(self):
        print(self, '正在吃饭')

zhangsan = Person()
zhangsan.eat()
lisi = Person()
lisi.eat()

其运行结果是:

正在执行构造方法
<__main__.Person object at 0x1031cd0f0> 正在吃饭
正在执行构造方法
<__main__.Person object at 0x103338da0> 正在吃饭

这里实例化了zhangsan和lisi两个对象。当他们同时调用eat方法时,Python会将其自身的引用绑定到self参数上,可以明显的看出不同的调用者其self参数是不同的。这里的self参数就相当于是Java里的this参数。

13.9. 类的变量

类的变量有三种,分别是类变量,实例变量和局部变量。下面直接举个例子吧!下面代码定义了一个Person类,该类定义了了三个变量,两个方法。

class Person:
    name = '张三'

    def __init__(self):
        self.age = 18

    def get_info(self):
        sex = '男'
        print('局部变量sex=', sex)
        return sex

print('类属性name=', Person.name)
person = Person()
print('实例属性age=', person.age)
person.get_info()

运行结果是:

类属性name= 张三
实例属性age= 18
局部变量sex= 男

这里name,age以及sex三个变量分别是三种不同的变量。下面就分别介绍一下
其中定义在了类体中,所有函数之外;此范围定义的变量就称为类变量,就像上面代码中的name变量。类变量属于整个类。可以直接通过类名.变量名来获取,就像上面的Person.name。在实际开发中类变量用的比较少。

定义在类体中,函数内部并且以self.变量名定义的变量就称为实例变量。就像上面代码中的age变量。实例变量只作用于调用方法的对象,只能通过对象名访问,无法通过类名访问。调用方式如上面实例中的person.age

定义在类体中,函数内部的变量以"变量名=变量值"的方式定义的变量就称为局部变量。就像上面代码中的sex变量,局部变量只能在函数内部使用。
详细内容可参考:
【Python从入门到精通】(十三)Python面向对象的开发,没有对象怎么能行呢?
【Python从入门到精通】(十四)Python面向对象的开发2,封装,多继承,多态都了解了么

14. 异常机制

14.1. 异常类之间的继承关系

异常类的个数和种类有很多。但是这些异常类之间不是相互独立的。它们的继承关系如下图所示:
【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】

所有的异常类都继承自基类BaseException类。这个基类BaseException类的父类是object类。基类BaseException类有四个子类。分别是GeneratorExit类,Exception类,SystemExit类,KeyboardInterrupt类。其中Exception类又是实际开发中最常接触到的异常类,程序中可能出现的各种异常,都继承自Exception.它以及它的子类构成了Python异常类结构的基础。其余三个异常类比较少见。
Exception类同样有三个子类,ArithmeticError类用来处理数字异常,BufferError用来处理字符异常。LookupError用来记录关键字异常,

  1. SyntaxError 语法错误,说白了就是编写的代码不符合语法格式
  2. IndentationError:缩进错误,Python是根据缩进来决定代码的作用范围的。
  3. TypeError:类型错误,如果数据类型用错则会报这个错误。
  4. NameError:变量名错误,忘记定义变量就会报这种错误
  5. AttributeError:属性错误,特性引用和赋值失效会引发属性错误
  6. IndexError:索引错误,使用的索引不存在,或索引超出序列范围。
  7. KeyError:关键字错误,使用了映射中不存在的关键字(键)时引发的关键字错误。

14.2. 异常机制的使用

介绍完了各种异常类的继承关系,接着就是介绍异常类的使用了。异常类的作用就是捕获程序运行时的各种异常的,如果不手动捕获异常的话,Python程序在遇到异常之后就会抛出异常并停止程序的运行。捕获异常的语法结构如下:

try:
    可能产生异常的代码块
except [ (Error1, Error2, ... ) [as e] ]:
    处理异常的代码块1
except [ (Error3, Error4, ... ) [as e] ]:
    处理异常的代码块2
except  [Exception]:
    处理其它异常

括号[]括起来的部分可以使用,也可以省略。其中:

  1. (Error1, Error2,…) 、(Error3, Error4,…):其中,Error1、Error2、Error3和Error4都是具体的异常类型。显然,一个except块可以同时处理多种异常。
  2. [as e]:作为可选参数,表示给异常类型起一个别名e,这样做的好处是方便在except块中调用异常类型。

它的执行过程是:

  1. 首先执行try中的代码块,如果执行过程中出现异常,系统会自动生成一个异常类型,并将该异常提交给Python解释器,此过程称为捕获异常。
  2. 当Python解释器收到异常对象时,会寻找能处理该对象的except块,如果找到合适的except块,则把该异常交给该except块处理,这个过程称为处理异常。如果Python解释器找不到处理异常的except块,则程序运行终止,Python解释器也将退出。
    还是举个简单的例子:
try:
    print('try....')
    r = 10 / 0
    print('result', r)
except ZeroDivisionError as e:
    print('ZeroDivisionError', e)
except Exception as e:
    print('Exception', e)
print('END')

运行结果是:

try....
ZeroDivisionError division by zero
END

可以看出当ZeroDivisionError异常能够匹配Python解释器自动生成的异常程序就会进入该except块中。这里需要注意的如果将Exception的except块写到ZeroDivisionError的except块前面的话,则运行结果会变成下面的结果。这是因为
ZeroDivisionError是Exception类的子类。

try....
Exception division by zero
END

14.3. 获取特定异常信息

每种异常类型都提供了如下几个属性和方法,通过调用它们就可以获取当前异常类型的相关信息。

  1. args: 返回异常的错误编号和描述符号
  2. str(e):返回异常信息,但不包括异常信息的类型。
  3. repr(e):返回较全的异常信息,包括异常信息的类型。
try:
    print('try....')
    r = 10 / 0
    print('result', r)
except ZeroDivisionError as e:
    print('', e.args)
    print('', str(e))
    print('', repr(e))

运行结果是:

try....
 ('division by zero',)
 division by zero
 ZeroDivisionError('division by zero')

14.4. finally

finally代码块,无论try块是否发生异常,最终都要进入finally语句中,并执行其中的代码块,在实际开发中可以将资源回收的工作放入finally块中。这样可以保证当不可预知的异常发生时,资源可以被正常回收。这里的资源指的是数据库连接,文件流的关闭等。
还是以上面的代码为例:

try:
    print('try....')
    r = 10 / 0
    print('result', r)
except Exception as e:
    print('Exception', e)
except ZeroDivisionError as e:
    print('ZeroDivisionError', e)
finally:
    print('发生异常时finally语句块执行...')
print('END')


try:
    print('try....')
    r = 10 / 1
    print('result', r)
except Exception as e:
    print('Exception', e)
except ZeroDivisionError as e:
    print('ZeroDivisionError', e)
finally:
    print('没发生异常时finally语句块执行...')
print('END')

运行结果是:

try....
Exception division by zero
发生异常时finally语句块执行...
END
try....
result 10.0
没发生异常时finally语句块执行...
END

可以看出,无论try中是否发生异常,finally块中的代码都会执行。当然finally块只需要搭配try块使用就可以了

14.5. raise用法

有时候我们自定义了一个业务异常类,当触发该异常时将该异常抛给其调用者。又或者当我们捕获到一个未知异常时,需要将该异常封装并抛给其调用者。这时候就可以使用raise关键字了。其语法结构是:

raise [exceptionName [(reason)]]

其有三种用法:

  1. raise: 单独一个raise。该语句引发当前上下文中捕获的异常(比如except块中)或默认引发RuntimeError异常。
  2. raise异常名称:raise后带一个异常类名称,表示引发执行类型的异常。
  3. raise异常类名称(描述信息):在引发指定类型的异常的同时,附带异常的描述信息。
try:
    a = input("输入一个数:")
    # 判断用户输入的是否为数字
    if (not a.isdigit()):
        raise ValueError("a 必须是数字")
except ValueError as e:
    print("引发异常:", repr(e))

当输入一个字母或者汉字时就会抛出ValueError异常。
当然也可以在except中使用raise,将异常重新抛出。
详细内容可参考:
【Python从入门到精通】(十五)Python异常机制,给代码加上安全TAO,不放过一个异常
【Python从入门到精通】(十六)Python异常机制2,正确使用Python异常机制的姿势是啥

15. 模块和包

15. 1.模块的介绍

什么是模块呢?简单理解的话:模块就是一个后缀名是.py的模板文件。模块主要是用于封装函数和变量。我们可以将实现某一个功能的所有函数封装放在同一个.py文件中以作为一个模块提供给其他模块使用。比如操作日期的模块time。其对应模板文件就是time.py。

15.2.导入模块

导入模块的方式有两种:

  1. import 模块名1 [as 别名1], 模块名2 [as 别名2],…
    使用这种格式的import语句,会导入指定模块中所有的成员(包括变量,函数、类等)
  2. from 模块名 import 成员名1 [as 别名1],成员名2 [as 别名2],…
    使用这种格式的import语句,只会导入模块中指定的成员,而不是全部成员。
    用第一种方式导入time模块,可以使用该模块下所有的函数
import time

# 记录当前的毫秒数
start_time = time.time()
print(start_time)
# 睡眠4秒
time.sleep(4)
# 计算耗时
print(time.time() - start_time)

可以看出导入time之后可以使用其模块内的所有成员
用第二种方式导入time模块中的sleep函数,则time函数是使用不了的。

from time import sleep

print(sleep())

PS:如果模块名中出现空格就无法使用import引入模块了,这是因为Python 是以空格来隔离一行语句中的不同元素的。针对有空格的模块名的导入,可以通过__import__函数来导入。比如:现在有一个名为hello test.py的模板文件,可以以__import__("hello test") 这种方式导入。

15.3.自定义模块

自定义模块说白了就是自行创建一个模板文件,然后使用其文件名作为模块名导入到其他模板文件中。定义了一个名为hello的模板文件,然后在hello_test.py文件中导入。
hello.py文件

def say():
    print('苦逼程序员唱着苦逼的歌')
say()
print(__name__)

这里需要注意的是hello.py文件是放在Python项目的根目录下。
hello_test.py文件

import hello
hello.say()

运行hello_test之后,输出的结果是:

苦逼程序员唱着苦逼的歌
hello
苦逼程序员唱着苦逼的歌

我们看到输出了两遍,这显然不是我们想要的结果。我们期望的是say函数只执行一遍,也就是在hello_test调用的地方执行。而不是在hello.py文件中还执行一遍。那么这个问题该如何处理呢?
【Python从入门到精通】(十七)Python模块和包的基本使用,简单一文,一分钟看完

16. 文件的基本操作

16.1. open函数

Python对文件的操作比较方便。没有像Java那样整那么多IO操作类。首先欢迎open函数粉墨登场,该函数主要是用来创建或者打开文件的,其语法格式是:

file=open(filename, mode='r', buffering=None, encoding=None, errors=None, newline=None, closefd=True)

file: 表示要创建的文件对象。
file_name:要创建或打开的文件的文件名称,该名称要用引号括起来,需要注意的是,如果要打开的文件和当前执行的代码文件位于同一目录,则直接写文件名即可。否则此参数需要指定打开文件所在的完整路径。
mode:可选参数,用于指定文件的打开模式,可选的打开模式如下表所示。如果不写,则默认以只读(r)模式打开文件。
buffering:可选参数,用于指定对文件做读写操作时,是否使用缓冲区。如果buffering参数的值为0(或者False)则表示在打开指定文件时不使用缓冲区;如果buffering参数值为大于1的整数,该整数用于缓冲区的大小(单位是字节);如果buffering参数的值为负数,则表示使用默认的缓冲区大小。默认情况下open函数是打开缓冲区的。
encoding: 手动设置打开文件时所使用的编码格式,不同平台的ecoding参数值也不同,以Windows为例,其默认的GBK编码。

表1. open函数支持的文件打开模式

模式 含义 注意事项
r 只读模式打开文件,读文件内容的指针会放在文件的开头 操作的文件必须存在
rb 以二进制格式,采用只读模式打开文件,读文件内容的指针位于文件的开头,一般用于非文本文件,如图片文件、音频文件 操作的文件必须存在
r+ 打开文件后,既可以从头读取文件内容,也可以从开头向文件中写入新的内容,写入的新内容会覆盖文件中等长度的原有内容 操作的文件必须存在
rb+ 以二进制格式、采用读写模式打开文件,读写文件的指针会放在文件的开头,通常针对非文本文件 (如音频文件) 操作的文件必须存在
w 以只写模式打开文件,若该文件存在,打卡时会清空文件中原有的内容 若文件存在,会清空其原有内容(覆盖文件);反之,则创建新文件。
wb 以二进制格式,只写模式打开文件,一般用于非文本文件(如音频文件) 若文件存在,会清空其原有内容(覆盖文件);反之,则创建新文件。
w+ 打开文件后,会对原有内容进行清空,并对该文件有读写权限 若文件存在,会清空其原有内容(覆盖文件);反之,则创建新文件。
wb+ 以二进制格式,读写模式打开文件,一般用于非文本文件
a 以追加模式打开一个文件,对文件只有写入权限,如果文件已经存在,文件指针将放在文件的末尾(即新写入内容会位于已有内容之后);反之,则会创建新文件
ab 以二进制格式打开文件,并采用追加模式,对文件只有写权限,如果该文件已存在,文件指针位于文件末尾(新写入文件会位于已有内容之后);反之,则创建新文件
a+ 以读写模式打开文件,如果文件存在,文件指针放在文件的末尾(新写入文件会位于已有内容之后);反之,则创建新文件
ab+ 以二进制模式打开文件,并采用追加模式,对文件具有读写权限,如果文件存在,则文件指针位于文件的末尾(新写入文件会位于已有内容之后);反之,则创建新文件

从上表我们可以得出如下结论:

  1. 模式中带b的就是以二进制格式打开文件,比如模式 rb;wb;ab
  2. 模式中带+的就是以读写模式打开文件,说白了就是既可以读文件也可以写文件,比如模式 r+;w+;a+;rb+;wb+;ab+
  3. 不带b,或者+的模式,比如r模式就是只读,w模式就是只写,a模式就是只追加文件。

16.2.文件的读取

文件的读取有三种方法:

  1. file.read(size)方法,逐个字节或者字符读取文件中的内容。file表示已打开的文件对象,size作为一个可选参数,用于指定一次最多可读取的字符(字节)个数。如果省略,则默认一次性读取所有内容。
  2. file.readline(size)方法:逐行读取文件中的内容。file表示已打开的文件对象,size为可选参数,用于指定读取每一行时,一次最多读取的字符(字节)数,可以通过循环的方式来读取文件中的全部内容
  3. file.readlines() 方法:一次性读取文件中多行内容,读取的结果以列表的形式返回。

举个例子吧!现创建一个名为my_file.txt的文件,文件中有如下内容:

全网同名:码农飞哥
这是Python系列的第十八篇文章
https://feige.blog.csdn.net/

下面分别用介绍的三种方法来读取该文件中的所有内容:

# 读取文本文件
print("*********read方法读取全部内容**********")
f = open('my_file.txt', encoding='utf-8')
print(f.read())
f.close()

print("*********readline方法读取全部内容**********")
f = open('my_file.txt', encoding='utf-8')
line_txt = f.readline()
while line_txt:
    print(line_txt)
    line_txt = f.readline()
f.close()

print("*********readline方法读取全部内容**********")
f = open('my_file.txt', encoding='utf-8')
print(f.readlines())

运行的结果是:

*********read方法读取全部内容**********
全网同名:码农飞哥
这是Python系列的第十八篇文章
https://feige.blog.csdn.net/
*********readline方法读取全部内容**********
全网同名:码农飞哥

这是Python系列的第十八篇文章

https://feige.blog.csdn.net/
*********readline方法读取全部内容**********
['全网同名:码农飞哥n', '这是Python系列的第十八篇文章n', 'https://feige.blog.csdn.net/']

一般而言,readline函数和readlines函数用于读取文本文件,而read函数则即可读取文本文件又可以读取非文本文件。

16.3.文件写入

现有文件write_file.txt,文件中有如下两行内容。

码农飞哥
这是Python系列的第十九篇文章

  1. file.write(str): 其中,file表示已经打开的文件对象;str表示要写入文件的字符串(或字节串,仅适用写入二进制文件中)
    需要注意的是在使用write()向文件中写入数据,需保证使用open()函数是以r+、w、w+、a或者a+模式打开文件,否则执行write()函数会抛出io.UnsupportedOperation 错误。
file = open('write_file.txt', mode='w', encoding='utf-8')
file.write('日拱一卒')
file.close()

通过w模式打开执行write方法写入日拱一卒之后,文件中原有的内容就被清空了。这里需要注意的这里要指定编码为utf-8,因为在window下文件的默认open函数指定的默认编码格式是gbk。用utf-8格式打开文件可能会出现乱码
【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】

  1. file.writelines(str):
    其中,file表示已经打开的文件对象,str表示要写入文件的内容,可以支持多行数据的写入。
file = open('write_file.txt', mode='w', encoding='utf-8')
file2 = open("my_file.txt", encoding='utf-8')
file.writelines(file2.read())
file.close()

这里读取的文件的编码和写入的文件编码需要保持一致。不然,就会报UnicodeDecodeError编码错误。写入之后会清空文件的原有内容。

16.4.各种模式的介绍

只读模式(只读数据r)

创建一个名为my_file.txt的文件,文件有如下内容:

全网同名:码农飞哥
这是Python系列的第十八篇文章
https://feige.blog.csdn.net/
  1. 通过二进制的方式(rb)打开图片,针对二进制文件(图片,视频等)的话只能使用rb方式来读取文件。
f = open('my_file.txt', 'rb')
f_bytes = f.read()
print(f_bytes)
print(f_bytes.decode('utf-8'))
f.close()

运行结果是:

b'xe5x85xa8xe7xbdx91xe5x90x8cxe5x90x8dxefxbcx9axe7xa0x81xe5x86x9cxe9xa3x9exe5x93xa5nxe8xbfx99xe6x98xafPythonxe7xb3xbbxe5x88x97xe7x9ax84xe7xacxacxe5x8dx81xe5x85xabxe7xafx87xe6x96x87xe7xabxa0nhttps://feige.blog.csdn.net/'
全网同名:码农飞哥
这是Python系列的第十八篇文章
https://feige.blog.csdn.net/
*********readline方法读取全部内容**********
全网同名:码农飞哥
这是Python系列的第十八篇文章

只写模式(重写数据w)

  1. 以二进制的方式写入数据
    如下代码就是将图片demo.jpg的内容写入到图片demo1.jpg中,如果demo1.jpg文件不存在则会先创建一个文件。
write_file = open('demo1.jpg', mode='wb')
read_file = open('demo.jpg', mode='rb')
write_file.write(read_file.read())
read_file.close()
write_file.close()
  1. 只写模式追加数据
    如下就是在append_file.txt文件中追加好好加油数据,a的话是只写模式
append_file = open('append_file.txt', mode='a', encoding='utf-8')
append_file.write("n好好加油")
append_file.close()

读写模式(追加数据a)

前面介绍的以w开头的模式,如果被写入文件中原有数据则会被覆盖。如果不想原有数据被覆盖可以使用追加写入数据的方式。
读写模式下可以调用read()方法进行读,也可以调用write方法进行写。

read_write_file = open('append_file.txt', mode='r+', encoding='utf-8')
print(read_write_file.read())
read_write_file.write('n努力向上,天天向上')
print(read_write_file.read())
read_write_file.close()

运行结果是:

全网同名:码农飞哥
这是Python系列的第十八篇文章
https://feige.blog.csdn.net/

【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】

需要注意的是read方法只能调用一遍。read函数只能读取写入之前的数据,如果要读取写入之后的数据需要重新调用open函数。

16.5. with as用法详解

我们注意文件操作完之后需要手动调用close()方法关闭文件流。实际开发中建议将调用close函数的代码块放在finally块中以防止出现异常导致文件流不能被关闭。标准的使用方式是:

f = open('my_file.txt', encoding='utf-8')
try:
    print(f.read())
finally:
    f.close()

那么,有没有更加便捷的方式呢?答案是有的:那就是通过with as 语句来操作文件。其语法格式是:

with 表达式 [as target]:
    代码块

此格式中,用[]括起来的部分可以使用,也可以省略。其中,target参数用于指定一个变量,该语句会将表达式指定的结果保存到该变量中。with as 语句中的代码块如果不想执行任何语句,可以直接使用 pass 语句代替。
用with as改造上面的代码就是:

with open('my_file.txt', encoding='utf-8') as f:
    print(f.read())

不用手动关闭文件流。
【Python从入门到精通】(十八)Python的文件夹操作,创建文件夹复制文件等等
【Python从入门到精通】(十九)Python对文件的读写操作一览表,非常实用,非常简单

17. 线程

17.1. 进程和线程

进程是什么?进程说白了就是应用程序的执行实例。你在电脑上听着歌儿,敲着代码,挂着微信。这些任务都是由不同的应用程序来执行。操作系统会给每个应用程序分配不同的进程。通过CPU的调度来使这些动作可以“同时”进行,这里说是同时进行实际上不是的,因为CPU在同一时间内只能有一条指令执行,但是因为CPU执行的速度太快了,给用户的感觉就是在同时进行。进程是可以占用物理内存的。

线程是进程的组成部分,一个进程可以拥有多个线程.CPU调度进程的最小粒度是线程。其中由主线程来完成从开始到结束的全部操作。其他线程在主线程运行时被创建或者结束。
主线程在程序初始化之后就会被创建。如果一个程序只有一个主线程就称为单线程,如果有多个线程则称之为多线程。 创建多个线程之后,每个线程的执行都是独立的。

17.2.线程的创建

Python创建线程的方式有两种: 创建线程都需要引入threading模块。首先让我们来看看Thread类的构造方法。

__init__(self, group=None, target=None, name=None,
                 args=(), kwargs=None, *, daemon=None):

此构造方法中,以上所有参数都是可选参数,即可以使用,也可以忽略。
其中各个参数的含义如下:

  1. group:指定所创建的线程隶属于哪个线程组。
  2. target:指定所创建的线程要调用的目标方法。
  3. args:以元组的方式,为target指定的方法传递参数,如果传入的是元组中有多个参数的话则传入方式是(arg1,arg2,....argn,)
  4. kwargs:以字典的方法,为target指定的方法传递参数。
  5. daemon:指定所创建的线程是否为后台线程。
  6. name: 指定线程的名称
  7. 第一种方式:就是直接调用Thread类的构造方法创建一个线程的实例。
import threading

# 定义线程要调用的方法
def async_fun(*add):
    for arc in add:
        print(threading.current_thread().getName() + " " + arc)


my_tuple = ("码农飞哥", "好好学习", "早日突破职业瓶颈")

# 创建线程
thread = threading.Thread(target=async_fun, args=my_tuple)
# 启动线程
thread.start()

for i in range(4):
    print(threading.current_thread().getName() + "执行" + str(i)+ "次")

运行结果是:

Thread-1 码农飞哥MainThread执行0次
MainThread执行1次

MainThread执行2次
MainThread执行3次
Thread-1 好好学习
Thread-1 早日突破职业瓶颈

如上方法就是实例化一个线程Thread-1让他异步调用async_fun方法。可以看出主线程MainThread和线程Thread-1是交替调用的(每次的执行结果都不同)。这说明了这两个线程是交替获得CPU的执行权限的。需要特别注意的是线程必须要调用start()方法才能执行。
2. 第二种方式:就是继承threading.Thread。然后,重写run方法。

import threading

class MyThread(threading.Thread):
    def __init__(self, add):
        threading.Thread.__init__(self)
        self.add = add
        # 重写run()方法

    def run(self):
        for arc in self.add:
            print(threading.current_thread().getName() + " " + arc)


my_tuple = ("码农飞哥", "好好学习", "早日突破职业瓶颈")

thread = MyThread(my_tuple)
thread.start()

for i in range(4):
    print(threading.current_thread().getName() + "执行" + str(i)+ "次")

运行结果是:

Thread-1 码农飞哥MainThread执行0次
MainThread执行1次

Thread-1 好好学习
Thread-1 早日突破职业瓶颈
MainThread执行2次
MainThread执行3次

这里定义了了MyThread类,并重写了run方法。run方法就是线程真正要执行的任务。相当于上例中async_fun函数的内容移到了run方法中。

17.3.线程的生命周期

说完了如何创建一个线程接下来让我们来看看线程的生命周期。一个线程一共有五个状态。
分别是新建状态(初始化状态),就绪状态,运行状态,阻塞状态以及死亡状态。状态之间的关系图如下:
【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】

  1. 新建状态:线程被刚刚创建,且未调用start()方法时的状态。即上面的threading.Thread(target=async_fun, args=my_tuple) 时的线程状态。
  2. 就绪状态:调用了start()方法之后,线程就由新建状态转成就绪状态,就绪状态就是表示线程可以是随时准备获取CPU的状态。
    以下几种情况下线程会进入就绪状态:1.sleep()方法规定的时间已过。2.调用了notify()方法或者notify_all()方法发出通知,3.其他线程释放了该同步锁,并由该线程获得。
  3. 运行状态:当就绪状态的线程获得了CPU的使用权之后,并开始执行target参数执行的目标函数或者run()方法,就表明线程处于运行状态。
  4. 阻塞状态:获得CPU的调度但是没有执行完任务的线程.
    以下几种情况下:线程都会进入阻塞状态:1.调用了sleep()方法。2.调用了wait()方法,并且等待条件满足。3.线程试图获取某个对象的同步锁时,如果该锁被其他线程占用,则当前线程进入阻塞状态。
  5. 线程死亡状态:线程的任务执行完成或者程序执行过程中发生异常,线程都会进入死亡状态。

17.4.Thread.join()用法详解。

join()方法的功能是在程序指定位置,优先让该方法的调用者使用CPU资源,即调用线程等待该线程完成后,才能继续用下运行。
该方法的语法格式如下:thread.join( [timeout])
其中,thread为Thread类或其子类的实例化对象;timeout参数作为可选参数,
其功能是指定thread线程最多可以霸占CPU资源的时间,如果省略,则默认直到thread执行结束才释放CPU资源

import threading


# 定义线程要调用的方法
def async_fun(*add):
    for arc in add:
        print(threading.current_thread().getName() + " " + arc)


my_tuple = ("码农飞哥", "好好学习", "早日突破职业瓶颈")

# 创建线程
thread = threading.Thread(target=async_fun, args=my_tuple, name="线程1")
thread2 = threading.Thread(target=async_fun, args=my_tuple, name='线程2')
# 启动线程
thread.start()
# 等待线程1执行完
thread.join()
# 线程1执行完之后启动线程2
thread2.start()
thread2.join()

运行结果是:

线程1 码农飞哥
线程1 好好学习
线程1 早日突破职业瓶颈
线程2 码农飞哥
线程2 好好学习
线程2 早日突破职业瓶颈

如上有线程1和线程2两个线程。在线程1调用thread.join()之后线程1和线程2的执行由并行改成了串行。也就是说必须是线程1执行完之后才启动线程2。现在把该语句去掉变成下面这样:

# 启动线程
thread.start()
# 线程1执行完之后启动线程2
thread2.start()
thread2.join()

运行结果是:

线程1 码农飞哥
线程2 码农飞哥
线程2 好好学习
线程2 早日突破职业瓶颈
线程1 好好学习
线程1 早日突破职业瓶颈

可以看出线程1和线程2的运行是并行的。

17.5.sleep函数的用法

位于time模块中的sleep(secs)函数,可以实现令当前执行的线程暂停secs秒后在继续执行,所谓暂停,即令当前线程进入阻塞状态,当达到sleep()函数规定的时间后,
再由阻塞状态转为就绪状态,等待CPU调度。

# 定义线程要调用的方法
def async_fun(*add):
    for arc in add:
        start_time = time.time()
        time.sleep(2)
        print(str((time.time() - start_time)) + " 秒 " + threading.current_thread().getName() + " 结束调用" + arc)


my_tuple = ("码农飞哥", "好好学习", "早日突破职业瓶颈")

# 创建线程
thread = threading.Thread(target=async_fun, args=my_tuple)
# 启动线程
thread.start()

运行结果是:

2.0052337646484375 秒 Thread-1 结束调用码农飞哥
2.004210948944092 秒 Thread-1 结束调用好好学习
2.002394199371338 秒 Thread-1 结束调用早日突破职业瓶颈

可以看出线程每次执行都花费了2秒的时间。
【Python从入门到精通】(二十)Python并发编程的基本概念-线程的使用以及生命周期

18. 线程池

18.1. 为什么要使用线程池呢?

前面几篇文章介绍的线程都是直接通过代码手动创建的线程。线程执行完任务之后就会被系统销毁,下次再执行任务的时候再进行创建。这种方式在逻辑上没有啥问题。但是系统启动一个新线程的成本是比较高,因为其中涉及与操作系统的交互,操作系统需要给新线程分配资源。打个比方吧!就像软件公司招聘员工干活一样。当有活干时,就招聘一个外包人员干活。当活干完之后就把这个人员辞退掉。你说在这过程中所耗费的时间成本和沟通成本是不是很大。那么公司一般的做法是:当项目立项时就确定需要几名开发人员,然后将这些人员配齐。然后这些人员就常驻在项目组,有活就干,没活就摸鱼。线程池也是同样的道理。线程池可以定义最大线程数,这些线程有任务就执行任务,没任务就进入线程池中歇着。

18.2. 线程池怎么用呢?

线程池的基类是concurrent.futures模块中的Executor类,而Executor类提供了两个子类,即ThreadPoolExecutor类和ProcessPoolExecutor类。其中ThreadPoolExecutor用于创建线程池,而ProcessPoolExecutor用于创建进程池。本文将重点介绍ThreadPoolExecutor类的使用。首先,让我们来看看ThreadPoolExecutor类的构造函数。这里使用的Python版本是:3.6.7。

      def __init__(self, max_workers=None, thread_name_prefix=''):
        """Initializes a new ThreadPoolExecutor instance.

        Args:
            max_workers: The maximum number of threads that can be used to
                execute the given calls.
            thread_name_prefix: An optional name prefix to give our threads.
        """
        if max_workers is None:
            # Use this number because ThreadPoolExecutor is often
            # used to overlap I/O instead of CPU work.
            max_workers = (os.cpu_count() or 1) * 5
        if max_workers <= 0:
            raise ValueError("max_workers must be greater than 0")

        self._max_workers = max_workers
        self._work_queue = queue.Queue()
        self._threads = set()
        self._shutdown = False
        self._shutdown_lock = threading.Lock()
        self._thread_name_prefix = (thread_name_prefix or
                                    ("ThreadPoolExecutor-%d" % self._counter()))

他的构造函数只有两个参数:一个是max_workers参数,用于指定线程池的最大线程数,如果不指定的话则默认是CPU核数的5倍。另一个参数是thread_name_prefix,它用来指定线程池中线程的名称前缀。其他参数:

  1. _shutdown初始值值为False,默认情况下线程池不销毁,即线程池的生命周期跟项目的生命周期一致。
  2. self._work_queue = queue.Queue()生成缓冲队列。
  3. _threads没有任务被提交时,线程的数量设置为0。
  4. _shutdown_lock 指定线程池的锁是Lock锁。
    说完了线程池的创建之后,接着来看看线程池中比较常用的几个方法吧。
  5. submit(self, fn, *args, **kwargs)
    该方法用提交任务,即将fn函数提交给线程池,*args代表传给fn函数的参数,**kwargs代表以关键字参数的形式为fn函数传入参数。
  6. shutdown(self, wait=True)
    关闭线程池
  7. map(func, *iterables, timeout=None, chunksize=1)
    该函数类似于全局函数map(func,*iterables),只是该函数将会启动多个线程,以异步方式立即对iterables执行map处理。

程序将task函数通过submit方法提交给线程池之后,线程池会返回一个Future对象,该对象的作用主要是用于获取线程任务函数的返回值。Future提供了如下几个方法。

  1. cancel():取消该Future代表的线程任务。如果该任务正在执行,不可取消,则该方法返回False;否则,程序会取消该任务,并返回True。
  2. result(timeout=None):获取该 Future 代表的线程任务最后返回的结果。如果 Future 代表的线程任务还未完成,该方法将会阻塞当前线程,其中 timeout 参数指定最多阻塞多少秒。
  3. add_done_callback(fn):为该 Future 代表的线程任务注册一个“回调函数”,当该任务成功完成时,程序会自动触发该 fn 函数。
  4. done():如果该Future代表的线程任务被成功取消或执行完成,则该方法返回True。

18.3.来个简单的例子

该例中创建了一个最大线程数是2的线程池来执行async_add函数。

from concurrent.futures import ThreadPoolExecutor
import threading
import time


def async_add(max):
    sum = 0
    for i in range(max):
        sum = sum + i
    time.sleep(1)
    print(threading.current_thread().name + "执行求和操作求得的和是=" + str(sum))
    return sum


# 创建两个线程
pool = ThreadPoolExecutor(max_workers=2, thread_name_prefix='测试线程')
# 向线程池提交一个task,20作为async_add()函数的参数
future1 = pool.submit(async_add, 20)
# 向线程池再提交一个task
future2 = pool.submit(async_add, 50)
# 判断future1代表的任务是否执行完
time.sleep(2)
print(future1.done())
print(future2.done())
# 查看future1代表的任务返回的结果
print('线程一的执行结果是=' + str(future1.result()))
# 查看future2代表的任务的返回结果
print('线程二的执行结果是=' + str(future2.result()))
print("----" + threading.current_thread().name + "----主线程执行结束-----")

运行结果是:

测试线程_0执行求和操作求得的和是=190
测试线程_1执行求和操作求得的和是=1225
True
True
线程一的执行结果是=190
线程二的执行结果是=1225
----MainThread----主线程执行结束-----

本例中定义了一个最大线程数是2的线程池,并向线程池中提交了两个任务,其中async_add函数就是要执行的任务。在async_add函数中添加 time.sleep(1) 休眠一秒是为了验证done()方法返回的结果。最后才打印主线程执行结束表明result()方法是阻塞的。如果将result()屏蔽掉。
改成如下形式:

# 创建两个线程
pool = ThreadPoolExecutor(max_workers=2, thread_name_prefix='测试线程')
# 向线程池提交一个task,20作为async_add()函数的参数
future1 = pool.submit(async_add, 20)
# 向线程池再提交一个task
future2 = pool.submit(async_add, 50)
# 判断future1代表的任务是否执行完
print(future1.done())
print(future2.done())
print("----" + threading.current_thread().name + "----主线程执行结束-----")

则运行结果是:

False
False
----MainThread----主线程执行结束-----
测试线程_0执行求和操作求得的和是=190
测试线程_1执行求和操作求得的和是=1225

详情可以查看
【Python从入门到精通】(二十二)Python线程池的正确使用姿势

19.Pil库

PIL库 Python Imaging Library,已经是Python平台事实上的图像处理标准库了。PIL功能非常强大,但API却非常简单易用。但是PIL库仅仅支持到Python 2.7。为了兼容Python 3.x开源社区提供了兼容版本Pillow,通过Pillow大家就可以愉快的在Python 3.x上使用PIL库了。

19.1.安装Pillow 以及版本兼容

通过pip命令安装Pillow还是非常方便的,一行命令就可以
安装最新版本的命令

pip install  Pillow

安装指定版面的命令pip install Pillow=={version} 这里的version需要替换成指定的版本号,比如要下载8.3.0版本。

pip install Pillow==8.3.0

如果你不知道有哪些版本可以通过pip install Pillow== 进行查看。
下表是Pillow与Python的版本对应表。
【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】

19.2. 常用模块介绍

PIL库有很多模块,这里重点介绍一些常用的模块。首先,总体来看下各个模块的作用。

模块名 主要作用
Image Image模块提供了一个具有相同名称的类用于表示PIL的image对象,它有许多工厂类,包括从文件中加载image以及创建新的image
ImageColor ImageColor 模块包含了CSS3中的颜色说明符到RGB元组的颜色表和转换器,这个模块在PIL.Image.new()和ImageDraw模块以及其他模块使用
ImageFont ImageFont 用于设置字体,它主要用在PIL.ImageDraw.ImageDraw.text() 方法中。
ImageDraw ImageDraw 模块为Image模块提供了简单的2D图形,利用该模块可以创建新图形,修饰现有图形,然后生成新的图形以供使用

下面就详细介绍下各个模块的

19.3. Image

Image是PIL库的核心模块,大部分图片的操作都离不开它,利用它可以从文件中加载image,以及创建新的image。以及将Images保存为图片。

  1. 加载图片文件
PIL.Image.open(fp, mode='r', formats=None)

其中 fp是图片文件名称,mode 表示图片路径的读取模式,默认是’r’模块。返回Image对象

try:
    img = Image.open("img1.jpeg")
finally:
    # 这种打开方式需要手动关闭文件流
    img.close()

这里Image使用完成之后需要手动关闭。比较推荐下面的with … as … 的写法

with Image.open('img1.jpeg') as img:

下面列举的img都是前面通过open获取到的Image对象。
2. 获取图片的宽,高。通过img.size 可以获取图片的宽,高。i

    width, height = img.size
  1. 展示图片show()
img.show()
  1. 图片旋转,通过rotate方法对图片进行旋转,下面就是将图片旋转45度展示出来。
   img.rotate(45).show()
  1. 图片缩放,通过thumbnail方法可以实现对图片的缩放。
    img.thumbnail((width / 2, height / 2))
  1. 保存图片
save(self, fp, format=None, **params)

该方法可以将Image对象保存为一个图片文件。其中:fp为图片保存的路径。**params是可变参数,一般是传入图片的后缀名。

    img.save('thumbnail.jpeg')
  1. 创建新Image
PIL.Image.new(mode, size, color=0)

该方法有三个参数,mode用于指定生成的图片是RGB还是RGBA。这里RGBA各个字母表示的意思是:r 表示red, g 表示gree, b表示blue,a 表示alpha 透明度。一般而言只需要指定RGB即可。如果需要创建一个透明底的图片则需要传入RGBA。
size 用于指定图片的宽高,传入的是一个元组。
color 用于指定图片的颜色,如果前面mode传入的是RGB的话,则该参数需要传入含有三个元素的元组。比如:(255, 0, 0),如果前面mode传入的是RGBA的话,则该参数需要传入含有四个元素的元素,比如:(255,0,0,204)。
下面的代码就是创建一个宽高各为500的,背景色是红色的图片。

 newImg = Image.new('RGB', (500, 500), (255, 0, 0))
 newImg.save('newImg.png')
  1. 复制图片,通过copy()方法,可以copy一个图片。
  # 复制图片
    copyImg = img.copy()
    newImg.save(op.join(base_path, 'copyImg.png'))
  1. 粘贴图片:通过 paste方法可以将一个图片粘贴到另一个图片之上。

ImageColor

该模块主要是从CSS3中的颜色说明符中获取到RGB值。这里说一个方法:getrgb 方法就是获取RGB值。

# 获取颜色的RBGA值
rgb_tup = ImageColor.getrgb("#ff0000cc")
print(rgb_tup)

运行结果是(255, 0, 0, 204)

19.4. ImageFont

ImageFont 用于设置字体,它主要用在PIL.ImageDraw.ImageDraw.text() 方法中。首先,这里介绍其最常用的方法

PIL.ImageFont.truetype (font = None , size = 10 , index = 0 , encoding = '' , layout_engine = None )

从文件或类文件对象加载 TrueType 或 OpenType 字体,并创建字体对象。该函数从给定的文件或类文件对象加载一个字体对象,并为给定大小的字体创建一个字体对象。
Pillow 使用 FreeType 打开字体文件。如果您在 Windows 上同时打开多种字体,请注意 Windows 将可以在 C 中同时打开的文件数限制为 512。如果接近该限制,OSError可能会抛出an ,报告 FreeType“无法打开资源”。
此功能需要 _imagingft 服务。
参数
font – 包含 TrueType 字体的文件名或类似文件的对象。如果在此文件名中找不到该文件,加载程序也可能会在其他目录中进行搜索,例如fonts/ Windows 或 上的目录/Library/Fonts/, /System/Library/Fonts/以及~/Library/Fonts/macOS上的目录。
size – 请求的大小,以磅为单位。
index – 要加载的字体(默认是第一个可用的字体)。
encoding—要使用的字体编码(默认为 Unicode)。可能的编码包括(有关更多信息,请参阅 FreeType 文档):
这指定要使用的字符集。它不会改变后续操作中提供的任何文本的编码。
layout_engine 要使用的布局引擎(如果可用): ImageFont.LAYOUT_BASIC或ImageFont.LAYOUT_RAQM.
返回值
一个字体对象。

img_font = ImageFont.truetype('simsun.ttf', size=20)

这里代码的意思是创建一个字体大小为20的宋体的字体。

19.5. ImageDraw

终于说到ImageDraw模块了,这个模块也是一个非常重要的模块,它主要是可以给图片添加文字以及划线等。

  1. Draw方法
PIL.ImageDraw.Draw(im, mode=None)

给指定的Image对象创建一个draw对象。
参数:
im: 需要被绘画的image对象
mode: 用于颜色值的可选模式,对于RGB图像,此参数可以是RGB或者RGBA(将绘图混合到图像中)。对于所有其他模式,此参数必须与图像模式相同,如果省略,模式默认是图像的模式。
2. text方法

ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=4, align='left', direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False)

在给定的位置上添加文本
参数:
xy – 文本的锚点坐标。
text – 要绘制的字符串。如果它包含任何换行符,则文本将传递给 multiline_text()。
fill- 用于文本的颜色。
font- 一个ImageFont实例。
anchor—— 文本锚对齐方式。确定锚点与文本的相对位置。默认对齐方式是左上角。有关有效值,请参阅文本锚点。对于非 TrueType 字体,将忽略此参数。
此参数存在于 Pillow 的早期版本中,但仅在 8.0.0 版中实现。
spacing– 如果文本传递到multiline_text(),则为 行之间的像素数。
align- 如果文本被传递到 multiline_text(), “left”,“center"或"right”。确定线条的相对对齐方式。使用anchor参数指定对齐到xy。
direction——文本的方向。它可以是"rtl"(从右到左)、“ltr”(从左到右)或"ttb"(从上到下)。需要 libraqm。
features—— 要在文本布局期间使用的 OpenType 字体功能列表。这通常用于打开默认情况下未启用的可选字体功能,例如"dlig"或"ss01",但也可用于关闭默认字体功能,例如"-liga"禁用连字或"-kern" 禁用字距调整。要获取所有支持的功能,请参阅OpenType 文档。需要 libraqm。
language—— 文本的语言。不同的语言可能使用不同的字形形状或连字。此参数告诉字体文本使用的语言,并根据需要应用正确的替换(如果可用)。它应该是BCP 47 语言代码。需要 libraqm。
stroke_width–文本笔划的宽度。
stroke_fill – 用于文本笔划的颜色。如果没有给出,将默认为fill参数。
embedded_color– 是否使用字体嵌入颜色字形(COLR、CBDT、SBIX)。8.0.0 版中的新功能。

# 给图片上添加文字
with Image.open(op.join(base_path, 'img4.jpeg')) as im:
    font = ImageFont.truetype(op.join(base_path, 'simsun.ttf'), size=80)
    rgb_tup = ImageColor.getrgb("#ff0000cc")
    draw = ImageDraw.Draw(im)
    text = "玛莎拉蒂"
    draw.text((650, 550), text, fill=rgb_tup, font=font)
    im.save(op.join(base_path, '玛莎拉蒂.png'), 'png')

运行结果是:
【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】

19.6. 合并美女照片

现在有这两张美女照片分别是:img2.jpeg和img3.png。我想把img3.png粘贴到img2.jpeg上。该如何操作呢? 其中img3.png还是透明底的。
【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】

  1. 直接上paste方法
# 将两张图贴起来
img2 = Image.open('img2.jpeg')
img3 = Image.open('img3.png')
img2.paste(img3)
img2.save('beautiful_paste.jpeg')

运行结果是:
【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】
img3.png 图片粘贴到img2上之后背景色变成了黑色,这显然没有达到我们期望的结果。这该如何处理呢?
问题不大,只需要小小的修改一下代码.
3. 小小修改一下,将背景改成透明底

# 透明底
img2 = Image.open('img2.jpeg').convert('RGBA')
img3 = Image.open('img3.png').convert('RGBA')
# 获取r,g,b,a的值
r, g, b, a = img3.split()
# 传入透明值
img2.paste(img3, box=(0, 0), mask=a)
img2.save('beautiful_paste2.png')

运行结果是:
【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】
这下就变成了透明底了。两位美女都可以尽情欣赏了。
详细内容可以查看
❤️【Python从入门到精通】(二十六)用Python的PIL库(Pillow)处理图像真的得心应手❤️
❤️【Python从入门到精通】(二十七)更进一步的了解Pillow吧!

总结

至此Python的基础内容已经全部介绍完了。
干货太多,编辑器都有点卡顿了。
还是那句话,收藏下来迈出了学习的第一步。
B站的小姐姐看一千遍还是别人的,C站的文章看一遍就是自己的了。
干货握在手,妹子跟你走。

Python知识图谱

为了更好帮助更多的小伙伴对Python从入门到精通,我从CSDN官方那边搞来了一套 《Python全栈知识图谱》,尺寸 870mm x 560mm,展开后有一张办公桌大小,也可以折叠成一本书的尺寸,有兴趣的小伙伴可以了解一下——扫描下图中的二维码即可购买。

【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】
我本人也已经用上了,感觉非常好用。图谱桌上放,知识心中留。
【Python从入门到精通】五万六千字对Python基础知识做一个了结吧!(二十八)【值得收藏】

我是码农飞哥,再次感谢您读完本文
需要源码的小伙伴关注下方公众号,回复【python】


喜欢 (0)