课程链接:头歌实践教学平台
自用学习整理,无商业用途
来源:头歌实践教学平台
3.1 结构化程序设计
结构化程序设计是一种进行程序设计的原则和方法,其特点是只有一个入口、一个出口。按照这种原则和方法可设计出结构清晰、容易理解、容易修改、容易验证的程序。 其目标是使程序具有一个合理结构,以保证和验证程序的正确性,从而开发出正确、合理的程序。结构化程序设计采用自顶向下、逐步求精的设计方法,各个模块通过“顺序、选择(分支)、循环”的控制结构进行连接。
3.2 顺序结构
顺序结构表示程序中的各操作是按照它们出现的先后顺序执行的。
radius = 6378.137 # 地球赤道半径6378.137km density = 5507.85 # 地球平均密度5507.85 kg/m³ pi = 3.141592653589793 # 圆周率pi值 volume = 4 / 3 * pi * radius ** 3 # 计算地球体积,立方千米 print(f'地球体积约为{volume}立方千米') # 地球体积约为1086851326519.959立方千米 weight = volume * 1000 ** 3 * density # 计算地球质量,kg print(f'地球质量约为{weight}千克') # 地球质量约为5.986214078772957e+24千克
Python是解释型执行,前面语句出错会导致后面的语句无法运行。
radius = 6378.137 # 地球赤道半径6378.137km density = 5507.85 # 地球平均密度5507.85 kg/m³ pi = 3.141592653589793 # 圆周率pi值 volume = 4 / 3 * pi * radius ** '3' # '3' 是字符串,不能做幂运算,触发异常 print(f'地球体积约为{volume}立方千米') # 地球体积约为1086851326519.959立方千米 weight = volume * 1000 ** 3 * density # 计算地球质量,kg print(f'地球质量约为{weight}千克') # 地球质量约为5.986214078772957e+24千克
程序执行到第4条语句时,因 '3' 是字符串,不能做幂运算,触发异常,导致后面的语句5,6,7无法执行。 遇到语法错误时,需要先修正错误,再继续向下执行。
3.3 选择(分支)结构
选择结构表示程序的处理步骤出现了分支,它需要根据某一特定的条件选择其中的一个分支执行。通过合理的设置选择的条件,使程序从起点到终点只能通过一条路径,保证程序只有一个出口。Python语言用if…elif…else关键字或其组合来选择要执行的语句,不同的组合可构成单选择、双选择和多选择三种形式。if、elif和else是Python中分支结构的关键词,分支结构由1个if、0个或多个elif和1个或零个else组成。由这三个关键词开始的语句最后以冒号结束,同时要求其后面满足该条件时要执行的语句块缩进。同一组分支结构里的每个关键词必须要对齐。
# 仅用于语法展示,请示要运行 if 条件测试表达式1: 语句块1 elif 条件测试表达式2: 语句块2 elif 条件测试表达式3: 语句块3 …… else: 语句块n
if 和 elif 子句后都有条件测试表达式,当表达式的值为真(非0数字、非空对象或True)时执行对应的语句块。 else 子句后面无条件测试表达式,直接以冒号结束。
3.3.1 多分支结构
多分支语句用一组 if…elif…else 语句将整个区间分为若干个区间,当满足其中某一个区间的条件时,一定不会再满足后续的其他条件,程序即终止判定,所以用这种方法编写程序时,若区间设定不误,可保证只能有一种处理和一个出口。
实例 4.1 百分制分数转五分制
输入一个整数,当输入不在[0,100]区间时输出提示“Data error!”; 当输入满足要求的前提下,用多分支结构实现百分制转五分制。 实现规则是根据分数所在区间[0,60)、[60,70)、[70,80)、[80,90)、[90,100],分别输出字符“E”、“D”、“C”、“B”、“A”。
score = int(input()) # 输入一个整数表示成绩 if score > 100 or score < 0: # 区间为score < 0 或score > 100 result = 'Data error!' elif score >= 90: # 区间为90 <= score <= 100 result = 'A' elif score >= 80: # 区间为80 <= score < 90 result = 'B' elif score >= 70: # 区间为70 <= score < 80 result = 'C' elif score >= 60: # 区间为60 <= score < 70 result = 'D' else: # 区间为0 <= score < 60 result = 'E' print(result)
在这个程序中,因为只能进入其中一个分支,每个分支下的处理结果定义为相同的变量名,在程序执行完毕后用一条语句输出。这样可以保证输出格式的一致性,也避免多次调用print()函数。
# 不推荐使用以下方法: score = int(input()) # 输入一个整数表示成绩 if score > 100 or score < 0: # 区间为score < 0 或score > 100 print('Data error!') elif score >= 90: # 区间为90 <= score <= 100 print('A') elif score >= 80: # 区间为80 <= score < 90 print('B') elif score >= 70: # 区间为70 <= score < 80 print('C') elif score >= 60: # 区间为60 <= score < 70 print('D') else: # 区间为0 <= score < 60 print('E')
3.3.2 二分支
Python也支持传统的二分支方法,每个else和他前面离他最近且与之对齐的 if 匹配
score = int(input()) if score >= 60: print('成绩合格') else: print('不合格')
score = int(input()) if score > 100 or score < 0: # score < 0 或score > 100 print('异常成绩') else: print('正常成绩')
score = int(input()) if 0 <= score <= 100: # score >= 0 and score <= 100 print('正常成绩') else: print('异常成绩')
0 <= score <= 100 等价于 score >= 0 and score <= 100 Python中比较运算推荐使用链式操作0 <= score <= 100,不建议使用score >= 0 and score <= 100,pycharm中可同时按alt+shift+enter自动转为链式操作。
score = int(input()) if score >= 0 and score <= 100: # pycharm中可同时按alt+shift+enter自动转为链式操作 if 0 <= score <= 100: print('正常成绩') else: print('异常成绩')
练一练3
闰年是为了弥补因人为历法规定造成的年度天数与地球实际公转周期的时间差而设立的。闰年分为普通闰年和世纪闰年,其判断方法为: 公历年份是4的倍数,且不是100的倍数,为普通闰年。 公历年份是整百数,且必须是400的倍数才是世纪闰年。 归结起来就是通常说的:四年一闰;百年不闰,四百年再闰。 写一个程序用于判断用户输入的年份是不是闰年,如果是闰年输出“True”,否则输出“False”。 输入: 输入一个代表年份的正整数 输出: “True”或“False”
# 补充你的代码
示例 1 输入:1900 输出:False 示例 2 输入:2000 输出:True 当分支较多时应用二分支方法需要嵌套使用分支结构。 Python根据缩进量来判断层次结构,使用嵌套分支结构时务必严格控制各级别代码块的缩进量,这决定各代码块的从属关系以及各代码块能否被正确的执行。 嵌套结构需多层缩进,不利于阅读,一般能用if…elif…else语句实现时,不建议用二分支嵌套方法。
score = int(input()) if score > 100 or score < 0: result = 'Data error!' else: # 此分支下0 <= score <= 100 if score >= 90: # 此分支下90 <= score <= 100 result = 'A' else: # 此分支 0 <= score < 90 if score >= 80: # 此分支下80 <= score < 90 result = 'B' else: # 此分支 0 <= score < 80 if score >= 70: # 此分支下70 <= score < 80 result = 'C' else: # 此分支 0 <= score < 70 if score >= 60: # 此分支下60 <= score < 70 result = 'D' else: # 此分支 0 <= score < 60 result = 'E' print(result)
3.3.3 单分支
Python也支持单分支方法,所有条件都用if语句判定。 与if…elif…else语句相比,其缺点是不论前面是否已经找到满足条件的分支,后续所有if 语句都会被执行,需要判定每个if语句中的条件,降低效率并容易引入错误。 在单分支结构中,只对条件表达式为True的情况进行处理,忽略了表达式结果为False时的处理,有时会引入意外的错误。尽量避免使用这种方法。
score = int(input()) if score >= 60: print('成绩合格')
score = int(input()) if 0<=score <= 100: print('成绩合法')
score = int(input()) # int()函数将输入的整数字符串转换成整数 if 90 <= score <=100: # 等价于score >= 90 and score <= 100 result = 'A' if 80 <= score < 90: result = 'B' if 70 <= score < 80: result = 'C' if 60 <= score < 70: result = 'D' if 0 <= score < 60: result = 'E' if score < 0 or score > 100: result = 'Data error!' print(result)
使用多分支方法时,因各区间互斥,当判定满足其中一个条件后,一定不会再满足其他条件,所以略过后续的判定,提高效率。 使用单分支方法时,无论前面条件是否满足,后面每个条件都会被依次判定,降低效率。 若条件设置不合适,可能还会得到错误结果,例如下面程序,当输入60、70、80、90时,会输出两个结果:
score = int(input()) # int()函数将输入的整数字符串转换成整数 if 90 <= score <= 100: # 等价于score >= 90 and score <= 100 print('A') if 80 <= score <= 90: print('B') if 70 <= score <= 80: print('C') if 60 <= score <= 70: print('D') if 0 <= score <= 60: print('E') if score < 0 or score > 100: print('Data error!')
需要注意,一般不要用多个if语句匹配一个else语句,容易导致条件判定错误。 因为多个分支时,程序只会进入其中一个分支,所以在每个分支里只改变变量的值,退出分支语句后用一个print()函数进行输出,可以使输出保持一致,避免引入错误。
3.3.4 条件表达式
所谓的条件表达式,是用一条语句完成一个二分支结构的程序语句。 语法格式:
# 语法格式,不要运行此单元 x if 条件 else y
if…else将语句分成三部分,是一个三元操作符的条件表达式。 首先对条件表达式求值,若值为True,则计算表达式x的值并返回其运算结果;当条件表达式结果为False时,对表达式y进行计算并返回其运算结果。 用二分支结构程序将用户输入的两个数中的较大者命名为max_number并输出,可以用以下代码实现。
m, n = eval(input()) # 输入用逗号分隔的两个整数,输出其中较大的值 if m > n: max_number = m else: max_number = n print(max_number)
用条件表达式语句实现:
m, n = eval(input()) # 输入用逗号分隔的两个整数,输出其中较大的值 max_number = m if m > n else n print(max_number)
类似的用法还可用于实现时验证用户名:
username = input() # 输入表示用户名的字符串 print("用户名正确") if username == 'admin' else print("用户名错误")
3.4 while循环
循环结构表示程序重复执行某个或某些操作,直到某条件为假(或为真)时才可终止循环。 在问题求解过程中,很多时候需要重复做一件事情很多次。这时可以选择重复使用这些语句来解决问题,也可以使用循环控制结构来完成。
人类重复做某件事情次数越多,出错的概率越大,所以数学家们研究了各种等差数列、等比数列的求和公式、求积公式,把重复多次的工作变成一个表达式的求解,以降低出错的概率。 计算机与人不同,计算机可以快速精准的完成重复性的工作,而循环结构的使用,既可以减少代码行数、又可以简化业务逻辑,提高程序的可读性。
Python中控制循环有两种结构:
1. while 循环 2. for...in 遍历循环
while循环语句一般用于循环次数不确定的情况下,通过判断是否满足某个指定的条件来决定是否进行下一次循环,也称为条件循环语句。
判断条件的返回值为布尔型,即True 或 False,任何非0或非空的值均为True。
当判断条件表达式的结果为True时,执行while下面缩进部分语句块中的全部语句,然后再次检查判断条件的值,重复以上操作,直到判断条件的结果为False时结束循环。
在循环体的语句中,一定有一条语句会使循环判断条件的结果发生变化,使之在经过有限次运行后能够变“假”,从而结束循环。或在循环体中设定一个边界条件,当条件满足时,执行break语句直接终止当前循环。
while循环的语法如下:
while 判断条件: 语句块(使判断条件表达式的值趋向于False)
实例 4.3 斐波那契数列前n项
1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,……
斐波那契数列又称黄金分割数列,这个数列从第3项开始,后一项的值总是与他前面两项的和的值相等。
在数学上,以递推的方法定义:
F(1)=1 F(2)=1 F(n)=F(n - 1)+F(n - 2) (n ≥ 3,n ∈ N*)
根据数学上的递推公式,可以先把变量名a,b指向开始两个值 1 和 1,然后构建一个循环。
在循环中输出 a 的值,然后计算 a + b 的和,将标签 a 指向对象 b,将标签 b 指向加和产生的新对象上作为新的一项,这个操作可以用同步赋值语句实现:
a, b = b, a + b
a, b = 1, 1 # 初始两项的值 while a < 1000: # 设定终止条件,当a值大于或等于1000时终止循环 print(a, end=',') # 输出当前a 值,不换行,用逗号结束输出 a, b = b, a + b # 将 b 和 a + b 值分别赋值给变量名a和b # 1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,
while循环还有一个重要的应用就是构造无限次循环,在循环体中使用if语句判定是否达到结束循环的条件,当满足条件时,用break语句终止循环。
此时需要注意,在程序执行过程中,务必要有使if 语句中的表达式结果趋于False的语句,以避免程序陷入无限循环。
实例 4.4 Leibniz公式计算圆周率
历史上有许多计算圆周率pi的公式,其中,格雷戈里和莱布尼茨发现了下面的公式(可由等比数列求和公式变换并积分获得):
这个公式累加1项是4.00,累加2项是2.67,累加3项是3.47...趋进于圆周率值。编程对其各项进行累加,直到最后一项的绝对值小于10−6为止,输出此时的圆周率值。
公式中每一项的分母数字正好相差2,符号正负交替,可以利用循环结构求解。
因循环次数未知,但知道终止条件,所以可以用while加判断条件控制循环,条件为True时进入循环,在循环里使判断条件值发生改变,待判断条件值为False时,不再进入循环。
pi, i = 0, 1 # 设定pi初值0, i为分母变量名 sign = 1 # sign用于改变符号 while 1 / i >= 1e-6: # 条件表达式值为True时进入循环 pi = pi + sign * 1 / i # 累加,每次循环加上最后一项 sign = - sign # 每循环一次改变一次符号,实现正负项交替 i = i + 2 # 下一项分母比当前项大2,此语句也会使1/i值变小 print(pi * 4) # 乘4得以π的计算值,放循环外只输出一次
也可用while加True或一个结果为True的表达式,使循环判断条件永远为True,构建无限次循环。
在循环体中,判断循环控制条件1/i < 10−6
的结果为False时,用break结束循环,使循环可以在有限次数内结束。
此处brek作用是程序执行到此语句时,提前结束循环。
pi, i = 0, 1 # 设定pi初值0, i为分母变量名 sign = 1 # sign用于改变符号 while True: # 构建无限循环 if 1 / i < 1e-6: # 最末项大于等于10-6时 break # 结束循环 else: # 否则执行累加语句 pi = pi + sign * 1 / i # 累加,每次循环加上最后一项 sign = - sign # 每循环一次改变一次符号,实现正负项交替 i = i + 2 # 下一项分母比当前项大2 print(pi * 4) # 乘4得以π的计算值,放循环外只输出一次
输出:
3.141590653589692
这个公式简单而优美,但收敛的较慢。
增加计算的项数可以提高计算精度,将终止条件设为10−9
时可以提高3位数的精度,再想提高精度,时间就难以接受了。
3.141592651589258
编写一个程序,可以重复接收用户输入的学生姓名,当输入的姓名为非空字符串时,输出“欢迎你,***同学!”(***处为用户输入的学生姓名),当用户直接输入回车时(此时input()函数接收到的是空字符串)结束程序。
user_name = input() while user_name: # 当输入非空时执行循环 print(f'欢迎你,{user_name}同学!') # 输出 user_name = input() # 循环体内的输入语句可重复接收输入
# 可用无限循环方法,用break结束程序 while True: user_name = input() if user_name != '': # 当user_name为非空字符串时,即输入非空时 print(f'欢迎你,{user_name}同学!') # 输出 else: break
if user_name != '': 等价于: if user_name: 后者表示当user_name为True时,因为user_name是input()结果,必然是字符串,所以仅当user_name为空字符串时,即直接输入回车时为False,其他情况均为True。
while True: user_name = input() if user_name: # 当user_name为非空字符串时,即输入非空时 print(f'欢迎你,{user_name}同学!') # 输出 else: break
# 可用前面讲过的赋值表达式更简洁的实现 while user_name := input(): # 当输入非空时执行循环 print(f'欢迎你,{user_name}同学!') # 输出
实例 4.5 应用之二分法
对于区间[a,b]上连续不断且f(a)·f(b)<0的函数y=f(x),通过不断地把函数f(x)的零点所在的区间一分为二,使区间的两个端点逐步逼近零点,进而得到零点近似值的方法叫二分法。
当数据量很大适宜采用该方法。
采用二分法查找时,数据需是排好序的。
基本思想:
假设数据是按升序排序的,对于给定值key,从序列的中间位置k开始比较,
如果当前位置arr[k]值等于key,则查找成功;
若key小于当前位置值arr[k],则在数列的前半段中查找,arr[low,mid-1];
若key大于当前位置值arr[k],则在数列的后半段中继续查找arr[mid+1,high],
直到找到为止,时间复杂度:O(log(n))
二分法求平方根
设计一个用二分法计算一个大于或等于 1 的实数 n 的平方根的函数sqrt_binary(n),计算精度控制在计算结果的平方与输入的误差不大于1e-6。 注:初始区间取[0,n]
num = float(input()) # 输入一个大于1的数 low, high = 0, num # 初始化二分查找的上下界 while True: # 开始无限循环 x = (high + low) / 2 # 计算上下界的中点 if abs(x ** 2 - num) <= 1e-6: # 如果中点的平方与输入的数的差的绝对值小于等于1e-6 print(x) # 打印中点的值 break # 结束循环 elif x ** 2 - num < 0: # 如果中点的平方小于输入的数 low = x # 将中点设为新的下界 else: # 如果中点的平方大于输入的数 high = x # 将中点设为新的上界
import math def sqrt_binary(num): low, high = 0, num while True: x = (high + low) / 2 if abs(x ** 2 - num) <= 1e-6: return x elif x ** 2 - num < 0: low = x else: high = x num = float(input()) if num >= 1: print(sqrt_binary(num)) print(math.sqrt(num))
二分法求函数的零点
现有方程: f(x)=x5−15x4+85x3−225x2+274x−121p
已知f(x)在[1.5,2.4]区间有且只有一个根,用二分法求解该根。
n = int(input()) # 输入一个整数n,表示小数点后的位数 low, high = 1.5, 2.4 # 初始化二分查找的上下界 while True: # 开始循环 x = (low + high) / 2 # 计算上下界的中点 # 计算多项式的值 y = x ** 5 - 15 * x ** 4 + 85 * x ** 3 - 225 * x ** 2 + 274 * x - 121 if abs(y) < 1 * 10 ** -n: # 如果多项式的值的绝对值小于10的-n次方 print('{:.6f}'.format(x)) # 打印中点的值,保留6位小数 break # 结束循环 elif y < 0: # 如果多项式的值小于0 high = x # 将中点设为新的上界 else: # 如果多项式的值大于0 low = x # 将中点设为新的下界
def f(x): return x ** 5 - 15 * x ** 4 + 85 * x ** 3 - 225 * x ** 2 + 274 * x - 121 def bisection_method(low, high): while True: mid = (low + high) / 2 if abs(f(mid)) < 1 * 10 ** -n: return '{:.6f}'.format(mid) elif f(mid) < 0: high = mid else: low = mid if __name__ == '__main__': n = int(input()) Low, High = 1.5, 2.4 print(bisection_method(Low, High))
二分法猜数游戏
写一个1-1024之间的整数,猜测这个数字,每欠猜测后给出“大了”“小了”“猜中”的提示
import random x = random.randint(1, 1024) # 随机产生一个1-1024之间的整数 for i in range(10): # 允许猜10次 guess = int(input()) # 输入猜的整数 if guess == x: print('猜中了') # 猜中则终止循环 break elif guess > x: print('猜的数大了') else: print('猜的数小了') else: print('你用完了十次机会也没猜中哦')
3.5 Range
range 类型表示不可变的数字序列,通常用于在 for 循环中指定循环次数,是for循环中应用最广泛的可迭代对象之一。
range()是python中的一个内置函数,用于生成一系列连续的整数,创建一个整数列表,一般用在 for 循环中。
range()函数既可用于控制循环,又可以用等差数列中的数求解很多数学问题。
range()函数语法:
range(stop) # 0, 1, 2, 3, 4, …stop-1,初值为0,步长为1的等差数列 range(start, stop[, step]) # start, start+step, start+2*step…,步长为step的等差数列
start 和 step 是可选参数,缺省时,start=0, step=1。 range 生成的内容: r[i] = start + step * i 当 step 为正数,要求 i >= 0 且r[i] < stop; 当 step 为负数,要求 i >= 0 且 r[i] > stop。 range()具有一些特性:
1.要全部输出range()生成的序列,不能直接输出range(n)
可以用print(list(range(n))) 的方法,将生成的序列转成列表形式输出。
可以用print(tuple(range(n)))的方法,将生成的序列转成元组的形式输出。
可以用print(*(range(n)))将其内容解包输出。
print(range(5)) # 输出range(0, 5),此语句不能输出整数序列 print(list(range(5))) # 输出[0, 1, 2, 3, 4] print(tuple(range(5))) # (0, 1, 2, 3, 4) print(*range(5)) # 解包输出:0 1 2 3 4
2. start、stop、step都必须是整数,否则抛出TypeError异常。
当 stop 值小于start 值且步长为正数时,返回的序列为空。
print(range(3.5))
print(range(1, 8.0)) # 8.0是值等于整数8的浮点数,不是整数
print(list(range(8, 1))) # range(8, 1)返回空序列,转列表结果为空列表
3. 如果start参数缺省,默认值为0;
如果step参数缺省,默认值为1;
当试图设置step为0时,会抛出ValueError异常。
# start参数缺省,默认值为0 print(*(range(8))) # *将range对象解包,取出里面对象输出0 1 2 3 4 5 6 7 print(list(range(8))) # range(8)返回空序列,转列表结果为空列表[0, 1, 2, 3, 4, 5, 6, 7]
# step参数缺省,默认值为1 print(*(range(1, 8))) # 输出1 2 3 4 5 6 7 print(list(range(1, 8))) # 输出[1, 2, 3, 4, 5, 6, 7] # step值为2 print(*(range(1, 8, 2))) # 输出1 3 5 7
print(*(range(1, 8, 0))) # step为0时,会抛出ValueError异常
4. 当step是正整数时,产生的序列递增; 当step为负整数时,生成的序列递减。
print(*(range(1, 8, 2))) # step是正整数时,产生的序列递增 print(*(range(8, 1, -2))) # step为负整数时,生成的序列递减
5. range()函数产生一个左闭右开的序列
print(*(range(5))) # 输出0 1 2 3 4,包含左边界0,不包括右边界5 print(*(range(1, 5))) print(*(range(1, 5, 2)))
6. range()函数产生的是可迭代对象,不是迭代器,也不是列表类型。
可迭代对象(Iterable):
凡是可以作用于for循环的对象都是可迭代对象,可用collections模块的Iterable来判断。
迭代器(Iterator):
不仅可以作用于for循环,还能作用于next()函数的对象是迭代器,表示一个惰性计算的序列,可用collections模块的Iterator来判断。
from collections import Iterable, Iterator print(isinstance(range(8), Iterable)) # 查看是否为可迭代对象,结果 True print(isinstance(range(8), Iterator)) # 查看是否为迭代器,结果 False
for i in range(1, 10): print(i) # 分行输出1到9的数字
for i in range(1, 10): print(f'{i} x 1 = {1*i}', end=' ') # 输出乘法表的一行 # 1 x 1 = 1 2 x 1 = 2 3 x 1 = 3 4 x 1 = 4 5 x 1 = 5 6 x 1 = 6 7 x 1 = 7 8 x 1 = 8 9 x 1 = 9
7. range对象是不可变数据类型,可用索引、切片等操作获取其中部分数据,但不可修改其中的数据。
print(range(10)[2]) # 索引,根据序列返回一个元素,2 print(range(10)[2:5]) # 切片,返回序列的一部分,range(2, 5) range(10)[2] = 5 # 修改range中序号为2的元素的值,触发异常
惰性求值
range()函数相对于列表和元组的优点在于占用内存固定,占用内存较小,他仅存储start、stop和step值,在需要时通过计算生成序列中的每个值。
print(range(10))会输出对象range(0, 10),说明range采用了惰性求值的方式产生数据,使用print()函数打印range数据并不能直接看到range数据的具体元素。但可以通过将其转换为列表或元组的方式,查看其生成的具体数据。
range(10000)可以按顺序产生一组数字0 , 1, 2, 3, 4,… 9998, 9999,但并不会一次将这些数据生成放在内存中,只有使用到其中的某个值时,range才会产生该值,可以减少生成数据和将数据写入内存的时间开销,对于某些应用来讲效率会非常高。如下例所示:
for i in range(1000000000): # 存储的是range(1,1000000000)对象 if i < 5: print(i, end=' ') # 输出0 1 2 3 4,只产生这5个数字
range()函数常与for和控制变量一起使用,遍历range()数列中的数并控制循环次数。
for <variable> in range([start,]stop[,step]): <语句块>
variable 为循环控制变量,经常用i、j等表示,每次循环从range()产生的数列中依次取一个值。首次进入循环时,变量取最小的值,即start值,当start缺省时,取值为0,后面每次循环依次取前一个值加步长step的值,当step值缺省时,取前一个值加1。
3.6 for 循环
for循环一般用于循环次数可确定的情况下,一般也被称为遍历循环。
for 语句可以依据可迭代对象中的子项,按他们的顺序进行迭代。
这些可迭代对象包括:range、字符串、列表、集合、字典和文件对象等可迭代数据类型。
for循环基本结构如下:
for 循环变量 in 可迭代对象: 语句块
需要注意,for开头的语句末尾一定是冒号结尾,其后面至少有一行语句需要在缩进的语句块中。 缩进语句块中的语句重复执行多次,直到for语句遍历完可迭代对象。
程序执行时,从可迭代对象中逐一提取元素,赋值给循环变量,每提取一个元素执行一次语句块中的所有语句,总的执行次数由可迭代对象中元素(项)的个数确定。
实例 4.4 等差数列前n项和
输入一下正整数,计算从1到这个正整数(包括该数本身)的所有整数的和。
range(1,n+1) 可生成1,2,……,n的序列,所以等差数列前n 项和的问题,可以用range实现:
算法描述:
- 先设置一个初值为0的变量
- 遍历由range()产生的整数序列,每得到一个整数就加到变量上
- 输出变量的值,即为所有整数的和
n = int(input()) # 将输入的字符转成整型,例如输入 100 sum_of_i = 0 # 设累加容器初值为0 for i in range(1, n+1): # 遍历1,2,...n的数列 sum_of_i = sum_of_i + i # 将当前项加到累加容器上,注意此处要缩进,表示循环执行 print(sum_of_i) # 输出累加总和,5050
这个问题也可以直接用sum()函数结合range()函数来实现。将range()函数直接作为sum()函数的参数,可以直接获得range()函数所生成序列中全部元素的和。
例如计算从1到n的加和可以写为以下程序:
n = int(input()) # 将输入的字符转成整型,例如输入 100 print(sum(range(1, n + 1))) # sum()函数可返回序列参数的元素的和,输出 5050
print(sum(range(1, int(input()) + 1))) # sum()函数可返回序列参数的元素的和,输出 5050
改变range()函数中start、stop、step的值,便可以计算不同等差数列中连续n项和了。如:
print(sum(range(1, 100, 2))) # 100以内奇数的和2500 print(sum(range(0, 101, 2))) # 不超过100的偶数的和2550 print(sum(range(0, 101, 5))) # 不超过100的偶数5的整数倍的数的和1050 print(sum(range(50, 80, 5))) # 序列50 55 60 65 70 75的和为375
实例 4.5 计算阶乘
一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且0的阶乘为1,自然数n的阶乘写作n!
range(1,n+1) 可生成1,2,……,n的序列,阶乘是数列前n 项积的问题,可以用range实现:
算法描述:
- 先设置一个初值为1的变量
- 遍历由range()产生的整数序列,每得到一个整数就乘到变量上
- 输出变量的值,即为所有整数的积,也就是阶乘值
n = int(input()) # 将输入的字符转成整型,例如输入6 fact = 1 # 设阶乘初值为1,n为0时不进入循环,0的阶乘为1 for i in range(1, n + 1): # 遍历1,2,...n的数列 fact = fact * i # 将当前项加到累加容器上 print(fact) # 输出累加总和,720
math库中有一个用于计算阶乘的函数factorial(n),可以调用函数简化程序设计。
import math print(math.factorial(6)) # factorial() 为计算阶乘的函数,720
实例 4.6 棋盘放米
相传古代印度国王要褒赏他的聪明能干的宰相 ,问他需要什么?
宰相回答说:“国王只要在国际象棋的棋盘第一个格子里放1粒麦子,第二个格子里放2粒,第三个格子里放4粒,按此比例以后每一格加1倍,一直放到64格,我就感恩不尽,其他的我什么也不要了。”
请计算一下,共需多少粒麦子?按照普通大米600粒质量为50克计算,问共需麦子约多少亿吨?写出程序。
算法描述:
- 设定累加容器初值为0
- 循环获得每格的序号 i
2.1. 计算当前格的麦粒数,为2的 i 次幂
2.2. 将当前格的麦粒数加到累加容器上
3. 输出累加容器中麦粒总数
4. 计算重量,换算单位
total = 0 # 米粒总数初值 for i in range(64): # 迭代获取整数序列中的值 t = 2 ** i # 当前格中米粒数为2的i次幂 total = total + t # 累加各格中米粒数 print(total) # 输出米粒总数,18446744073709551615 x = 50/600 # 每粒米的质量,单位为克 total_weight = total * x * 1e-3 * 1e-3 * 1e-8 # 每粒米的质量从克转为千克再转为吨再转为亿吨 print(f'总质量为{total_weight:.2f}亿吨')
# 变量t只用一次,可不定义,直接将当前格米粒数量加上即可,减少不必要的变量定义可使程序更清晰 total = 0 # 米粒总数初值 for i in range(64): # 迭代获取整数序列中的值 total = total + 2 ** i # 当前格中米粒数为2的i次幂加到当前总数上 print(total) # 输出米粒总数,18446744073709551615
# 列表推导式方法,第6章学习 print(sum([2 ** i for i in range(64)]))
实例4.7 拉马努金法计算圆周率
前面提到的Leibniz公式计算圆周率的方法存在收敛慢的问题。拉马努金曾经提出过很多收敛速度极快的求π的公式,比如这个拉马努金在1914年发布的以他自己名字命名的著名公式可用于计算圆周率。
公式中的希腊字母∑,英文译音是Sigma, 表示数学中的求和号,主要用于求多项数之和,公式展开可以表示为k从0到n时各项的累加,可以用循环实现。这个公式收敛速度极快,累加3次时,就可以达到math.pi相同的精度。
from math import factorial # 导入math库中的factorial函数 n = int(input()) # 输入正整数表示累加项数,例如输出3 result = 0 # 累加初值为0 for k in range(n): # 重复执行n次累加 result = result + factorial(4 * k) * (1103 + 26390 * k) / (factorial(k) ** 4 * 396 ** (4 * k)) pi = 1 / (result*2 * 2 ** 0.5 / 9801) # 累加结果取倒数为圆周率值 print(pi) # 输出圆周率值,输出3.141592653589793
for循环可以多重嵌套使用,每增加一层循环多一层缩进,最内层循环体内的语句执行的次数为各重循环次数相乘。
如果循环语句的循环体中又出现循环语句,就构成多重循环结构。
for和while都支持多重循环,且可以混用。
一般常用的有二重循环和三重循环。循环层数越多,运行时间越长,程序越复杂,最内层程序语句执行的次数是各层循环次数的乘积。
枚举法
实例 4.8 百钱买百鸡
我国古代数学家张丘建在《算经》一书中提出的“百钱买百鸡”的数学问题,题目意思是1只公鸡5文钱、1只母鸡3文钱、3只小鸡1文钱。计算并输出有几组可能的解?
若用数学方法求解,3个未知量,只能列2个方程,不能直接求解。
在计算机领域,这个问题可以用枚举算法求解,枚举算法的思想是:
将问题的所有可能的答案一一列举,然后根据条件判断此答案是否合适,保留合适的,舍弃不合适的。
基本思路如下: (1)确定枚举对象、范围和判定条件。 (2)逐一枚举可能的解并验证每个解是否是问题的解。 算法步骤: (1)确定解题的可能范围,不能遗漏任何一个真正解,同时避免重复。 (2)判定是否是真正解的方法。 (3)为了提高解决问题的效率,使可能解的范围将至最小
我们先用一个规模较小的问题为例。日常用的皮箱的经常是三位的密码锁,当忘记密码时,可以采用逐位猜测的方法试出来密码,我们知道,3个数字的全部组合只有1000个,所以我们试1000次一定可以找到密码。
具体操作方法:
- 1.先固定第1位为0
- b. 第2位设0
- ⅰ. 第3位从0试到9
- c. 重复b并依次设1-9
- b. 第2位设0
- 2.重复1并依次设第1位为1-9
算法: 1.第1位数遍历从0到9的数字 2.第2位数遍历从0到9的数字 3.第3位数遍历从0到9的数字 4.比较当前的三位数是否与密码相同,若相同 5.输出当前三位数
import random # 导入随机数模块 keys = random.randint(100, 999) # 随机产生一个三位整数 print(keys) # 查看当前生成的随机数,每次结果不同,此处若密码为651 for i in range(10): # 猜测百位上的数字 for j in range(10): # 猜测十位上的数字 for k in range(10): # 猜测个位上的数字 # 三个数字拼接为一个三位整数,若此三位数与随机产生的相同,则找到密码 if i * 100 + j * 10 + k == keys: print(f'密码是{i}{j}{k}') # 密码是651
参考找回密码的程序,将问题规模扩大,遍历公鸡、母鸡和小鸡的数量都从1到100,一定可以找到所有正确的解。
python中的timeit()方法, 它用于获取代码的执行时间。该库将代码语句运行一百万次,并提供从集合中花费的最短时间。
%%timeit for cock in range(1, 101): # 公鸡数量不为0且小于或等于100 for hen in range(1, 101): # 母鸡数量不为0且小于或等于100 for chicken in range(1, 101): # 小鸡数量大于0小于等于100且是3的倍数 # 鸡的总数为100,钱的总数为100 if cock + hen + chicken == 100 and 5 * cock + 3 * hen + chicken // 3 == 100 and chicken % 3 ==0 : print(f'公鸡{cock}只,母鸡{hen}只,小鸡{chicken}只') # 遇到满足条件的数字组合就输出 # 57.4 ms ± 3 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
这是一个三重循环,最内层循环中的if语句的执行次数约为:100 * 100 * 100 = 100万次,这是一个很大的数字,计算时间开销也很大,算法的效率不高。 我们可以用“剪枝”这个方法,剪枝的思想就是剪掉不可能出现的情况,避免计算机多余的运算。
我们可以用range(3, 101, 3)产生3的整数倍的数列,小鸡数量是3的倍数,所以可取的值一定在这个数列中。
%%timeit # 100 * 100 * 33 = 33万次 for cock in range(1, 101): # 公鸡数量不为0且小于或等于100 for hen in range(1, 101): # 母鸡数量不为0且小于或等于100 for chicken in range(3, 101, 3): # 小鸡数量大于0小于等于100且是3的倍数 # 鸡的总数为100,钱的总数为100 if cock + hen + chicken == 100 and 5 * cock + 3 * hen + chicken // 3 == 100: print(f'公鸡{cock}只,母鸡{hen}只,小鸡{chicken}只') # 遇到满足条件的数字组合就输出 # 20.7 ms ± 626 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
实际上当公鸡和母鸡数量x, y 确定的情况下,小鸡的数量 z 可由100 – x - y计算,并不需要用循环进行遍历。可将其用两重循环实现求解,此时,最大循环次数为10000次,效率提高33倍。
python中的timeit()方法用于获取代码的执行时间。该库将代码语句运行一百万次,并提供从集合中花费的最短时间。
# %%timeit # 100 * 100 = 1万次 for cock in range(1, 101): # 公鸡数量不为0且小于或等于100 for hen in range(1, 101): # 母鸡数量不为0且小于或等于100 chicken = 100 - cock - hen # 小鸡数量可由公鸡和母鸡数量计算得到 if chicken % 3 == 0 and 5 * cock + 3 * hen + chicken // 3 == 100: print(f'公鸡{cock}只,母鸡{hen}只,小鸡{chicken}只') # 遇到满足条件的数字组合就输出 # 1.35 ms ± 41.5 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
由于要求每种鸡数量都不能为0,所以公鸡最多只能买19只,母鸡最多只能买32只,这样继续裁剪,减少外面两层循环的次数:
%%timeit # 19 * 32 = 608次 for cock in range(1, 20): # 公鸡数量不超过20 for hen in range(1, 33): # 母鸡数量不超过33 chicken = 100 - cock - hen # 小鸡数量可由公鸡和母鸡数量计算得到 if chicken % 3 == 0 and 5 * cock + 3 * hen + chicken // 3 == 100: print(f'公鸡{cock}只,母鸡{hen}只,小鸡{chicken}只') # 遇到满足条件的数字组合就输出 # 88.7 µs ± 5.1 µs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
从结果中可以发现这样的一个规律:
公鸡是4的倍数,母鸡是7的递减率,小鸡是3的递增率,为了确认这一规律,数学上推导一下这个不定方程:
x + y + z = 100
5x + 3y + z/3 = 100
消去z可得:7x + 4y = 100
由此可得:
y = 25 - (7/4)x
z = 75 + (3/4)x
因为0 < y < 100,且是自然数,则可得知 x 必为4的整数倍的正整数才能保证y 和 z 都是整数;
x 最大值必小于16 才能保证y 值为正数。
所以x值只能取4、8、12。这样只循环3次就可以找到所有可能的解。
可以继续优化代码提高效率:
%%timeit for cock in range(4, 16, 4): # cock 初值为4,小于16,步长为4 hen = 25 - cock // 4 * 7 # 整除“//”符号保证运算结果仍为整数 chicken = 75 + cock // 4 * 3 print(f'公鸡{cock}只,母鸡{hen}只,小鸡{chicken}只') # 遇到满足条件的数字组合就输出 # 15.9 µs ± 518 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
%%timeit import math ls = [] for num in range(3, 20000): if str(num) == str(num)[::-1] and math.factorial(num) % 7 == 0: ls.append(num) print(ls) # 715 ms ± 68.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%%timeit import math ls = [] for num in range(3, 20000): if math.factorial(num) % 7 == 0 and str(num) == str(num)[::-1]: ls.append(num) print(ls) # 715 ms ± 68.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
尽可能减少循环嵌套的层数,让代码趋于扁平,使逻辑更简单,更容易阅读、理解和维护代码。需多重循环求解时,可以将内层循环的功能定义成函数,将二重循环转换为两个一重循环,使代码逻辑更清晰。
迭代法
迭代法也称辗转法,是一种不断用变量的旧值递推新值的过程,跟迭代法相对应的是直接法(或者称为一次解法),即一次性解决问题。 迭代算法是用计算机解决问题的一种基本方法,它利用计算机运算速度快、适合做重复性操作的特点,让计算机对一组指令(或一定步骤)进行重复执行,在每次执行这组指令(或这些步骤)时,都从变量的原值推出它的一个新值,迭代法又分为精确迭代和近似迭代。 比较典型的迭代法如“二分法”和"牛顿迭代法”属于近似迭代法。
兔子繁殖问题
兔子从出生后第3个月起每个月都会生一对兔子,小兔子成长到第三个月后每个月又会生一对兔子。初始有一对小兔子,假如兔子都不死,用户输入一个月份数,计算并在一行内输出从1到n月每个月的兔子数量。 各月的兔子数量形成的数列是:
1,1,2,3,5,8,13,……
斐波那契数列以如下被以递推的方法定义:
F(1)=1
F(2)=1
F(n)=F(n - 1)+F(n - 2)(n ≥ 3,n ∈ N*)
计算a,b指向数据的和作为新的一项,同时将变量b指向新产生的数据项,将变量a指向倒数第二项,也就是原来变量b指向的那个数据,这个操作可以用同步赋值语句a,b=b,a+b实现。
n = int(input()) # int()将input()接收的字符串转整数,例如输入12 a, b = 1, 1 # 设定数列前两项的初值 for i in range(n): # i 只用于控制循环次数,n值为12 print(a, end=' ') # 每次循环输出一个值,不换行 a, b = b, a + b # 迭代,b的值赋给a,把a,b的和赋值给b # 1 1 2 3 5 8 13 21 34 55 89 144
迭代法开平方
求平方根的迭代公式:
x1=1/2*(x0+a/x0)
算法:
1.先自定一个初值x0,作为a的平方根值,在我们的程序中取a/2作为x0的初值;利用迭代公式求出一个x1。此值与真正的a的平方根值相比,误差很大。
2.把新求得的x1代入x0中,准备用此新的x0再去求出一个新的x1. 3.利用迭代公式再求出一个新的x1的值,也就是用新的x0又求出一个新的平方根值x1,此值将更趋近于真正的平方根值。 4.比较前后两次求得的平方根值x0和x1 4.1 如果它们的差值小于我们指定的值,即达到我们要求的精度,则认为x1就是a的平方根值,去执行步骤5; 4.2 否则执行步骤2,即循环进行迭代。
import math x = int(input()) # 输入整数 x0 = x / 2 # 初值 x1 = (x0 + x / x0) / 2 # 迭代公式 while True: # 迭代精度 x0 = x1 x1 = (x0 + x / x0) / 2 if abs(x0 - x1) < 1e-5: break print(x1) # 2.23606797749979 print(math.sqrt(x)) # 2.23606797749979
牛顿迭代法
牛顿迭代法解下列方程的解: x4−3∗x3+1.5∗x2+−4=0
牛顿迭代法是一种可以用来快速求解函数零点的方法,关键问题:切线是曲线的线性逼近。 牛顿迭代法的本质是借助泰勒级数,从初始值开始快速向零点逼近。 在经过多次迭代后,我们就可以得到一个距离零点非常接近的交点。
# 定义初始值和参数 x0 = 0.2 max_iter = 50 tol = 1e-5 # 开始迭代 for i in range(max_iter): # 计算f(x)和f'(x) fx = x0 ** 4 - 3 * x0 ** 3 + 1.5 * x0 ** 2 + -4 fx_first_order = 4 * x0 ** 3 - 9 * x0 ** 2 + 3 * x0 # 计算新的x值 x = x0 - fx / fx_first_order # 检查是否满足精度要求 if abs(x - x0) < tol: print(f'经{i}次迭代,估计参数值是{x}') break # 更新x0 x0 = x else: print('达到最大迭代次数,无法收敛')
def f(x): """f的方程""" return x ** 4 - 3 * x ** 3 + 1.5 * x ** 2 + -4 def f_first_order(x): """f的一阶导数""" return 4 * x ** 3 - 9 * x ** 2 + 3 * x def get_root(x0, max_iter=50, tol=1e-5): """将初始值浮点化""" p0 = 0.2 # 初始化迭代值 for i in range(max_iter): p = p0 - f(p0) / f_first_order(p0) # f的一阶导数不能为0 if abs(p - p0) < tol: # 如果小于精度值则退出迭代 return f'经{i}次迭代,估计参数值是{p}' p0 = p print('达到最大迭代次数,无法收敛') if __name__ == '__main__': print(get_root(0)) # 经12次迭代,估计参数值是2.648936536182061
蒙特卡洛模拟
蒙特卡洛(Monte Carlo)方法是由数学家冯·诺伊曼提出的,诞生于上世纪40年代美国的“曼哈顿计划”。蒙特卡洛是一个地名,位于赌城摩纳哥,象征概率。蒙特卡洛方法的原理是通过大量随机样本,去了解一个系统,进而得到所要计算的值。
蒙特卡洛法计算圆周率
用蒙特卡洛方法计算圆周率π的原理如下:一个边长为2r的正方形内部相切一个半径为r的圆,圆的面积是πr2,正方形的面积为4r2,二者面积之比是π/4,因为比值与r大小无关,所以可以假设半径 r的值为1。
在这个正方形内部,随机产生n个点,坐标为(x,y),当随机点较多时,可以认为这些点服从均匀分布的规律。计算每个点与中心点的距离是否大于圆的半径(x2+y2>r2),以此判断是否落在圆的内部。统计圆内的点数c,c与n的比值乘以4,就是π的值。理论上,n越大,计算的π值越准,但由于随机数不能保证完全均匀分布,所以蒙特卡洛法每次计算结果可能不同。 编程实现用蒙特卡洛方法计算π值,为了自动测评的需要,请先读入一个正整数sd作为随机数种子,并要求使用 x,y = random.uniform(-1,1) , random.uniform(-1,1) 语句来生成随机点的坐标值。
import random # 输入正整数,表示产生点数量 times = int(input()) # 计数器,落在圆内的点数,初值为0 hits = 0 for i in range(times): x, y = random.uniform(-1, 1), random.uniform(-1, 1) # 计算坐标(x,y)到原点的距离,小于半径则在圆内 if x**2 + y**2 < 1: hits += 1 # 计算并输出圆周率值,浮点数 pi = (hits / times) * 4 print(pi)
蒙特卡洛法计算百钱百鸡问题
import random def monte_carlo_cock(num): answer = [] for i in range(num): hen = random.randint(1, 20) cock = random.randint(1, 33) chicken = 100 - cock - hen if chicken % 3 == 0 and 5 * cock + 3 * hen + chicken // 3 == 100 : group = [cock, hen, chicken] if group not in answer: answer.append(group) return answer if __name__ == '__main__': result = monte_carlo_cock(10000) for x in result: print('公鸡{}只,母鸡{}只,小鸡{}只'.format(*x))
实例 4.9 圆周率查询统计
数据文件下载: 3200wpi.txt
圆周率日是一年一度的庆祝数学常数π的节日,时间被定在3月14日。通常是在下午1时59分庆祝,以象征圆周率的六位近似值3.14159,有时甚至精确到26秒,以象征圆周率的八位近似值3.1415926;习惯24小时记时的人在凌晨1时59分或者下午3时9分(15时9分)庆祝。全球各地的一些大学数学系在这天举办派对。
由于圆周率小数位是无限的,那么,这里面会不会出现任意的数字组合呢?我们的生日、密码之类的数字能不能在圆周率中找到呢?
事实上,较短的数字组合很容易在圆周率中找到,而且还会重复出现多次,例如,000000第一次出现在1,699,927位,第二次出现在2,328,783位。123456首次出现于第2,458,885位,第二次出现于第3,735,793位。任意的6位数字组合,都出现在圆周率小数位的前1500万。
据说圆周率中能找到所有人生日、银行卡密码和手机号,尝试一下,看你的生日或手机号在圆周率前3000万位中是否存在?
birthdate = input() # 输入自己的生日或其他字符串 n = len(birthdate) # 获取输入的字符串的位数 pi = open('/data/bigfiles/3200wpi.txt').read() # 读取文件中的圆周率为字符串 # print(len(pi)) # 去掉注释可输出文件中圆周率位数 for i in range(len(pi)): # 逐个遍历圆周率中的字符序号 if birthdate == pi[i: i + n]: # 如果从当前字符向后的n个字符构成的字符串与输入的字符串相同 print(i, i + n) # 输出这个字符串的起止位置序号 # print(pi[:i + n]) # 输出到匹配字符串为止的圆周率,此语句在本地运行,不要在平台上运行 # break # 若去掉注释,匹配到一个就终止循环
输入: 5201314 输出: 2823258 2823265 25808025 25808032 26170648 26170655 输入 13297966265 输出: 22822747 22822758
a, b, c, d, e = 0, 0, 0, 0, 0 # 累加器清0 pi = open('/data/bigfiles/3200wpi.txt').read() # 读取文件中的圆周率为字符串 for i in pi: # 逐个遍历圆周率中的字符序号 if i == '0': a = a + 1 elif i == '1': b = b + 1 elif i == '2': c = c + 1 elif i == '3': d = d + 1 elif i == '4': e = e + 1 # 分别统计输出0,1,2,3,4的数量 print(a, b, c, d, e) # 3355091 3355566 3356627 3355076 3357258
读文件统计成绩平均值
/data/bigfiles/his.txt
fr = open('/data/bigfiles/his.txt') # 打开文件,创建一个可遍历的文件对象 total_score = 0 # 总成绩置0 count = 0 # 计数器置0 for score in fr: # 遍历文件,每次取到文件中的一行,字符串类型 total_score = total_score + float(score) # 字符串的成绩转数值类型,加到总成绩上 count = count + 1 # 成绩数量计数 avg_score = total_score / count # 计算平均成绩 print(avg_score) print(f'{avg_score:.2f}') # 格式化输出
3.7 流程跳转语句
3.7.1 pass 语句
pass相当于一个空语句,他不执行任何操作,可用在语法上需要语句,却不需要执行任何操作时,如pass 用作函数或条件语句体的占位符,让你保持在更抽象的层次进行思考。pass 会被默默地忽略:
while True: pass # 忙-等待键盘中断
也常用作为类、函数或条件体的占位符,或者创建占位程序、保证格式完整、保证语义完整等。
def initlog(): pass # 占位,提醒这是一个待完成的函数定义
3.7.2 continue
continue语句应用于while或for...in循环语句中。
一般置于条件判定语句块中,当满足某条件时触发该语句的执行。
作用是跳过跳过本次循环中continue后面剩余语句的执行,提前进入下一次循环。
实例 4.9 统计平均成绩
现有一个读文件切分得到的列表,其元素是表示一位同学各门课程成绩的字符串,空字符串表示该门课程因缓考原因导致成绩缺失,请计算并输出该同学的平均成绩。
我们知道,缓考导致的成绩缺失并不表示成绩为0,所以计算平均成绩时,应该略过缺失的成绩,只统计正常参加考试获得的成绩,这种情况下,就可以用continue跳过本次循环中剩余程序语句。
# 读成绩文件得到各门课程成绩,空字符串表示选课但无成绩 score = ['69', '85', '88', '', '100', '76', '', '95'] total = 0 # 总分初值 amount = 0 # 有成绩的课程数量初值 for x in score: # 遍历列表,x每次取值为一个字符串 if x == '': # 若当前取值为空字符串 continue # 跳过当前循环剩余语句,开始下一循环 total = total + int(x) # 字符串转整数,并累加到总成绩上 amount = amount + 1 # 有成绩课程数量增加1 avg_score = total / amount # 计算平均成绩 print(avg_score) # 忽略无成绩课程的平均分85.5
可以改变判定条件为当前成绩是非空字符串时进行累加,避免使用continue。
实际上,python中,大部分使用continue 的情况都可以通过改变条件回避其使用。
score = ['69', '85', '88', '', '100', '76', '', '95'] total = 0 # 总分初值 amount = 0 # 有成绩的课程数量初值 for x in score: # 遍历列表,x每次取值为一个字符串 if x: # 若当前取值为非空字符串时值为True total = total + int(x) # 字符串转整数,并累加到总成绩上 amount = amount + 1 # 有成绩课程数量增加1 avg_score = total / amount # 计算平均成绩 print(avg_score) # 忽略无成绩课程的平均分85.5
3.7.3 break 语句
break语句应用于while或for循环语句中,一般置于条件判定语句块中,当满足某条件时触发该语句的执行。 其作用是在循环次数未达到设定次数或未达到循环终止条件时跳出循环,提前结束语句所在层次的循环。
例如枚举时,找到一个满足条件的结果就终止循环。
如果一个 for...in 循环被 break 所终结,该循环的控制目标会保持其当前值。
示例4.10 加法训练机
random模块random.randint(m, n)可以随机产生一个n, m之间的整数,利用这个函数产生两个20以内的正整数a和b,屏幕上输出“a + b = ”,用户计算并输入计算结果,计算机评测计算是否正确。
结果正确时输出“恭喜你,回答正确”,回答错误时输出“回答错误,你要加油哦!”。
编写一个程序,重复产生两个整数做计算练习,直到用户不再计算,直接输入回车时,输出“练习结束”并结束程序。
import random # 导入随机数模块 while True: # 无限循环,每次产生一个新问题 a = random.randint(1, 20) # 随机产生一个20以内的整数 b = random.randint(1, 20) # 随机产生一个20以内的整数 print(f'{a} + {b} = ', end='') # 格式化输出 result = input() # 用户输入计算结果字符串 if result == '': # 如果直接输入回车(接收到空字符串) print('练习结束') break # 结束循环 elif int(result) == a + b: # 若输入数字等于a+b的值 print('恭喜你,回答正确') else: print('回答错误,你要加油哦!')
实例 4.11 自身以外的最大因数
输入一个大于1的整数,编程输出其自身以外的最大因数。
若一个数对i取模的结果为0,则i 是这个数的因数。
一个整数的因数可能有很多个,可以用遍历的方法逐一判定。
本题要求找最大因数,为提高效率,可以从大到小进行判定,找到的第一个因数时便为最大因数。
输出该数后,用break结束循环,不再进行判定。
num = int(input()) # 输入一个整数,例如:100 for i in range(num - 1, 0, -1): # 遍历小于num且大于1的整数 if num % i == 0: # 如num可被i整除,则i为num的因数 print(i) # 输入100时,输出50 break # 中止循环,退出循环
break只能提前结束当前循环。
当循环层数多于一层时,不能直接用break跳出多重循环。
一般的方法可以用一个标记变量,用break退出一重循环后,根据标记变量的值决定是否继续退出外层循环。
实例 4.12 百钱买百鸡进阶
公鸡五元一只,母鸡三元一只,小鸡三只一元。
用户输入想买的鸡的数量和付出的钱数,计算公鸡、母鸡和小鸡的数量。
如果有解,输出公鸡最少,小鸡最多的一组; 如果无解则输出“无解”。
题目要求如果有解,输出公鸡最少,小鸡最多的一组,可以按公鸡数量由少到多遍历,找到一组解后就结束程序。
题目需要两层循环,一个break只能结束一层循环,所以可以为内层和外层循环各匹配一个else子句。
若内层循环没有遇到break而正常结束,则执行匹配的else(第8行)中的continue(第9行),从而达到跳过内层循环中剩余语句(外层循环中位于第10行的break)的效果。而且因为外层循环未遇到break,会执行与for匹配的else及其输出子句。 若内层循环遇到break,则会跳过内层循环匹配的else(第8行)及其子句continue,执行张10行的break,这时则会跳过第11行的else及其用于输出的子句。
num, money = map(int, (input().split())) # 切分空格分隔的输入转整型 for cock in range(1, num): # 从小到大遍历公鸡数量 for hen in range(1, num): # 从小到大遍历母鸡数量 chicken = num - cock - hen # 小鸡数量计算得到 if chicken % 3 == 0 and chicken > 0 and 5 * cock + 3 * hen + chicken // 3 == money: print(cock, hen, chicken) # 输出找到的第一组解 break # 结束内层循环 else: # 如果内层遇到break,跳过else语句: continue # 内层未遇到break时执行,跳过当前循环中后续的break语句 break # 内层遇到break时,执行此语句,结束外层循环并跳过下一个else分支 else: # 如果外层循环未执行到break,表明无解: print('无解')
else与 continue 及break,所以可以在找到解后设定一个标记,根据标记的值决定是否结束外层循环。
也可以根据标记的值是否发生改变判定是否找到了解。
num, money = map(int, (input().split())) # 切分空格分隔的输入转整型 flag = False # 设定标记,False为无解,或solution = '无解' for cock in range(1, num): # 从小到大遍历公鸡数量 for hen in range(1, num): # 从小到大遍历母鸡数量 chicken = num - cock - hen # 小鸡数量计算得到 if chicken % 3 == 0 and chicken > 0 and 5 * cock + 3 * hen + chicken // 3 == money: flag = True # 找到解后改变标记,或solution = '有解' print(cock, hen, chicken) # 输出找到的第一组解 break # 结束内层循环 if flag: # 根据flag值是否为True判断是否找到解,或if solution == '有解' break # 找到解时结束外层循环,否则继续执行外层循环 if not flag: # 两层循环都结束时,如not flag值为True,表明未找到解,或if solution == '无解' print("无解")
程序中有两重循环,在内层循环中找到一个满足条件的解并输出后,按题目要求应该终止程序的执行。
提前终止循环可以用break关键字,但该关键字只能终止当前层次的循环,无法终止外层循环的执行。
外层循环不能无条件终止,需要先判断是否已经在内层中找到了满足条件的解。
所以可以在内层找到解时设定一个标记,上述程序中设置变量flag作为标记,一旦找到了满足条件的解,就改变flag变量的值。
在外层循环中根据flag的值是否发生了变化判定是否找到了解,一旦找到了一个满足条件的解,就用break提前终止外层循环。
3.7.4 else子句
Python中的 for...in 和 while 循环都有一个可选的else子句,在循环迭代正常完成之后执行(包括执行0次)。
else 子句可以省略,这个子句只有当循环以正常方式结束时,else子句中的语句才能被执行。
如果在循环语句块中遇到break语句跳出循环或遇到return语句结束程序,则不会执行else 部分。
在 for 循环中,else 子句会在循环成功结束最后一次迭代之后执行。
在 while 循环中,它会在循环条件变为假值后执行。无论哪种循环,如果因为 break 而结束,那么 else 子句就不会执行。
else 子句用于循环时比起 if 语句的 else 子句,更像 try 语句的。try 语句的 else 子句在未发生异常时执行,循环的 else 子句则在未发生 break 时执行。 。
实例 4.13 输出小于n的素数
用户输入一个大于 1 的正整数n,计算并输出小于 n 的素数。
素数也称为质数,是指在大于1的自然数中,除了1和它本身以外不再有其他因数的自然数。
判定一个整数 n 是否是素数有两个条件:
一是这个数 n 是大于1的;
二是这个数 n 不能被从2到n-1中的任意一个数整除,只要存在2到n-1之间的因子,那么n就不是素数。
算法描述: 1. 从n到1逆序遍历: a. 判定从 2 到 i-1 之中是否还存在能整除的数: ⅰ. 如果存在一个能整除的数字: 1. 则数字 i 必然不是素数,不需要继续判断,用break跳出循环 ⅱ. 如果顺利完成从 2 到 i-1 的遍历,未发现能整除的数,说明该数是素数 1. 输出这个数
n = int(input()) # 输入一个正整数,例如输入50 for i in range(2, n): # 0,1不是素数,产生从2 到 n 的整数序列 for j in range(2, i): # 产生从2到i-1序列,j逐个取值为2到i-1 if i % j == 0: # 若2到i-1中存在能被i整除的数,i不是素数 break # 存在整数因子时执行break语句并跳过else语句的执行 else: # 与for对齐,循环中未执行到break时执行此语句 print(i, end=' ') # 输出2 3 5 7 11 13 17 19 23 29 31 37 41 43 47
用else语句改写实例 4.12 百钱买百鸡进阶,了解break和continue的使用:
num, money = map(int, (input().split())) # 切分空格分隔的输入转整型 flag = False # 设定标记,False为无解,或solution = '无解' for cock in range(1, num): # 从小到大遍历公鸡数量 for hen in range(1, num): # 从小到大遍历母鸡数量 chicken = num - cock - hen # 小鸡数量计算得到 if chicken % 3 == 0 and chicken > 0 and 5 * cock + 3 * hen + chicken // 3 == money: flag = True # 找到解后改变标记,或solution = '有解' print(cock, hen, chicken) # 输出找到的第一组解 break # 结束内层循环 if flag: # 内层未遇到break时执行continue break else: # 外层循环未遇到break时执行缩进语句块 print("无解")
while…else用法类似于for…else,仅当while因条件为假而退出(即没有被break中断)时运行else分支下的语句块。
实例 4.14 自除数
一个不含0的多位数,如果它能被它的每一位除尽,则它是一个自除数。
例如128是一个自除数,因为128能被1、2、8整除。
编写函数selfDivisor(num)判断num是否为自除数,使用该函数输出不大于N的所有自除数。
(注意,含有数字0的数不是自除数)
需要注意的是,每次都要从最小的质数开始尝试除,例如360可以除2、除2、除2,被2整除3次才得到一个不能被2整除的45,然后再除3,除3,最后得到质数5。
num = int(input()) # 输入一个多位正整数 for c in str(num): # 对数字num中的每位数字进行遍历 if num % int(c) != 0: # 测试num的每一位是否是num的因子 print('不是自除数') # 如果存在不能整除的数,则不是自除数 break # 提前结束循环 else: # 如果for遍历顺利结束,未遇到break,则执行else子句 print('是自除数')
测试用例: 128 是自除数 129 不是自除数
3.8 异常处理
3.8.1 异常
异常是在程序执行过程中发生的一个事件,该事件会影响程序的正常执行。
一般情况下,在Python无法正常处理程序时或者说程序运行时候发生错误而没有被处理时就会发生一个异常。
这些异常会被Python中的内建异常类捕捉, 异常的类型有很多,在前面的学习过程中,遇到过SyntaxError、 NameError 、TypeError、 ValueError等多个错误提示信息,这些都是异常。
# 除数为0时,触发除零异常 a = 8 b = 0 print(a / b)
# 对象名字拼写错误,变量名未定义异常 pi = 3.14 # 圆周率 diameter = 4 # 直径 area = pi * ((dimeter / 2) ** 2) # 计算圆的面积
当程序发生异常时需要捕获他并进行一些处理,使其平稳结束,否则程序会终止执行甚至直接崩溃。
本节主要学习异常的一些处理方法和利用异常进行程序设计。
在程序设计过程中,要尽可能考虑全面,避免类似异常的存在,同时,尽可能对可能产生的异常进行处理,使程序具有更好的健壮性和容错性,避免程序崩溃。
也可以利用异常处理的方法实现程序的不同的功能。
Python中有许多内置的异常,有一个内置异常的完整层次结构,每当解释器检测到某类错误时,就能触发相对应的异常。
在程序设计过程中,可以编写特定的代码,专门用于捕捉异常,如果捕捉到某类异常,程序就执行另外一段代码,执行为该异常定制的逻辑,使程序能够正确运行,这种处理方法就是异常处理。
3.8.2 try…except子句
在Python中,可以使用try、except、else和finally这几个关键词来组成一个包容性很好的程序,通过 捕捉和处理异常,加强程序的 健壮性。用try可以检测语句块中的错误,从而让except语句捕获异常信息并处理。
try…except语法如下:
try: <语句块1> # 需要检测异常的代码块 except <异常名称1>: <语句块2> # 如果在try部份引发了异常名称1时执行的语句块 [except <异常名称2>: <语句块3>] # 如果在try部份引发了异常名称2时执行的语句块 [else: <语句块4>] # 没有异常发生时执行的语句块 [finally: <语句块5>] # 无论是否触发异常都要执行的语句块
except语句和finally语句都不是必须的,但是二者必须要有一个,否则就没有try的意义了。
except语句可以有多个,Python会按except语句的顺序依次匹配指定的异常。
1. 程序首先执行try子句
a. 如果try里面的程序没有触发异常 ⅰ. 跳过except子句 ⅱ. 执行else里面的语句
b. 如果在执行try子句的过程中发生异常 ⅰ. 根据错误类型选择执行对应的except里面的语句,这里面可以是错误信息或者其他的可执行语句。 ⅱ. except可以有多个,分别用于处理不同类型的异常 ⅲ. 每个except只捕捉一个异常,一旦捕捉到异常就不会再进入其他except语句块 ⅳ. except 子句 可以用带圆括号的元组来指定多个异常,except (RuntimeError, TypeError, NameError): ⅴ. 如果发生的异常与 except 子句 中指定的异常不匹配,则它是一个 未处理异常,执行将终止并输出error消息。 ⅵ. except 或 else 子句执行期间也会触发异常,该异常会在 finally 子句执行之后被重新触发 c. 若有finally子句,无论是否发生异常,都执行finally语句块 ⅰ. finally放在最后,其内容通常是做一些后事的处理 ⅱ. 比如关闭文件、资源释放之类的操作 ⅲ. 如果 finally 子句中包含 break、continue 或 return 等语句,异常将不会被重新引发 ⅳ. finally语句块是无论如何都要执行的 ⅴ. 如果执行 try 语句时遇到 break,、continue 或 return 语句,则 finally 子句在执行 break、continue 或 return 语句之前执行。 ⅵ. 如果 finally 子句中包含 return 语句,则返回值来自 finally 子句的某个 return 语句的返回值,而不是来自 try 子句的 return 语句的返回值。
a, b = map(float, input().split()) try: print(f'{a} / {b} = {a / b:.2f}') except ZeroDivisionError: print('除数为0,不能做除法运算')
实例4.14 四则运算
随机产生两个整数,输出其加、减、乘、除的结果,结果为浮点数时保留小数点后2位。
题目很简单,下面代码可以完成计算,但考虑到随机生成数字时,b的值可能会取到整数0,此时除数为0,会触发异常(小概率事件,需多次运行,但理论上一定会出现这个问题。)
import random a = random.randint(0, 20) # 随机产生一个20以内的整数 b = random.randint(0, 3) # 随机产生一个3以内的整数 sign = random.choice('+-*/') # 随机产生一个运算符号 print(f"{a} + {b} = {a + b}") print(f"{a} - {b} = {a - b}") print(f"{a} * {b} = {a * b}") print(f"{a} / {b} = {a / b:.2f}")
注意到0不能做除数,所以当第二个数字为0时,程序要能够对异常输入进行处理,使程序不至于因除零异常而崩溃。
可以将可能会触发异常的语句“print(f"{a} / {b} = {a / b:.2f}")”放到try的语句块中,一旦触发异常,程序将执行except下面的语句块中的语句。
import random a = random.randint(0, 20) # 随机产生一个20以内的整数 b = random.randint(0, 3) # 随机产生一个3以内的整数 sign = random.choice('+-*/') # 随机产生一个运算符号 print(f"{a} + {b} = {a + b}") print(f"{a} - {b} = {a - b}") print(f"{a} * {b} = {a * b}") try: print(f"{a} / {b} = {a / b:.2f}") except ZeroDivisionError: print('除数为0,不能做除法运算')
此题中非常肯定的知道只有当产生的 “b”值为“0”时,才会陷入异常,这种问题明确的情况下,尽可能用if…else 语句进行处理。
import random a = random.randint(0, 20) # 随机产生一个20以内的整数 b = random.randint(0, 5) # 随机产生一个5以内的整数 sign = random.choice('+-*/') # 随机产生一个运算符号 print(f"{a} + {b} = {a + b}") print(f"{a} - {b} = {a - b}") print(f"{a} * {b} = {a * b}") if b == 0: print('除数为0,不能做除法运算') else: print(f"{a} / {b} = {a / b:.2f}")
Python允许在一个程序里同时对多类异常进行捕捉,触发哪个异常就执行哪个异常对应的语句。
表3.6列出了Python中常见的异常名称及其描述,可以参考Python文档查看所有异常类及其子类。
异常名称 | 描述 |
---|---|
Exception | 常规异常的基类,可以捕获任意异常 |
SyntaxError | 语法错误 |
NameError | 未声明/未初始化的对象(没有属性) |
SystemError | 一般的解释器系统错误 |
ValueError | 传入无效的参数,或传入一个调用者不期望的值,即使值的类型是正确的 |
IndentationError | 缩进错误(代码没有正确对齐) |
ImportError | 导入模块/对象失败(路径问题或名称错误) |
ModuleNotFoundError | 模块不存在 |
ZeroDivisionError | 除(或取模)零 |
OverflowError | 数字运算超出最大限制 |
AttributeError | 对象没有这个属性 |
IndexError | 索引超出序列边界,如x只有10个元素,序号为0-9,程序中却试图访问x[10] |
KeyError | 映射中没有这个键(试图访问字典里不存在的键) |
TypeError | 对类型无效的操作 |
TabError | Tab和空格混用 |
RuntimeError | 一般的运行时错误 |
try: import turtle # import tutle 时输出“模块名称有误” size = eval(input()) print(size) # 参数写成sizee时会输出“变量未定义” turtle.circle(size) turtle.done() # done写成one时,会输出“属性不存在” except ModuleNotFoundError: # 遇到不同类型的异常给出不同的错误提示 print('模块名称有误') except NameError: print('变量未定义') except AttributeError: print('属性不存在') except SyntaxError: print('存在语法错误')
Python内置了的“Exception”类可以捕捉到所有内置的、非系统退出的异常,以及所有用户定义的异常。当需要输出程序遇到的异常时,可以使用以下方法:
try: import tutle # 修改为import tutle 后运行时输出No module named 'tutle' size = eval(input()) print(size) # size写成sizee时输出name 'sizee' is not defined turtle.circle(size) # 写成circe时输出 module 'turtle' has no attribute 'circe' turtle.done() except Exception as e: print(e)
3.8.3 else子句
没有触发异常时执行的语句
3.8.4 finally子句
如果try中的异常没有在“Exception”中被指出,那么系统将会抛出默认错误代码(Traceback),并且终止程序,接下来的所有代码都不会被执行。
但如果有finally关键字存在,则会在程序抛出默认错误代码之前,执行finally中的语句。
这个方法在某些必须要结束的操作中颇为有用,如释放文件句柄,或释放内存空间等。
s = ''' 静夜思 李白 床前明月光, 疑是地上霜。 举头望明月, 低头思故乡。 ''' try: file = open('test.txt', 'w', encoding='utf-8') # 以“写”模式打开文件 file.write(s) # 写入s中的字符串 file.seek(0) # 文件指针回到文件开头 for line in file: # 遍历逐行读文件 print(line, end='') # 逐行输出文件内容 # file.close()如果放在此处,如果前面遇到异常,将无法关闭文件 except: print('文件读写权限错误') finally: file.close() # finally中的语句无论是否触发异常都会被执行,可确保文件关闭
3.8.5 异常处理的应用
在一些特殊情况下,可以应用异常来实现一些特定的功能。例如正整数A+B的问题,利用其他语言实现可能需要近100行代码,而用Python结合异常处理来实现,仅需不到20行就可以实现。
实例 4.15 正整数A+B
题的目标很简单,就是求两个正整数A和B的和,其中A和B都大于0。稍微有点麻烦的是,输入并不保证是两个正整数。
输入在一行给出A和B,其间以空格分开。
问题是A和B不一定是满足要求的正整数,有时候可能是超出范围的数字、负数、带小数点的实数、甚至是一堆乱码。
我们把输入中出现的第1个空格认为是A和B的分隔。题目保证至少存在一个空格,并且B不是一个空字符串。
如果输入的确是两个正整数,则按格式“A + B = 和”输出。如果某个输入不合要求,则在相应位置输出“?”,显然此时和也是“?”。
a, b = input().split(' ', maxsplit=1) # 根据空格切分输入为列表,只切分一次 try: a = int(a) # 列表第一个元素转整型,转换类型不成功则触发异常 if a <= 0: # 若转换成功,说明是整数,再判定是否为负数 a = '?' except ValueError: a = '?' # 当异常被触发时,表明不是整数,用“?”代替 try: b = int(b) # 列表第二个元素转整型,转换类型不成功则触发异常 if b <= 0: # 若转换成功,说明是整数,再判定是否为负数 b = '?' except ValueError: b = '?' # 当异常被触发时,表明不是整数,用“?”代替 if a == '?' or b == '?': print(f'{a} + {b} = {"?"}') # 存在非正整数时按格式输出 else: print(f'{a} + {b} = {a + b}') # 均为正整数时按格式输出
虽然try...except可以捕捉和处理程序中的异常,但不能过于依赖这种方法。在程序设计过程中,首先应该尽可能排除语法错误与逻辑错误,防御性方式编码比捕捉异常方式更好,应尽量采取这种编程方式,提升性能并且使程序更健壮。
不要试图用try语句解决所有问题,这将会极大的降低程序的性能。只有在错误发生的条件无法预知的情况下,才使用try...except进行处理。
在程序设计过程中,一般情况下异常处理与程序主要的功能是没有关系的,过多的应用异常处理,会导致代码可读性变差。要尽量减少try/except语句块中的代码量,try语句块的体积越大, 期望之外的异常就越容易被触发,越容易隐藏真正的错误,从而带来严重后果。
使用finally子句来执行那些无论try语句块中有没有异常都应该被执行的代码,常用于终止处理程序,这对于清理资源常常很有用,例如关闭文件。
3.9 match 语句
Python 3.10中新增加的一个语句。
match语句接受表达式并将其值与作为一个或多个case块给出的连续模式进行比较。
只有第一个匹配的模式被执行,它还可以将组件(序列元素或对象属性)从值提取到变量中。
最简单的形式是将主题值与一个或多个文本进行比较:
def http_error(status): """根据参数中给定的状态码,返回http出错提示信息""" match status: case 400: return "Bad request" # 错误请求 case 401 | 403: # 可匹配多个条件 return "Not allowed" # 未授权或禁止访问该资源 case 404: return "Not found" # 找不到请求的资源 case 408: return "Request Timeout" # 请求超时 case _: return "Something's wrong with the internet" print(http_error(400)) # Bad request print(http_error(404)) # Not found print(http_error(408)) # Request Timeout
raise 语句
raise是Python中用于主动抛出异常的关键字。当程序执行到raise语句时,会立即中断当前代码的执行,并将异常信息传递给上层的异常处理机制。
基本语法:
最简单形式:raise Exception("错误信息")
抛出特定异常:raise ValueError("无效的值")
重新抛出当前异常:raise
常见用途:
参数验证:当函数参数不符合要求时抛出异常
业务逻辑验证:当业务规则被违反时抛出异常
程序流程控制:在特定条件下中断程序执行
异常转换:将捕获的异常转换为更适合的异常类型
使用建议:
选择合适的异常类型,如ValueError用于值错误,TypeError用于类型错误等
提供清晰的错误信息,帮助定位问题
在适当的层级处理异常,不要过度传播
自定义异常类时应该继承Exception类
注意事项:
raise后必须是一个异常实例或异常类
单独的raise语句只能在except块中使用,用于重新抛出当前异常
抛出异常会影响程序性能,不要用它来控制正常的程序流程
记录异常信息便于调试和维护
实际使用场景:
输入验证:检查用户输入的数据是否合法
配置检查:验证程序配置是否正确
资源管理:确保资源的正确使用和释放
接口约束:确保API使用符合约定
raise是一个强大的错误处理工具,合理使用可以提高程序的健壮性和可维护性,但应该谨慎使用,避免过度使用影响程序的可读性和性能。
下面是一个关于Python中raise语句用法的详细教程,包含了常见场景和示例:
def demonstrate_raise(): """Python中raise语句的各种用法示例""" print("=== 1. 最基本的raise用法 ===") try: print("引发一个简单的异常") raise Exception("这是一个基本异常") except Exception as e: print(f"捕获到异常: {e}") print("\n=== 2. 抛出特定类型的异常 ===") try: age = -5 if age < 0: raise ValueError("年龄不能为负数") except ValueError as e: print(f"捕获到ValueError: {e}") print("\n=== 3. 自定义异常类 ===") class CustomError(Exception): """自定义异常类""" def __init__(self, message, error_code): self.message = message self.error_code = error_code super().__init__(self.message) try: raise CustomError("这是自定义错误", 1001) except CustomError as e: print(f"捕获到自定义异常: {e.message}, 错误代码: {e.error_code}") print("\n=== 4. 异常链 ===") try: try: x = 1 / 0 except ZeroDivisionError as e: raise ValueError("发生了除零错误") from e except ValueError as e: print(f"捕获到ValueError: {e}") print(f"原始异常: {e.__cause__}") print("\n=== 5. 条件抛出异常 ===") def validate_age(age): if not isinstance(age, (int, float)): raise TypeError("年龄必须是数字") if age < 0: raise ValueError("年龄不能为负数") if age > 150: raise ValueError("年龄超出合理范围") return True try: validate_age("twenty") except (TypeError, ValueError) as e: print(f"验证失败: {e}") print("\n=== 6. 在函数中使用raise ===") def divide_numbers(a, b): try: if b == 0: raise ZeroDivisionError("除数不能为零") return a / b except TypeError: raise TypeError("参数必须是数字") from None try: result = divide_numbers(10, 0) except ZeroDivisionError as e: print(f"除法运算失败: {e}") print("\n=== 7. raise和finally配合使用 ===") def resource_operation(): resource = None try: resource = "打开的资源" raise ValueError("操作出错") finally: if resource: print(f"清理资源: {resource}") try: resource_operation() except ValueError as e: print(f"操作失败: {e}") print("\n=== 8. 重新抛出异常 ===") try: try: raise ValueError("原始错误") except ValueError: print("在处理异常时...") raise # 重新抛出当前异常 except ValueError as e: print(f"重新捕获到异常: {e}") def practical_example(): """实际应用示例:用户注册系统""" class UserRegistrationError(Exception): """用户注册相关的异常""" pass def validate_username(username): if not username: raise UserRegistrationError("用户名不能为空") if len(username) < 3: raise UserRegistrationError("用户名长度必须大于3个字符") if not username.isalnum(): raise UserRegistrationError("用户名只能包含字母和数字") def validate_password(password): if len(password) < 8: raise UserRegistrationError("密码长度必须大于8个字符") if not any(c.isupper() for c in password): raise UserRegistrationError("密码必须包含大写字母") if not any(c.isdigit() for c in password): raise UserRegistrationError("密码必须包含数字") def register_user(username, password): try: validate_username(username) validate_password(password) print(f"用户 {username} 注册成功!") except UserRegistrationError as e: print(f"注册失败: {e}") raise # 测试用例 test_cases = [ ("", "Password123"), # 空用户名 ("u", "Password123"), # 用户名太短 ("user@123", "Password123"), # 用户名包含特殊字符 ("user123", "pass"), # 密码太短 ("user123", "password123"), # 密码缺少大写字母 ("user123", "Password"), # 密码缺少数字 ("user123", "Password123") # 有效输入 ] print("\n=== 用户注册系统示例 ===") for username, password in test_cases: print(f"\n测试: 用户名='{username}', 密码='{password}'") try: register_user(username, password) except UserRegistrationError: print("注册过程终止") if __name__ == "__main__": demonstrate_raise() practical_example()