在现实世界中,人们常常遇到一些逻辑判断,这些判断会产生是或否的结果,我们往往会根据不同的结果,对事情采取不同的处理措施。在自然语言中(以汉语为例),人们经常使用 “如果”、“否则” 等关键词来表示对不同判断结果的处理态度。而编程语言的设计正是从解决人类问题的角度出发的,在较早出现的汇编语言中,判断语句已经被广泛使用,直到现在,所有热门的编程语言中都保留了判断语句。
判断在数据处理方面也是必要的技术,经常与循环语句一起使用。举个例子,我们需要在全国 36 万多个家庭农场(统计截至2021年底,数据来自浙大卡特-企研中国涉农研究数据库,简称“CCAD”)中筛选出从事水果种植或销售的家庭农场。(以下是数据样例)
初步筛选的思路是基于“其经营范围中是否含有与水果有关的词语(“水果”、“果蔬”、“梨”……)”,如果与水果相关的词语出现在经营范围中,那么该家庭农场就非常有可能是我们想要的,否则就不是。对所有的家庭农场做这样的判断之后,就能初步得到我们想要的结果。
以上是一个简单的应用场景,接下来我们介绍判断语句的基础知识。
Python 中的三大控制结构
图灵奖获得者,计算机先驱之一,荷兰计算机科学家 Edsger Wybe Dijkstra
于1965年指出,任何算法都可以用顺序、分支和循环三种结构组合嵌套而成。这个理念对计算机编程语言的发展起到了重要作用,直到今天,我们所要学习的 Python 三大控制结构也正是这三种,即顺序结构、分支结构和循环结构。
1顺序结构
顺序结构是 Python 语言的默认结构,也是最容易被人们理解的一种控制结构。以顺序结构编写的程序严格按照程序语句的先后顺序依次执行,每一行代码只执行一次,如下图所示。
图1 程序顺序结构示意图
2分支结构
通俗来讲,分支结构就是 Python 中的判断语句。对于要先做判断再选择处理方式的问题就要使用分支结构。分支结构的执行是依据一定的条件选择执行路径,而不是严格按照语句出现的前后顺序。
根据判断条件和处理方式的不同,分支结构又分为单分支、二分支和多分支结构三种,这些分支结构足以处理各种各样的判断问题。下面是这三种分支结构的程序示意图。
单分支结构示意图如下图所示。图中的 “是”、“否” 表示判断条件是否成立,下同。单分支结构的特点是,当判断条件成立,则执行隶属于该判断语句成立情况下的语句块1
;如果判断条件不成立,那么程序将不会做任何事情,转而执行后续(单分支之外)的代码。
图2 单分支结构示意图
二分支结构示意图如下图所示。二分支结构的目的非常清晰,即根据判断条件是否成立,给出了两种不同的处理方式,分别对应着图中的 语句块1
和 语句块2
。与单分支结构相比,唯一的区别就是当判断条件不成立时,二分支结构将会执行另一路径的 语句块2
,而不是直接跳过去执行分支结构之外的代码。
图3 二分支结构示意图
多分支结构示意图如下图所示。多分支结构常用于更多种情况的判断,当使用一个判断条件,两种处理方式不足以解决问题时就可以用到多分支结构。在此分支结构中我们可以使用多个判断条件,并根据每个条件的成立情况执行不同的语句块或者转向下一个判断条件。需要注意的是,程序在执行多分支结构时,会按照条件的顺序逐一判断,直到遇到一个成立的条件,然后执行隶属于该条件下的语句块,随后会直接跳出整个多分支结构!如果所有的条件都不成立,则会执行多分支结构最后的语句块(对应着图中的 语句块N
)。所以在编写多分支结构的代码时需要注意判断条件的顺序,避免出现逻辑错误,下文中我们将使用例子帮助大家理解。
图4 多分支结构示意图
3循环结构
循环结构是在程序中需要反复执行某个功能而设置的一种程序结构。可以遍历循环体中的每个元素,经常与分支结构(判断语句)组合使用。
下面通过一个场景简单认识一下循环结构。
首先将所有在营的家庭农场数据筛选出来(假设共有1000个家庭农场),随后我们逐一查看这些家庭农场是否拥有商标,如果拥有商标,那么取出这条数据存入另一张表中;如果没有商标,则不做处理,继续查看下一个家庭农场。
我们可以将上面这件事情视作一个使用了循环结构 + 单分支结构的程序。那么就有了下面的对照。
-
这 1000 条在营的家庭农场代表循环体;
-
“逐一” 代表循环(的动作),即一个一个地处理循环体中所有元素;
-
“查看” 代表单分支结构中的判断条件,判断家庭农场是否拥有商标;
-
存入另一张表还是跳过则是在面对不同的判断结果做出的不同反应。
本期文章的主题是分支结构,所以这里就不再过多介绍循环了。
分支结构中的判断条件
在介绍判断条件之前,我们需要了解一下 Python 语法中的 缩进
。Python 语言有着严格的书写格式,在 Python 代码中,使用缩进来表示代码之间的逻辑关系。缩进是指每一行代码中的最左侧的空白部分,没有逻辑关系(顺序执行)的代码会顶格编写,左侧不留空白。而当表示分支、循环、函数、类等具有逻辑关系的程序时,需要在 if
、else
、elif
、for
、while
、def
、class
等保留字所在语句最后面加上英文半角冒号 :
,并在之后(下一行开始)的代码中进行缩进(在键盘上按一次【Tab】键或者使用连续四个空格可以表示一层缩进)。缩进表达了代码的所属关系,单层缩进的代码隶属于之前一行没有缩进的代码,多层缩进代码需要根据缩进关系决定所属范围。例如:
if <判断条件>:
<语句1>
<语句2>
else:
<语句3>
<语句4>
<语句5>
<语句6>
在上面的伪代码中,语句 if <判断条件>:
和 else:
为非缩进语句,语句1-语句5
均为单层缩进语句,那么 语句1-语句2
构成了一个语句块,这个语句块隶属于语句 if <判断条件>:
;语句3-语句5
构成了一个语句块,这个语句块隶属于语句 else:
;而 语句6
则是在分支结构之外的语句,不隶属于任何语句。关于缩进的更多内容可以移步这篇博客:https://blog.csdn.net/wosind/article/details/100012180
。
Python 中使用关键字 if
+ 判断条件
来表示判断语句,但这种语句不能单独使用,还需要在判断语句后加上英文半角冒号 :
,并在下一行添加语句块(语句块每一行代码的最前面要添加一层 缩进
,代表这段代码隶属于前面的判断语句),表示判断条件成立时需要执行的内容。这也是典型的单分支结构,书写格式如下:
if <判断条件>:
<语句块>
在上面的伪代码中, <判断条件>
的形式有很多种,这些判断条件大致可以分为三类,即关系表达式、数据表达式和逻辑表达式。下面逐一介绍他们。
1关系表达式
关系表达式是判断条件中最直观、容易理解的一类,关系表达式使用比较运算符或成员运算符表示两个数据的关系,如果关系表达式成立,那么会产生结果 True
,否则产生结果 False
,判断语句中的关键字 if
则会根据关系表达式产生的结果选择是否执行后续的语句块。例如:
if 1+1==2:
print(True) # 条件成立时需要执行的代码,下同
if 100>=9:
print(True)
if 10<9:
print(True)
if '农业' in '农业技术':
print(True)
常用在判断条件中的比较运算符和成员运算符如下表所示:
2数据表达式
数据表达式直接将一个 Python 数据作为判断语句中的判断条件,这种表达式的前身是布尔值 True、False 与数值 1 、0 之间的对应关系。由于其含义与布尔值 True 和 False 完全吻合且效率极高,所以这种习惯在编程语言中得以保留。而在数据分析中,大家也习惯以 1 表示真(True),0 表示假(False)。这种对应关系在 Python 中也有体现,如下图所示。
在计算机内部,系统将数字 1 与 0 转换为两种不同的电信号,并以此指挥计算机硬件的工作。
于是在判断语句中,数字 1 或 0 也能作为一个判断条件了,如下图所示。
另外,判断条件中的数值并不局限于数字 1 和 0 ,实际上,所有的数字都可以作为判断条件。判断规则如下:
-
所有与数字 0 相等的数字会被判断为假(False),比如数字 0、0.0
-
所有与数字 0 不相等的数字会被判断为真(True),比如数字 0.1、5、-3
-
空值 None 会被判断为假(False)
除此之外,Python 中任何一类基础数据类型都可以作为判断条件。也就是说除了数值型数据之外,能够存储多个元素的组合数据类型或者字符串也能作为判断条件。如果你是初学者,可能会觉得很离谱,为什么一个列表能作为一个判断条件?怎么判断?是不是太扯了?
别着急,下面跟大家解释清楚。数字作为判断条件的判断依据是数字是否等于 0,而组合数据类型或字符串作为判断条件进行判断的依据并不是其中的元素值的大小,而是其中元素的个数。以列表为例,一个含有 1 个元素的列表作为判断条件时会被判断为真,而一个空列表则会被判断为假。上述规则适用于其他任何组合数据类型以及字符串。简单示例如下图所示:
常见的数据表达式如下表所示。
3逻辑表达式
逻辑表达式就是使用逻辑运算符将一到多个判断条件连接起来,组成一个复合表达式。三种逻辑运算符如下表所示。
比如有这样一个逻辑表达式作为判断条件。
if <条件1> and <条件2> and <条件3>:
<语句块>
在上面的伪代码中,逻辑运算符 and
连接了三个条件,那么当且仅当这三个条件都为真时才会被关键字 if 判断为真。这里的单个条件可以是关系表达式、数据表达式、逻辑表达式中的任意一种,如果是逻辑表达式,则需要根据运算符的情况选择是否使用小括号包裹起来,使判断条件的结构更加清晰。示例如下:
if <条件1> and (<条件2> or <条件3>): # 如果小括号中是 and, 则可以不用小括号
<语句块>
其他逻辑运算符的使用,大家可以参考上表中的描述自行探索。
分支结构的 Python 代码
前面我们介绍了 Python 中三种分支结构,并使用示意图帮助大家了解它们的结构和流程,下面我们从写代码解决实际问题的角度来介绍这三种分支结构。
1单分支结构
单分支结构是最简单的分支结构,我们在介绍判断条件的类型时已经使用过,其标准代码格式以及结构示意图如下:
if <判断条件>:
<语句块>
【问题提出】:家庭农场数据(截至2021年底)中有一字段记录着家庭农场的经营状态,如下表所示。
请在给出家庭农场名称和经营状态的情况下,编写代码将注销的家庭农场筛选出来并将其名称存放在一个列表中。
【答】:
# 给定家庭农场名称和经营状态
NAME = "*****家庭农场"
STATE = "***" # 注销 或者 在营
# 创建一个列表用于存放已经注销的家庭农场的名称
Target = []
# 下面是使用单分支结构解决这个问题的代码
if STATE == "注销":
# 条件为真时,将给定的家庭农场名称加入到列表 Target
Target.append(NAME)
上面的单分支语句代码虽然只能对一家合作社的经营状态进行判断并作出相应操作,但是分支结构常常与循环结构一起使用,这样一来我们就可以通过循环对所有家庭农场的经营状态进行判断并处理。其他分支结构的使用同样是这个道理。
2二分支结构
与单分支结构相比,二分支结构增加了一个处理路径 。即当判断条件不成立时,二分支结构将会执行另一路径的代码,而不是什么都不做。如果单分支结构可以被描述为:如果……那么……
,那么二分支结构则可以被描述为:如果……那么……否则……那么……
。二分支结构的标准代码格式和示意图如下:
if <判断条件>:
<语句块1>
else:
<语句块1>
【问题提出】:还是处理家庭农场数据的经营状态,增加了一个需求,在给出家庭农场名称和经营状态的情况下,编写代码将已注销家庭农场的名称存放在一个列表中,将非注销状态的家庭农场的名称存放到另一个列表中。
【答】
# 给定家庭农场名称和经营状态
NAME = "*****家庭农场"
STATE = "***" # 注销 或者 在营
# 创建两个空列表
ZhuXiao = [] # 用于存放注销状态的家庭农场名称
NOT_ZhuXiao = [] # 用于存放非注销状态的家庭农场名称
# 下面是使用二分支结构解决这个问题的代码
if STATE == "注销":
# 条件为真时,说明该家庭农场已经注销,将家庭农场名称存入列表 ZhuXiao
ZhuXiao.append(NAME)
else:
# 当条件不为真时,执行另一操作,将家庭农场名称存入列表 NOT_ZhuXiao
NOT_ZhuXiao.append(NAME)
3多分支结构
多分支结构与其他两种分支结构有很大不同,常用于处理更加复杂的情况。由于多分支结构中涉及多个判断条件,所以需要特别注意多个条件之间的顺序,以免产生逻辑错误。多分支结构的标准代码格式和示意图如下:
if <判断条件1>:
<语句块1>
elif <判断条件2>:
<语句块2>
elif <判断条件3>:
<语句块3>
……
elif <判断条件 N-1>:
<语句块N-1>
else:
<语句块N> # 判断条件 1 ~ N-1 都不成立时才会执行 else 下面的语句块
下面我们举例演示多分支结构的用处和用法。
【问题提出】农民专业合作社基本信息数据(截至2021年底)中有一字段记录着合作社的注册资本金,样例数据如下图所示。
我们需要根据合作社注册资金的大小给合作社打一个标签,用来描述合作社的规模,规则如下:
-
注册资金在数值区间
(0 - 10)
的,标签为微型合作社 -
注册资金在数值区间
[10 - 100)
的,标签为小型合作社 -
注册资金在数值区间
[100 - 500)
的,标签为中型合作社 -
注册资金在数值在 500(含500)以上的,标签为大型合作社
-
注册资金数值小于等于 0 的,标为注册资金异常合作社
在给定合作社名称、注册资金的情况下,根据规则给合作社打上标签,并将这种合作社名称和规模标签以 键-值对
的形式存储在一个 Python 字典中。
【答】:
# 给定合作社名称和注册资金(单位:万元人民币)
NAME = "*****合作社"
REG_CAP = ****
# 创建一个空字典,用于存储合作社名称与规模标签的对照
SCALE = {}
# 下面是使用多分支结构为合作社打标签的代码
if REG_CAP <= 0:
SCALE[NAME] = '注册资金异常合作社'
elif 0 < REG_CAP < 10:
SCALE[NAME] = '微型合作社'
elif 10 <= REG_CAP < 100:
SCALE[NAME] = '小型合作社'
elif 100 <= REG_CAP < 500:
SCALE[NAME] = '中型合作社'
elif REG_CAP >= 500:
SCALE[NAME] = '大型合作社'
else:
pass # pass 表示跳过,什么都不做
上面的代码能够根据一家合作社的注册资本给该合作社打上我们设定的规模标签,如果将上述代码放到循环中使用,我们就可以为整张表中所有的合作社打上标签。
循环结构简介
从结构上来说,循环结构是在程序中需要反复执行某个功能而设置的一种程序结构。从代码层面来讲,循环是程序设计语言中反复执行某些代码的一种计算机处理过程,常见的有遍历循环(按次数循环)和无限循环(按条件循环),在 Python 中分别对应着 for 循环
和 while 循环
。
下面我们通过两个场景简单认识一下这两种循环结构。
1遍历循环
假设,我们需要在全国在市场监管部门登记在册,在营的家庭农场中找出所有拥有商标的家庭农场。经统计,截至 2021 年底,全国共有 310465 家在营的家庭农场。我们逐一查看这些家庭农场是否拥有商标,如果拥有商标,那么就把这个家庭农场的信息另存入一张表中;如果未拥有商标,则跳过不做处理。
我们可以将上述场景转换成一个使用了for 循环 + 单分支结构
的程序,那么下面这些对照会帮助大家理解这个 “程序”。
-
310465 家在营的家庭农场代表循环体
-
“逐一” 代表 for 循环(的动作),即一个一个地处理循环体中所有元素
-
“查看” 代表单分支结构中的判断条件,判断家庭农场是否拥有商标
-
存入一张表/还是跳过则是在面对不同的判断结果做出的不同反应
下图是该 “程序” 的示意图。黄色圆形代表循环体,其中包含 310465 家在营家庭农场的信息(包括是否拥有商标的标识字段);for 循环每次从循环体中获取一个家庭农场的相关信息并根据是否拥有商标做出不同的处理,处理过后继续获取循环体中下一个(注:for 循环会按照循环体中元素的顺序依次取出元素,而不是每次随机获取一个元素)家庭农场的信息并进行判断和处理,直到所有的家庭农场都经过处理,循环就会结束。
2无限循环
无限循环并不一定真的会无限制地执行循环,之所以称之为无限循环,是因为循环的次数是不确定的。只有满足终止循环的条件才会退出循环,用一句话概括——不达目的誓不罢休!
在数据处理过程中,需要获取所有家庭农场的
统一社会信用代码
。但由于部分家庭农场在统一社会信用代码推出之前(2015 年 6 月 4日 正式推出)就已经注销,所以这部分家庭农场没有统一社会信用代码,如下图所示。我们将表中的 “统一社会信用代码” 字段转为
列表 A
,由于一些机构缺少统一社会信用代码,导致一些空值''
也混入了列表 A,接下来我们要做的就是将列表 A 中的所有空值''
全部删除,只保留有效的统一社会信用代码。可问题是列表类型每次只能删除一个元素,所以需要用到循环结构来解决问题,处理思路是每次从列表 A 删除一个空值,直至列表 A 中不再含有空值。处理后的列表 A 可以写入数据表另作他用。
下图是使用无限循环(while 循环)解决上述问题的流程图。
通过以上场景,我们已经能够初步了解 Python 中两种循环结构的特点。顺便提一句,在数据处理和数据分析中,遍历循环的使用频率相对更高。
遍历循环中的循环体
在 Python 中的两种循环结构中,只有遍历循环(for 循环)中有循环体的概念。我们从上一节的场景中可以大致了解到循环体是一种能够存放多个元素的类型,因为只有这样,循环才会有意义。那么哪些类型可以在 for 循环中充当循环体的角色呢?
在 Python 中,只要是可迭代的
对象都可以作为循环体,由于 Python 中的数据类型不仅仅只有基础的数据类型,还包括其他第三方库中的类型,例如科学计算库 numpy
中的 array
类型、数据处理与分析库 pandas
中的表结构 DataFrame
类型等。我们如何判断这些类型是否为可作为迭代对象呢,下面是解决方案。
# 从标准库 collections 导入 Iterable 方法
from collections import Iterable
# 使用 isinstance 方法 + Iterable 方法可以判断下面语句
# 中的 “Target” 是否为可迭代对象,如果是,会返回 True, 否则返回 False
print(isinstance(Target, Iterable))
下面我们使用上述方法验证 Python 中哪些基础数据类型是可迭代的,那些是不可迭代的。验证过程如下图所示。
经过验证,得知字符串以及四种组合数据类型都是可迭代的,可以作为循环体。即使是其中无序的集合类型,也可以在 for 循环中使用,只不过由于集合的无序性,循环中元素的顺序是随机的,循环的次数仍是集合中元素的数量。除了以上基础类型之外,range()
函数也是一种常用作循环体的对象(作为循环体时其中的元素均为整数),它最大的特点是可以指定循环的次数。比如在处理一个 10000 行的数据时,我们使用 range 函数指定循环次数为 10000 次,在循环过程中每次处理一行数据,最后刚好在处理完全部 10000 行数据后退出循环。
下面我们简单介绍一下
range()
函数。
range(start,stop[,step]) # [] 代表非必需,即可省略参数
上述代码是 range() 函数的标准用法,共有三个参数。其中参数 start
表示起始数字,默认值为 0;参数 stop
表示终止数字;参数 step
表示步长,默认值为 1。这三个参数中,只有 stop 参数是必不能省略的。下面我们使用代码来分情况介绍 range() 函数的具体用法。
(1) 不省略任何参数
(2)省略参数step
(3)只使用参数stop
循环结构的 Python 代码
1遍历循环——for 循环语句
Python 中 for 循环
(或者说 for-in 循环
)的功能是依次取出循环体中的元素,并根据这些元素处理相关的数据。循环体中所有元素都经过处理后,循环自动结束。for 循环语法格式和示意图如下。
for <循环变量> in <循环体>:
<语句块>
下面我们使用一个实例来介绍一下 for 循环
。
【场景1】在全国所有在市场监管部门登记在册的家庭农场数据中,统计出成立时间在 2018-2021年之间的家庭农场数量。
【解决方式】先将家庭农场成立时间这一字段转为列表,随后循环此列表,每发现一个成立日期在指定时间区间内的数据,则目标统计值加1,代码如下。
# 将表中成立时间这一字段转为列表,那么列表中的数据均是企业成立时间
ESDATE = list(data['成立时间'])
# 初始化目标统计值
Num = 0
# 使用 for 循环遍历列表 ESDATE
for date in ESDATE:
# date 就是列表 ESDATE 中的元素,也就是一个成立日期
# 设置判断条件判断是都是符合条件的日期
if 2018 <= date.year <= 2021:
Num += 1 # 日期符合条件,那么统计目标 Num 增加 1
# 循环结束后,输出统计值
print(f'2018-2021年,共有{Num}个家庭农场成立。')
# 输出:2018-2021年,共有182855个家庭农场成立。
2无限循环——while 循环语句
与 for 循环不同,while 循环
不涉及循环体,也没有明确的循环次数,在 while 循环开始之前需要设置一个终止循环的条件。只有当满足这个终止条件,循环才会终止。while 循环的语法格式和示意图如下。
while <循环终止条件>:
<语句块>
在上面的 while 循环结构或示意图中,语句块中的内容最好能够直接影响循环终止条件,如果语句块中的内容对终止条件的判断毫无影响,那么 while 循环会有可能陷入死循环。
在上图的 while 循环中,终止条件 number > 1
永远是成立的,语句print(number)
对终止条件毫无影响。那么在这个程序被关闭或机器坏掉之前,这个循环将会永远执行下去。我们把程序变换一下,添加一行代码,每次循环让 number
减去 1,那么终止条件就会在几次循环之后不再成立,程序也会结束,如下图所示。
主动退出循环与主动跳过循环
在循环结构中,除了让循环程序自动结束之外,我们还可以使用 break
关键字或 continue
关键字主动控制循环,这在实际的编程中是比较常用的手段。
1主动退出循环——break
无论是在 for 循环中还是在 while 循环中,都可以使用 break
关键字直接跳出整个循环结构,执行循环之外的代码。如果有多层循环(循环中还有循环),则会跳出 break
语句所在的最内层循环。下面是使用示例。
上图所示程序是我们在介绍 while 循环时用到的死循环,我们在死循环中加入条件,当程序执行时间大于等于 5 秒钟,就执行 break 语句跳出循环。在程序最后,变量 number
的值仍是 5,说明循环终止条件一直都是不成立的,但当程序执行时间大于 5 秒,进而执行到 break 语句时,这个死循环还是乖乖的结束了。
这个关键字/语句有什么实际用处呢?在实际的编程中,经常需要测试代码或者修改代码中的 BUG,如果我们在循环的最后面加上一句 break,那么程序在执行完第一次循环后就会主动结束。这时我们可以方便地查看程序这一次循环中的变量值,进而快速定位、解决问题,不再需要等待整个循环结束。除此之外,当在循环程序中已经达到既定目的后,也可以使用 break 语句退出循环,这就和 while 循环有些相似了。
2主动跳过循环——continue
continue 语句的功能是中断/跳过本次循环,直接进入下一次循环。下面我们通过一个场景简单了解 continue 语句的功能。
【场景 2】在全国所有在市场监管部门登记在册的家庭农场数据中,统计出成立时间在 2018-2021年之间且在 2021 年底仍是在营状态的家庭农场数量。
【解决方式】主要思路是先查看企业状态是否是在营,如果不是,那么直接跳过这次循环,进入下一次,不再继续判断该家庭农场的成立时间是否符合要求。如果是是在营状态,则再查看成立时间。代码如下。
# 将表中成立时间、企业状态两字段分别转为列表
ESDATE = list(data['成立时间'])
STATE = list(data['企业状态(2021年底)'])
# 初始化目标统计值
Num = 0
# 使用 for 同时循环遍历列表 ESDATE 和 STATE
for date,state in zip(ESDATE, STATE):
# date 是成立日期, state 是企业状态
# 如果企业状态不是“在营(开业)企业”,那么直接跳过本次循环,不再判断成立时间是否符合要求
if state != '在营(开业)企业':
continue
# 判断企业成立时间是否符合要求
if 2018 <= date.year <= 2021:
Num += 1 # 日期符合条件,那么统计目标 Num 增加 1
# 循环结束后,输出统计值
print(f'2018-2021年,共有{Num}个家庭农场成立且至少存活至2021年底。')
# 输出:2018-2021年,共有174351个家庭农场成立且至少存活至2021年底。
同步遍历循环——使用zip()函数
在遍历循环最简单的用法中,循环体往往是一个包含多个元素的可迭代对象。比如列表、字典等单一的组合数据类型,在上期介绍循环的文章中,我们使用了一个 统计成立时间在 2018-2021年之间的家庭农场数量 的案例来介绍遍历循环。主要过程是在全国所有在市场监管部门登记在册的家庭农场数据中,使用 for 循环处理所有家庭农场的成立日期(如下图所示,截图仅为样例数据),循环过程中每遇到一个家庭农场成立日期在指定时间范围内的数据,统计值(初始值为 0)就会自动加一,循环结束后的统计值就是我们想要得到的结果。
这个案例的 Python 代码如下。
# 读取样例数据(随文赠送的)
data = pd.read_csv('./全国家庭农场成立日期(样例数据).csv')
# 成立日期字段转为日期类型
data['成立日期'] = data['成立日期'].astype('datetime64[ns]')
# 将表中成立时间这一字段转为列表,那么列表中的数据均是企业成立时间
ESDATE = list(data['成立日期'])
# 初始化目标统计值
Num = 0
# 使用 for 循环遍历列表 ESDATE
for date in ESDATE:
# date 就是列表 ESDATE 中的元素,也就是一个成立日期
# 设置判断条件判断是都是符合条件的日期
if 2018 <= date.year <= 2021:
Num += 1 # 日期符合条件,那么统计目标 Num 增加 1
# 循环结束后,输出统计值
print(f'2018-2021年,共有{Num}个家庭农场成立。') # 输出:2018-2021年,共有182855个家庭农场成立。
在上面的案例中,循环体ESDATE
是一个列表,其中包含了所有家庭农场的成立时间。如下图所示:
现在我们换一个需求,要求 获取所有成立时间在2018-2021年之间的家庭农场的企业名称和成立时间 。这时我们发现, 仅循环成立时间列表的话只能得到符合要求的家庭农场的数量或成立时间,却无法得到这些家庭农场的名称。比较稳妥的做法是同时循环企业名称列表和企业成立时间列表,那么在每一次循环中同时存在一家企业(家庭农场)的名称和成立日期,如果成立日期符合要求,我们获取企业名称和成立时间就可以了。
迎面而来的问题是怎么样才能同步循环这两个列表呢?答案是使用 Python 内置的 zip()
函数。
在 Python 3.x 版本中,zip()
函数的功能是返回一个元组迭代器,这样说非常不直观,下面我们用一个例子帮助大家理解。
观察上图中红色方框中的内容,会发现zip()
函数将图中的字母列表和数字列表中的元素按顺序一 一绑定对应起来,每一组绑定的结果都使用元组保存。而这个结果是我们将其转为列表之后才观察得到的,而zip()
函数本身返回的结果则是一个无法直观看到任何内容的迭代器(见代码输出结果中的<zip object at 0x000001F4D424FD80>
),这是因为迭代器是一个占用内存很少的类型,这种做的目的就是帮助我们节省内存。说到这里,我们应该如何使用zip()
函数做到同步循环两个列表呢?请看下图。
在上图所示的 for 循环中使用zip()
函数, 发现每一次循环中的字母和数字都是按顺序对应的。另外,笔者故意将数字列表
的长度设置的更长,最后我们发现,当zip()
函数中的多个对象长度不一致时候,会按照木桶原理返回一个长度与zip()
中最短对象的长度相同的迭代器,其他较长对象中多余的元素会被舍弃。
回到解决问题的立场上来,我们发现掌握了zip()
函数的用法之后,这个问题就迎刃而解了,具体代码如下。
# 将表中成立时间这一字段转为列表,则列表中的元素值均是家庭农场成立日期
ENTNAME = list(data['企业名称'])
ESDATE = list(data['成立日期'])
# 用于存储符合条件的企业名称的列表
TARGET_NAME_DATE = []
# 使用 for 循环遍历列表 ENTNAME 和 ESDATE
for name, date in zip(ENTNAME, ESDATE):
# name 是列表 ENTNAME 中的元素。即企业名称
# date 是列表 ESDATE 中的元素。也就是成立日期
# 此时 name 和 date 是对应的,date 就是 name 的成立日期
# 设置判断条件判断是否是符合条件的日期
if 2018 <= date.year <= 2021:
TARGET_NAME_DATE.append([name, date]) # 每有一个家庭农场的成立时间符合要求,目标统计值就会加 1
# 循环结束后, 将符合要求的企业名称列表转为表格,使用到 pandas 模块
TARGET_TABLE = pd.DataFrame(TARGET_NAME_DATE,
columns=['2018-2021成立的家庭农场的名称', '成立时间'])
TARGET_TABLE
所得结果如下图所示。
添加索引循环——使用enumrate()函数
一般的遍历循环往往是遍历循环体中的每一个元素。而在一些特殊的需求中,我们需要在循环中添加循环元素在循环体中的索引。此时只需要在循环体外加上 enumrate()
函数,那么循环中就既含有循环体中的元素,又包括该元素在循环体中的索引值。如下图所示。
以上就是 enumrate()
函数的功能。同 zip() 函数一样,enumrate()
函数也是 Python 内置的函数,并且enumrate()
函数的返回结果也是一个迭代器。下面我们通过一个解决实际问题的场景说明 enumrate()
的作用。
问题提出:有一个占用空间很大的 csv 文件,我们希望快速的了解这个 csv 数据的数据量,但是由于这个文件占用空间太大了,如果使用 Stata、Python 等工具来导入这个文件,可能会导致计算机内存不足。此时我们可以使用遍历循环 + enumrate()
函数的方式来获取这个文件的行数,也就是数据量。
💡 csv 是一种可以表示表结构的文本文件,文件后缀名为 “.csv” 。我们可以使用 Excel、WPS 等工具打开 csv 文件,打开后就是表格形式;也可以以文本文件方式打开 csv 文件,打开后是文本,此时文本中的一行内容就表示表格中的一行内容,也就是说文本的行数就是该 csv 表格的行数(数据量)。
解决以上问题的 Python 代码如下。
# 这里使用随文赠送的 csv 文件,该文件的编码为 utf-8
for count,row in enumerate(open('./全国家庭农场成立日期(样例数据).csv', 'rU', encoding='utf-8')):
pass
count # 循环结束后,count 就是 csv 文件的行数(表头不算在内)
以上代码的原理就是循环读取 csv 的每一行数据,每次只读取一行数据(代码中的 row
),因此只会占用极少的内存空间。使用 enumrate()
函数后还能得到循环中一行数据在整个数据中的索引(代码中的count
,也就是一行数据的序号)。在每一次循环中,我们什么都不处理(代码中的pass
表示跳过),这样能最快速的结束循环,循环结束后的变量 count 就是 csv 文件中最后一行数据的索引值,也就是 csv 文件的总行数。在这个场景中enumerate()
函数节省内存和获取索引的特性被完全发挥了出来。
循环中的异常处理——使用 try-except 语句
程序异常是我们常说的“报错”中的一种,“报错”可以分为两大类,一类是语法错误(SyntaxError),另一类是程序异常(Exception),它是在程序没有语法错误的前提下发生的。如下图所示。
无论是程序错误还是程序异常,只要程序报告错误,就会立刻停止运行。我们在使用 Python 时遇到程序报告语法错误,只需要找到语法错误的语句并改正就好了。如果在一般程序中遇到程序异常,我们可以分析异常原因并加以改正。可是一旦在循环程序中遇到程序异常就不好办了,因为循环程序一般会重复运行很多次循环代码,所以在一个完整的循环中,程序异常的具体类型和发生异常的次数是不可控的。因此我们必须掌握 Python 中能够处理程序异常的的语法。
Python 中通常使用try-except
语句实现异常处理,下面我们来介绍这个语句的不同用法。
用法1:平等处理所有异常
try-except
语句最简单粗暴的用法的语法如下。
try:
<语句块1>
except:
<语句块2>
在循环中,我们可以把容易出现异常的代码放在try
语句下的<语句块1>
中,表示系统尝试执行这段代码,如果执行try
下面的代码块时没有触发异常,系统会跳过except
语句,进而执行整个try-except
之后的代码。而一旦try
下面的代码块触发异常(包括任何异常类型),Python 不会立即报告异常,而是会找到except
语句并执行下面的<语句块2>
。但是如果<语句块2>
也触发异常,那么程序则会报告异常并停止运行。
用法2:针对处理某种异常
在上面的try-except
语法中,只要try
语句下的代码一旦触发异常(不包括语法错误),那么不管是何种异常类型,程序都会跳到except
语句下的代码。这种“一刀切”的做法既有利也有弊,利在于给程序员减少了很多麻烦,弊则是程序对不同类型异常的识别和处理方式不够精准。
try-except
语句提供了另外一种精准处理某种异常的做法,使用语法如下。
try:
<语句块1>
except <异常类型x>:
<语句块2>
这种用法与 用法1 最大的区别就是,用法1 中的try-except
语句会处理所有类型的异常,而这种用法则只针对<异常类型x>
做处理。如果<语句块1>
中触发异常且异常类型为<异常类型x>
,那么程序会执行except
语句下的<语句块2>
;而如果触发异常的类型不是<异常类型x>
,此时程序就会正常地报告异常,随后停止运行。也就是说,这种用法只针对<异常类型x>
给出了解决方案,对其他的异常类型不起任何作用。
用法3:针对处理多种异常
在 用法2 中我们只对一种异常类型做针对处理,实际上我们还可以在 用法2 的基础上做扩展,实现对多种不同异常类型的针对处理,语法如下。
try:
<语句块1>
except <异常类型x>:
<语句块2>
except <异常类型y>:
<语句块3>
except <异常类型z>:
<语句块4>
……
上述代码对try
语句下的<语句块1>
中可能触发的三种异常类型x、y、z分别做了针对处理。若<代码块1>
触发<异常类型x>
则执行<语句块2>
;触发<异常类型y>
则执行<语句块3>
;触发<异常类型z>
则执行<语句块4>
……除此之外,try-except
语句还有else
子句和finally
子句,不过这些子句的使用场景极其少,其作用对数据分析者来说十分鸡肋,一般只有在一些大型项目的源代码中才能看到,这里就不再过多介绍了。
以上三种 Python 使用try-except
语句处理异常的用法中,前两种用法的使用频率更高些。在数据处理过程中,如果需要使用异常处理,绝大多数情况下,使用本文所介绍的用法1已经足够解决问题。下面我们通过一个实际案例简单介绍一下try-except
语句的使用场景。
案例:很多时候我们需要 OCR 识别一些 pdf 文件或图片文件,将其中的表格转化为可编辑可分析的 Excel 表。例如这里我们 OCR 识别中国城市统计年鉴中的表格并获取各项指标。部分原件如下图所示。
我们对该文件做 OCR 识别,转为 Excel 表后再进行整合处理,得到下图所示指标统计表。
由于表格是使用 OCR 技术识别的,所以上表中“指标取值”
这一字段中的数值都是字符型。不过由于部分原件比较模糊且 OCR 技术的识别率不够高,导致一些数字指标的识别可能出现差错,如下图所示。
对于上图所示识别出错的指标,我们需要人工查找原件进行核对,问题是如何定位到所有可能识别出错的指标呢?我们的方法是借助 Python 的内置函数 eval()
来处理。这个函数的功能是将传入的字符串当做表达式来求值并返回计算结果,如下图所示。
处理的主要思路是使用 eval()
函数循环处理表中“指标取值”
这一字段,将指标数值由字符型一 一转为数值型(整数或浮点数),并且在循环中使用try-except
语句,如果在try
语句中遇到不能转为数值的数据,那么程序会触发异常(这也是为什么不能使用判断语句进行处理的原因),这时我们返回一个自定义的标记,循环结束后再根据这个标记就可以找到可能识别出错的指标并人工核对。具体的代码如下。
结果如下。
# 读取数据
data = pd.read_excel('./中国城市统计年鉴(样例数据).xlsx')
# 添加新的字段,用于存放 eval() 函数转化后的指标数值。
data['指标取值(数值)'] = ''
for i in data.index:
# i 是数据 data 的行索引
value_str = data['指标取值'][i] # 根据行索引获取指标值(字符型)
# 尝试将字符型指标转为数字型并填充到新的字段中
try:
data['指标取值(数值)'][i] = eval(value_str)
except:
# 如果 eval(value_str) 触发异常,说明数值可能识别错误了,
# 这里我们将一个特殊的标记填入新字段中
data['指标取值(数值)'][i] = '识别可能出错!'
循环结束后,我们利用设置的特殊标记找到所有可能识别出错的数据,代码和结果如下。
# 找到新字段中的值等于我们设置的标记的数据
data[data['指标取值(数值)'] == '识别可能出错!']
最后根据上面的结果找到所有可能识别出错的结果,然后人工核对、修改即可。
以上就是 Python 程序异常处理语句try-except
的一个简单应用场景。