13  循环程序结构

一些任务需要不断重复相同或相似的动作来完成。 在编程时,可通过循环程序结构来实现这些任务。

注记循环应用:上课点名

上课时,老师通过点名确认学生的出勤情况。对一个班级人数为30人的班级,点名活动进程如下:

  1. 点第1名学生的名字;
  2. 点第2名学生的名字;

……

  1. 点第30名学生的名字。
注记循环应用:开根号

如何求一个正数x的平方根?可用二分法逐步缩小范围。如求7的平方根:

  1. 已知 0<\sqrt{7}<7 ;计算区间的中点 (0+7)/2=3.5
  2. 3.5^2 = 12.25>7 ,知 0<\sqrt{7}<3.5 ; 计算区间的中点 (0+3.5)/2=1.75
  3. 1.75^2 = 3.0625<7 ,知 1.75<\sqrt{7}<3.5 ;计算区间的中点 (1.75+3.5)/2=2.625

……

最终区间会变得足够小,区间上的任意点都是 \sqrt{7} 的近似解。

循环程序中,每次循环执行的操作可能完全相同(如打印相同的字符串), 也可能略有差异(如第一次循环打印整数 1,第二次循环打印整数 2)。

循环次数是有限的,否则程序陷入无限循环不会停止。 可以事先固定循环的次数(如循环 3 次就结束),也可设定一个必然能达成的循环停止条件。

# 每次循环操作完全相同
# 将“春眠不觉晓”重复3遍
for i in range(3):
    print('春眠不觉晓')
春眠不觉晓
春眠不觉晓
春眠不觉晓
# 每次循环操作不完全相同
# 从5开始倒数到1
for i in range(5, 0, -1):
    print(i)
5
4
3
2
1

13.1 for循环

13.1.1 for循环语法

for循环用于循环次数确定的情况。

for i in iterable:
    循环体

iterable可迭代对象。可以将其理解为一些对象构成的集合。 可迭代对象的具体范围包括:

1. 序列数据类型,如:列表(list),元组(tuple),字符串(str),range;
2. 字典(dict),文件对象(file object);
3. 其他具有`__iter__()`方法的对象。

可用collections.abc中的Iterable结合isinstance()函数,判断对象是否是可迭代对象。

from collections.abc importIterable
isinstance(对象, Iterable) # 判断对象是否是可迭代对象
from collections.abc import Iterable
# 列表是可迭代对象
isinstance([1, 2, 3], Iterable)
True

你可以自己验证各类型的数据是否是可迭代对象,如'123'123[1, 2, 3]

for循环语法如下:

  • 每轮循环开始时,返回可迭代对象iterable中的下一个对象,将标识符i绑定到此对象;
  • 执行循环体;
  • 循环体执行结束后,本轮循环结束,进入下一轮循环;
  • iterable中的项目被耗尽,for循环结束。

for语句流程图。图中将for后面的标识符表示为i
  • Python中的可迭代对象被定义为具有__iter__()的对象,对可迭代对象调用__iter__()方法的效果是返回相应的迭代器iterator
  • 迭代器代表一个数据流,它是一种特殊的可迭代对象:迭代器比迭代对象多了一个__next__()方法。每次调用__next__()方法,返回数据流中的下一个元素;当数据流中的元素消耗殆尽时,__next__()提出一个StopIteration异常。要求迭代器具有方法__iter__()__next__()的规定,称为迭代器协议(iterator protocol)。
  • 在进入for循环前,Python对可迭代对象iterable使用__iter__()方法,获得迭代器。
# for循环伪代码
for item in iterable:
    循环体
# for 循环的实际实现
# 1. 从可迭代对象(iterable)获取迭代器(iterator)
iterator = iter(iterable)  # 调用 iterable.__iter__()
while True:
    try:
        # 2. 从迭代器获取下一个元素
        item = next(iterator)  # 调用 iterator.__next__()
        # 3. 执行循环体
        循环体
    except StopIteration:
        # 4. 捕获StopIteration异常,结束循环
        break

迭代器协议是Python的核心语言机制,具有为不同数据类型提供统一遍历接口,惰性计算与内存高效,解耦迭代逻辑与数据结构、允许实现更灵活的遍历逻辑等优点。除了for循环,列表推导式,解包,map()filter()sum()等内置函数都通过迭代器协议实现,其构成Python迭代功能的基石。

