完整讲解 · 6 段教学·配 5 道练习题·预计 35 分钟
本页是本章的通读版,可直接读完全部讲解。想动手写代码、跑判分,去 闯关模式。
这一节内容挺多的,而且不太好理解。新手看完后可能还会一脸懵——这其实很正常。如果你看完是迷糊的,建议先继续学后面的内容,等学完一轮再回来重看几次,那时候就豁然开朗了。
主题:迭代器和生成器
这一节的核心其实就两个词:迭代 和 生成器。理解了它们,你以后处理「一组数据」时就能写出更优雅、更省内存的代码。
比如在 Java 中,我们通过 List 集合的下标来遍历 List 集合中的元素;在 Python 中,给定一个 list 或 tuple,我们可以通过 for 循环来遍历它,这种遍历就叫迭代(iteration)。
可是,Python 的 for 循环抽象程度要 高于 Java 的 for 循环。为什么这么说?因为 Python 的 for 循环不仅可以用在 list 或 tuple 上,还可以作用在 其他可迭代对象 上。
也就是说,只要是可迭代的对象,无论有没有下标,都是可以迭代的。
# 1、for 循环迭代字符串
for char in 'liangdianshui':
print(char, end=' ')
print('\n')
# 2、for 循环迭代 list
list1 = [1, 2, 3, 4, 5]
for num1 in list1:
print(num1, end=' ')
print('\n')
# 3、for 循环也可以迭代 dict(字典)
dict1 = {'name': '两点水', 'age': '23', 'sex': '男'}
for key in dict1: # 迭代 dict 中的 key
print(key, end=' ')
print('\n')
for value in dict1.values(): # 迭代 dict 中的 value
print(value, end=' ')
print('\n')
# 如果 list 里面一个元素有两个变量,也是很容易迭代的
for x, y in [(1, 'a'), (2, 'b'), (3, 'c')]:
print(x, y)输出的结果:
l i a n g d i a n s h u i
1 2 3 4 5
name age sex
两点水 23 男
1 a
2 b
3 c
能放进
for ... in后面的东西,就叫「可迭代对象」。
字符串、list、tuple、dict、set,甚至我们后面要讲的「生成器」,都是可迭代对象。
上面简单介绍了一下「迭代」。迭代是 Python 最强大的功能之一,是访问集合元素的一种方式。现在正式进入主题:迭代器(iterator)。
迭代器是一个可以记住遍历位置的对象。
迭代器对象从集合的第一个元素开始访问,直到所有元素被访问完为止。
迭代器有两个特点:
iter() 把可迭代对象变成迭代器;用 next() 取下一个元素。字符串、列表、元组都可以用来创建迭代器。迭代器对象既可以用 for 语句遍历,也可以用 next() 函数遍历。
# 1、字符串创建迭代器对象
str1 = 'liangdianshui'
iter1 = iter(str1)
# 2、list 对象创建迭代器
list1 = [1, 2, 3, 4]
iter2 = iter(list1)
# 3、tuple 对象创建迭代器
tuple1 = (1, 2, 3, 4)
iter3 = iter(tuple1)
# for 循环遍历迭代器对象
for x in iter1:
print(x, end=' ')
print('\n------------------------')
# next() 函数遍历迭代器
while True:
try:
print(next(iter3))
except StopIteration:
break最后输出的结果:
l i a n g d i a n s h u i
------------------------
1
2
3
4
iter(对象):把可迭代对象变成 迭代器。next(迭代器):从迭代器里取出 下一个元素。next() 会抛 StopIteration 异常 —— 所以 while True + try/except StopIteration 是手动遍历迭代器的标准写法。for 循环本质上就在帮你做上面这些事,只是它把 StopIteration 给你「悄悄」处理掉了。之前我们学过怎么创建一个 list。可是有时候用 赋值 的方式创建 list 太麻烦了,特别是 有规律的 list。
比如,要生成一个 30 个元素的 list,里面是 1 到 30 这些整数。我们可以这样写:
list1 = list(range(1, 31))
print(list1)输出:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30]
那如果要生成的不是 1-30,而是 1² ~ 10² 这样的 平方数列表 呢?这时候就该「列表生成式」登场了。
[expr for iter_var in iterable]
[expr for iter_var in iterable if cond_expr]
iterable 里所有内容,把每次的元素放到 iter_var 中,再用前面的 expr 计算出新值,组成新列表。if 判断,只有满足条件的元素才会被处理。要点:因为是 list 生成式,外层是 [];中间用 for 循环;可以可选地接 if。
list1 = [x * x for x in range(1, 11)]
print(list1)输出:
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
把要生成的元素 x * x 放在前面,后面跟 for x in range(1, 11),就把 list 创建出来了。
list1 = [x * x for x in range(1, 11) if x % 2 == 0]
print(list1)输出:
[4, 16, 36, 64, 100]
这个例子是求 1 到 10 中偶数的平方。后面 if x % 2 == 0 就是过滤条件。
list1 = [(x + 1, y + 1) for x in range(3) for y in range(5)]
print(list1)输出:
[(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5)]
其实知道了列表生成式怎么组合,你就明白了:它只是把以前学过的知识点重新组合,换成一种更简洁的写法而已。
通过上面的学习,我们知道用 列表生成式 可以直接创建一个列表。
但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含 1000 万个元素 的列表,不仅占用很大的存储空间;如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占的空间就白白浪费了。
如果列表元素可以按照某种算法推算出来,那我们能不能在循环的过程中不断推算出后续的元素呢?
这样就不必创建完整的 list,从而节省大量空间。
在 Python 中,这种 一边循环一边计算 的机制,称为 生成器(generator)。
在 Python 中,使用了 yield 的函数被称为生成器。
跟普通函数不同的是,生成器是一个返回迭代器的函数,只能用于迭代操作,更简单点理解:生成器就是一种迭代器。
在调用生成器运行的过程中,每次遇到 yield 时函数会 暂停并保存当前所有的运行信息,返回 yield 后面的值;下一次执行 next() 方法时,从当前位置 继续运行。
最简单的方法就是把一个列表生成式的 [] 改成 ():
gen = (x * x for x in range(10))
print(gen)输出(地址会不一样):
<generator object <genexpr> at 0x0000000002734A40>
创建 list 和 generator 的区别仅在于最外层的 [] 和 ()。
但生成器并不真正创建数字列表,而是返回一个生成器对象。这个生成器在每次计算出一个条目后,把这个条目「产生」(yield)出来。这种「惰性计算(lazy evaluation)」在列表很长时尤其省内存。
按我们的思维,遍历当然用 for 循环:
gen = (x * x for x in range(10))
for num in gen:
print(num)也可以用 next() 一个一个取,跟前面的迭代器一模一样。
实际开发中,大多数生成器都是通过函数来实现的。怎么写?
先看一个普通函数:
def my_function():
for i in range(10):
print(i)
my_function()输出 0 到 9。
如果我们要把它变成生成器,只需要把 print(i) 改为 yield i 就行了:
def my_function():
for i in range(10):
yield i
print(my_function())输出(看到的不是 0-9 而是 generator 对象):
<generator object my_function at 0x...>
这是因为 调用生成器函数不会执行函数体,而是返回一个生成器对象。要看到值,得用 for 或 next() 去遍历它。
generator 和普通函数的执行流程不一样:
return 或最后一行就返回。next() 时执行,遇到 yield 就暂停,下次再调用 next() 时从上次的 yield 后面继续。看个直观的例子:
def odd():
print('step 1')
yield 1
print('step 2')
yield 3
print('step 3')
yield 5
o = odd()
print(next(o))
print(next(o))
print(next(o))输出:
step 1
1
step 2
3
step 3
5
可以看到,odd 不是普通函数而是 generator,在执行过程中遇到 yield 就中断,下次又继续执行。三次 yield 后已经没有 yield 可执行了,如果再 next(o),就会抛 StopIteration 异常。
def fibon(n):
a = b = 1
for _ in range(n):
yield a
a, b = b, a + b
for x in fibon(10):
print(x, end=' ')输出:
1 1 2 3 5 8 13 21 34 55
哪怕你写 fibon(1000000),也不会卡死,因为生成器 不会一次性把所有结果都算出来,而是用一个就算一个。这就是「惰性」带来的好处。
迭代器和生成器很多用法是相通的,下面是两个常见的实战场景。
反向迭代也是常有的需求。比如最开始的例子里,我们顺序输出 list 的元素 1 到 5:
list1 = [1, 2, 3, 4, 5]
for num1 in list1:
print(num1, end=' ')那如果想从 5 到 1 呢?很简单,Python 中有内置的函数 reversed():
list1 = [1, 2, 3, 4, 5]
for num1 in reversed(list1):
print(num1, end=' ')输出:
5 4 3 2 1
注意一点:反向迭代仅当对象的大小可以预先确定,或者对象实现了
__reversed__()特殊方法时才能生效。 如果两者都不符合,那你必须先把对象转换为一个列表才行。
我们也可以在自定义类上实现 __reversed__() 方法:
class Countdown:
def __init__(self, start):
self.start = start
def __iter__(self):
# 正向迭代器
n = self.start
while n > 0:
yield n
n -= 1
def __reversed__(self):
# 反向迭代器
n = 1
while n <= self.start:
yield n
n += 1Countdown(3) 正向遍历就是 3 2 1,反向遍历就是 1 2 3。
你想 同时迭代多个序列,每次分别从一个序列中取一个元素,怎么办?用内置的 zip() 函数:
names = ['liangdianshui', 'twowater', '两点水']
ages = [18, 19, 20]
for name, age in zip(names, ages):
print(name, age)输出:
liangdianshui 18
twowater 19
两点水 20
zip(a, b) 会生成一个返回元组 (x, y) 的迭代器,其中 x 来自 a,y 来自 b。一旦其中某个序列到了结尾,迭代就结束。因此迭代长度跟参数中最短序列的长度一致。
利用 zip(),我们还可以把一个 key 列表和一个 value 列表组合成 dict:
names = ['liangdianshui', 'twowater', '两点水']
ages = [18, 19, 20]
dict1 = dict(zip(names, ages))
print(dict1)输出:
{'liangdianshui': 18, 'twowater': 19, '两点水': 20}
提一句:zip() 是可以接受 多于两个 的序列参数的,不仅限于两个。
写 for 的时候,如果你 既要值又要下标,可以用 enumerate():
fruits = ['苹果', '香蕉', '橘子']
for i, fruit in enumerate(fruits):
print(i, fruit)输出:
0 苹果
1 香蕉
2 橘子
enumerate(可迭代对象) 返回一个 (下标, 元素) 的迭代器,省去你自己维护一个计数变量的麻烦。
总结:
for / iter / next / yield / zip / enumerate / reversed—— 这一套就是 Python 处理「一组数据」的核心武器。下面来做最后一道练习。
读完了?动手练一遍才算真会。
去闯关模式练习 →