初次接触可迭代对象、迭代器机制,迭代器协议等概念时,会感到比较复杂。 这很正常,因此初学者不用深入理解这些,只要会用for循环就好。 如果有朝一日你成为了重度程序员,应该就能体会迭代器协议的上述优势。

# 将'春眠不觉晓'逐字打印,每个字占一行
for i in '春眠不觉晓':
    print(i)
春
眠
不
觉
晓
注记debug
for i in 123:
    print(i)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[5], line 1
----> 1 for i in 123:
      2     print(i)

TypeError: 'int' object is not iterable

13.1.2 可迭代对象range

内置函数range()接受1 \sim 3个位置参数,分别表示起始值,结束值和步长,生成内容为等差数列的可迭代对象。 起始值默认为0,步长默认为1。 注意,range()返回的可迭代对象不含结束值,其最后一个元素恰好在结束值的“前面”。

# 生成从 start 开始,到 stop 结束(不含 stop),公差为 step 的等差数列
# range()函数不接受关键字参数,即传参时直接写数字,不要写`参数名`
# 这里写start, stop, step,只是为了标注参数含义
range(start = 0, stop[, step = 1])
# range()接受1个参数
# range(3)的3代表stop结束值
for i in range(3):
    print(i)
0
1
2
# range()接受2个参数
# range(1, 3)的1代表start起始值,3代表stop结束值
for i in range(1, 3):
    print(i)
1
2
# range()接受3个参数
for i in range(2, 6, 2):
    print(i)
2
4
# 步长可以为负
for i in range(6, 2, -2):
    print(i)
6
4

range对象不存储具体的数,在每次迭代/循环时才计算具体序列值。 因此用print()不能返回range()创建的可迭代对象中的具体成员。要看range()的具体成员,除了用for循环逐个打印,还可以用list()将此对象转换为列表。

# range()对象不存储具体的数
print(range(3))
type(range(3))
range(0, 3)
range
# 转换为list后range()的具体成员可以显示
print(list(range(3)))
[0, 1, 2]
注记思考

range对象不存储具体的数,而是随用随取。这种称为惰性计算(lazy evaluation)的特性有什么优势?

注记debug: 问题在哪?
range(3.0, 4.0, 0.1)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[12], line 1
----> 1 range(3.0, 4.0, 0.1)

TypeError: 'float' object cannot be interpreted as an integer
range(start = 10)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[13], line 1
----> 1 range(start = 10)

TypeError: range() takes no keyword arguments
range(0, 10, 0)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[14], line 1
----> 1 range(0, 10, 0)

ValueError: range() arg 3 must not be zero
注记例:等差数列求和I

for循环求 1+2+\cdots+100 的值。

total = 0
for i in range(1, 101):
    total += i

print(total)
5050
注记例:物不知数

有一个数,用3除余2,用5除余3,用7除余2,请问0~1000中这样的数有哪些?

一些数学问题,可以编程用循环枚举求解。这是一种重要的计算思维。

for i in range(1001):
    if i % 3 == 2:
        if i % 5 == 3:
            if i % 7 == 2:
                print(i)
23
128
233
338
443
548
653
758
863
968

此题也可只使用一个if,自己试试看。

注记例:斐波那契数列

斐波那契数列是每一项等于前两项之和的数列,最初的两项一般 是1、1。显示 Fibonacci 数列:1、1、2、3、5、8、· · · 的前10项。

f1 = 1
f2 = 1
# 因为每轮循环输出2个数,因此5轮循环打印前10项
for i in range(1, 6):
    print(f1,f2, end = '\t', sep = '\t')
    f1 += f2
    f2 += f1
1   1   2   3   5   8   13  21  34  55  
注记提示:循环学习方法
  1. 画流程图;
  2. 用自然语言描述第1轮循环中发生了什么,第2轮循环中发生了什么,…,直至找到规律,最后描述最后一轮循环发生了什么。

13.2 while循环

while语句用于无法事先确定循环次数、但知道循环继续条件的情形:当条件满足时,执行循环体,否则脱出循环。

while 条件表达式c:
    循环体

while语句流程图
注记例:等差数列求和II

while循环求 1+2+\cdots+100 的值。

i = 1
total = 0
while i <= 100:
    total += i
    i += 1

print(total)
5050
注记例:模拟I:负二项分布

生成 1\sim10 范围内的随机整数,将结果记为x。然后继续生成此范围内的随机数,直到再次生成x。返回迄今为止生成的随机数的个数。

import random
x = random.randrange(1, 11)
counter = 2
while x != random.randrange(1, 11):
    counter += 1

print(f'x={x},为了再次得到x,共生成了{counter}个随机数。')
x=8,为了再次得到x,共生成了5个随机数。

13.3 嵌套循环

有的任务需要在循环中再套循环,内层循环要相对外层循环缩进。 如:

  1. 小学生炼字。
    • 写汉字‘写’字需要5笔,写一个‘写’字要将【写一笔】这件事循环5次,此为内循环;
    • 如果一个字要写10遍,就要将【写一个字】循环10次,此为外循环。

5笔完成一个‘写’字
# 写一个‘写’字的Python伪代码
for i in range(1, 6):
    写第i笔
# 写3个‘写’字的Python伪代码
for i in range(3):
    # 外层循环
    # 开始写第i+1个字
    for j in range(1, 6):
        # 内层循环
        写第i个字的第j笔
  1. 打印九九乘法表。
    • 要打印9行,此为外循环;
    • 打每一行时,要打印9列,此为内循环。
for i in range(1, 10): 
    # 外层循环,开始打印第i行           
    for j in range(1, 10): 
        # 内层循环,开始打印第i行的第j列        
        print(i, '*', j, '=', i*j,  end = '\t', sep = '')
    # 每行打印结束时,打印换行符\n换行
    print('\n', end = '')
1*1=1   1*2=2   1*3=3   1*4=4   1*5=5   1*6=6   1*7=7   1*8=8   1*9=9   
2*1=2   2*2=4   2*3=6   2*4=8   2*5=10  2*6=12  2*7=14  2*8=16  2*9=18  
3*1=3   3*2=6   3*3=9   3*4=12  3*5=15  3*6=18  3*7=21  3*8=24  3*9=27  
4*1=4   4*2=8   4*3=12  4*4=16  4*5=20  4*6=24  4*7=28  4*8=32  4*9=36  
5*1=5   5*2=10  5*3=15  5*4=20  5*5=25  5*6=30  5*7=35  5*8=40  5*9=45  
6*1=6   6*2=12  6*3=18  6*4=24  6*5=30  6*6=36  6*7=42  6*8=48  6*9=54  
7*1=7   7*2=14  7*3=21  7*4=28  7*5=35  7*6=42  7*7=49  7*8=56  7*9=63  
8*1=8   8*2=16  8*3=24  8*4=32  8*5=40  8*6=48  8*7=56  8*8=64  8*9=72  
9*1=9   9*2=18  9*3=27  9*4=36  9*5=45  9*6=54  9*7=63  9*8=72  9*9=81  

前面演示的是两个for循环的嵌套,实际上根据任务需要,for和while可以自由相互嵌套,嵌套层数可以超过两层。

# 各种嵌套循环
for x in iterable_a:
    ...
    for y in iterable_b:
        ...

while c1:
    ...
    while c2:
        ...

for x in iterable_a:
    ...
    while c:
        ...

while c1:
    ...
    for x in iterable:
        ...

13.4 循环控制:break, continue

break 打断其所处的循环,接着执行循环语句的后继语句。

continue 跳过本轮循环中 continue 下面尚未执行的语句,进入下一轮循环。

存在嵌套循环时,breakcontinue 影响的是其所处的循环,注意分辨。

注记例:打印乘法表II

打印九九乘法表,但跳过偶数行。

for i in range(1, 10): 
    # 外层循环,跳过奇数行
    if i % 2 == 0:
        # 如果进入此分支
        # 这个if所在的循环体本次循环不再执行
        # 直接进入下一轮循环
        continue        
    for j in range(1, 10): 
        # 内层循环,打印第j列        
        print(i, '*', j, '=', i*j,  end = '\t', sep = '')
    print('\n', end = '')
1*1=1   1*2=2   1*3=3   1*4=4   1*5=5   1*6=6   1*7=7   1*8=8   1*9=9   
3*1=3   3*2=6   3*3=9   3*4=12  3*5=15  3*6=18  3*7=21  3*8=24  3*9=27  
5*1=5   5*2=10  5*3=15  5*4=20  5*5=25  5*6=30  5*7=35  5*8=40  5*9=45  
7*1=7   7*2=14  7*3=21  7*4=28  7*5=35  7*6=42  7*7=49  7*8=56  7*9=63  
9*1=9   9*2=18  9*3=27  9*4=36  9*5=45  9*6=54  9*7=63  9*8=72  9*9=81  

对比,如果将上述的continue换为break,得到的输出如下。想想为什么。

for i in range(1, 10): 
    if i % 2 == 0:
        break      
    for j in range(1, 10): 
        print(i, '*', j, '=', i*j,  end = '\t', sep = '')
    print('\n', end = '')
1*1=1   1*2=2   1*3=3   1*4=4   1*5=5   1*6=6   1*7=7   1*8=8   1*9=9   
注记例:打印乘法表III

打印九九乘法表,但跳过偶数列。

for i in range(1, 10): 
    # 外层循环,打印第i行
    for j in range(1, 10): 
        # 内层循环,跳过奇数列   
        if j % 2 == 0:
            # 如果进入此分支
            # 这个if所在的循环体本次循环不再执行
            # 直接进入下一轮循环  
            continue   
        print(i, '*', j, '=', i*j,  end = '\t', sep = '')
    print('\n', end = '')
1*1=1   1*3=3   1*5=5   1*7=7   1*9=9   
2*1=2   2*3=6   2*5=10  2*7=14  2*9=18  
3*1=3   3*3=9   3*5=15  3*7=21  3*9=27  
4*1=4   4*3=12  4*5=20  4*7=28  4*9=36  
5*1=5   5*3=15  5*5=25  5*7=35  5*9=45  
6*1=6   6*3=18  6*5=30  6*7=42  6*9=54  
7*1=7   7*3=21  7*5=35  7*7=49  7*9=63  
8*1=8   8*3=24  8*5=40  8*7=56  8*9=72  
9*1=9   9*3=27  9*5=45  9*7=63  9*9=81  

对比,如果将上述的continue换为break,得到的输出如下。想想为什么。

for i in range(1, 10): 
    for j in range(1, 10): 
        if j % 2 == 0: 
            break  
        print(i, '*', j, '=', i*j,  end = '\t', sep = '')
    print('\n', end = '')
1*1=1   
2*1=2   
3*1=3   
4*1=4   
5*1=5   
6*1=6   
7*1=7   
8*1=8   
9*1=9   
注记例:模拟II:负二项分布

生成1 \sim 10范围内的随机整数,将结果记为x。然后继续生成此范围内的随机数,直到再次生成x。返回迄今为止生成的随机数的个数。使用break

import random
x = random.randrange(1, 11)
counter = 1
while True:
    x1 = random.randrange(1, 11)
    counter += 1
    if x1 == x:
        print(counter)
        break    
5
注记例:素数判断I

用循环判断一个正整数是否是素数。

import math
m = 1001
k = int(math.sqrt(m))
flag = True
i = 2
while (i <= k):
    if m % i == 0:
        flag = False
        break
    else:
        i += 1
if flag:
    print(f'{m}是素数!')
else:
    print(f'{m}是合数!具有因子{i}')
1001是合数!具有因子7
注记例:求平方根I

编程,用二分法求2025的平方根,保留2位小数。

# 二分法查找
x = 2025
tol = 1e-20 # 精确度tolerance
max_iter = 1e10 # 最大迭代次数maximum iteration
n_iter = 1 # 当前迭代次数
e1 = 0 # 区间左端点
e2 = x # 区间右端点
m = (e1 + e2)/2 # 区间中点
while abs(m ** 2 - x) > tol:  # 不断近似,直到误差足够小
    if n_iter > max_iter:
        break
    # if语句更新区间端点
    if m ** 2 > x:
        e2 = m
    elif m ** 2 == x:
        break
    else:
        e1 = m 
    # 更新区间端点后,更新中点
    m = (e1 + e2)/2
    n_iter += 1

print(f'经过{n_iter}次迭代,得到解{m}。')
经过57次迭代,得到解45.0。
注记例:求平方根II

编程,用牛顿-拉弗森方法求2025的平方根,保留2位小数。

牛顿-拉弗森方法用利用泰勒展开的前几项求方程 f(x)=0 的根。 对于方程 x^2-a=0 ,具体算法是:

假设t=a,开始循环; 如果t=a/t或小于容差),则t等于a的平方根,循环结束并返回结果; 否则,将t和a/t的平均值赋值给t,继续循环。

a = 2025
tol = 1e-20
max_iter = 1e10
n_iter = 1
t = a
while abs(t - a/t) > tol:
    if n_iter > max_iter:
        break
    t = (a/t + t)/2
    n_iter += 1

print(f'经过{n_iter}次迭代,得到解{t}。')
经过11次迭代,得到解45.0。

13.5 循环技巧

13.5.1 enumerate():变量对象与索引

enumerate() 接受可迭代对象作为参数,返回索引与可迭代对象,使用户能在循环中使用索引。 索引默认从 0 开始,可以用 start 参数规定起始索引

seasons = ['Spring', 'Summer', 'Autumn', 'Winter']
for i, s in enumerate(seasons, start = 1):
   print(f'第{i}季节:{s}')
第1季节:Spring
第2季节:Summer
第3季节:Autumn
第4季节:Winter

13.5.2 zip():一次遍历多个可迭代对象

zip(iterable1, iterable2, ...)将多个可迭代对象中对应的元素打包成元组,返回这些元组形成的可迭代对象。

如果各可迭代对象参数的元素个数不同,返回与最短参数长度相同的可迭代对象。

a = ['天', '雨', '大陆', '山花', '赤日']
b = ['地', '风', '长空', '海树', '苍穹']
for item in zip(a, b):
    print(item)
('天', '地')
('雨', '风')
('大陆', '长空')
('山花', '海树')
('赤日', '苍穹')
a = ['天', '雨', '大陆', '山花', '赤日']
b = ['地', '风', '长空', '海树', '苍穹']

for x, y in zip(a,b):
    print(f'{x}{y}')
天对地
雨对风
大陆对长空
山花对海树
赤日对苍穹
# zip()多个参数不等长时,返回值与最短参数的长度相同
a = ['天', '雨', '大陆', '山花', '赤日']
b = ['地', '风', '长空']

for x, y in zip(a,b):
    print(f'{x}{y}')
天对地
雨对风
大陆对长空

13.5.3 else子句

含有break的循环,后面可跟else子句(else的缩进水平与循环的forwhile相同):

  • 如果循环是被 break 中断的,不执行 else 子句的内容;
  • 否则,执行else子句的内容。
for n in range(2, 10):
    for x in range(2, n):
        if n % x == 0:
            print(n, '=', x, '*', n//x)
            break
    else: # 这里else与for配对,而非与if配对,代码没错哦
        # 如果没能找到n的因子,执行这里
        print(n, '是素数。')
2 是素数。
3 是素数。
4 = 2 * 2
5 是素数。
6 = 2 * 3
7 是素数。
8 = 2 * 4
9 = 3 * 3
# 不用else的等价代码
is_prime = True
for n in range(2, 10):
    for x in range(2, n):
        if n % x == 0:
            print(n, '=', x, '*', n//x)
            is_prime = False
            break
    if is_prime:
        print(n, '是素数。')
    is_prime = True
2 是素数。
3 是素数。
4 = 2 * 2
5 是素数。
6 = 2 * 3
7 是素数。
8 = 2 * 4
9 = 3 * 3

13.6 练习

  1. 猜数字游戏。随机生成一个 0\sim999 范围内的整数,请用户猜测。 对用户的每次猜测,返回“偏大”或“偏小”的反馈,直到用户猜对或认输。 用户认输时,显示正确答案。

  2. 1A2B 猜数字:系统随机生成一个指定位数、各位数字都不同的数, 用户每次猜测,系统提示 A 表示数字和位置都正确,B 表示数字正确但位置错误 。 如答案为 1234 ,用户猜测 1356,则系统提示 1A1B 。 根据用户的输入给出提示,直到用户猜对或认输。

  3. “百元买百鸡”是我国古代数学家张丘建在《算经》一书中提出的数学问题:鸡翁一值钱五,鸡母一值钱三,鸡雏三值钱一。百钱买百鸡,问鸡翁、鸡母、鸡雏各几何? 用现代语言描述为:用100元钱买来100只鸡,公鸡5元钱一只,母鸡3元钱一只,小鸡1元钱3只。请问在这100只鸡中,公鸡、母鸡、小鸡各是多少只?

  4. 输出100以内的所有质数。

  5. 鸡兔同笼,35头,94足,求鸡、兔的数量。如果无解,则输出“无解”。

  6. 赌徒有3元本金。 每次赌博,赌徒有0.5的概率获得1元,有0.5的概率失去1元。 如果赌徒的本金累积到10元,则退出赌博;如果赌博失去所有本金,则破产。进行模拟,报告每局赌博的结果,以及赌徒的结局是见好就收还是破产。