引言
本文作为 Python 系列的特别篇第 3 篇,主要介绍 Python 里的两大利「器」,生成器 (generator) 和迭代器 (iterator)。
抱歉两大利器这个标题有点标题党,因为此器非彼器,我只能用干货内容来弥补。接下来 2 节来详细介绍这两大利器。
1.生成器
定义生成器 (generator) 有两种方法:
1.1第一种方法
首先看一个简单函数 square,计算列表里每个数的平方值。
def square(nums): results = [] for num in nums: results.append(num*num) return results
输入 [1, 2, 3, 4, 5],输出 [1, 4, 9, 16, 25]
my_nums = square([1, 2, 3, 4, 5])print(my_nums)
[1, 4, 9, 16, 25]
在 square 函数里面做点两个小修改:
def square(nums): for num in nums: yield num*num
这时 square 不再是函数了,而是一个生成器了。因此记住,用关键词 return 的是函数,用关键词 yield 的是生成器。打印来看看。
my_nums = square([1, 2, 3, 4, 5])print(my_nums)
<generator object square at 0x00000179D3629ED0>
和函数 square 的输出相比,生成器 square 的输出不是一个列表,而是一段乏味的信息。
如何来看生成器里的元素呢?有两种方法:1. 转换成 list;2. 用 next()。
转换成 list
list(my_nums)
[1, 4, 9, 16, 25]
虽然打印出了结果,但这不是生成器的用法。这样做的话还不如直接用列表解析式呢。
生成器中真正有特点的用法是用 next() 把不断获得下一个返回值。
用 next()
先打印出一个元素。
print(next(my_nums))
1
这时候生成器内部状态 (state) 已经更新到第 2 个元素了,即 [1, 4, 9, 16, 25] 里面的 4。接着打印四遍 next()。
print(next(my_nums))print(next(my_nums))print(next(my_nums))print(next(my_nums))
4
9
16
25
这时候生成器内部状态已经更新到第 5 个元素,即最后一个元素了,再用 next() 会发生什么呢?
print(next(my_nums))
果然,报错了!StopIteration 意思就是迭代 (iteration) 停止 (stop) 了。等等,迭代?生成器可以被迭代?那生成器可不可以叫做迭代器?可以的!至于迭代器是第 2 节的内容。
我们知道 for 循环就是遍历一个迭代器里的每个元素的,那试试用 for 循环来遍历生成器 my_nums。
my_nums = square([1, 2, 3, 4, 5])
for num in my_nums: print(num)
1
4
9
16
25
总结:生成器可以用生成函数 (generator function) 来定义,记住要用 yield 而不是 return。
1.2第二种方法
复习〖Python 入门篇 (下)〗第 5 节回忆一下列表解析式。注意用中括号 [] 来定义列表解析式。
my_nums = [x*x for x in [1,2,3,4,5]]print(my_nums)
[1, 4, 9, 16, 25]
要定义生成器,只需要把中括号 [] 换成小括号 ()。
my_nums = (x*x for x in [1,2,3,4,5])print(my_nums)
<generator object <genexpr> at 0x00000179D3C4B048>
print(list(my_nums))
[1, 4, 9, 16, 25]
总结:生成器可以用生成表达式 (generator expression) 来定义,记住和列表解析式很像,将 [] 改成 () 即可。
1.3生成器 vs 列表
两种用函数和表达式来定义生成器的方法介绍完了,现在思考,生成器好在哪里?好就好在生成器是按需求调用 (call-by-need) 的,你需要调用一个值,我就 yield 一个值,然后用 next() 更新内部状态,等待你下次调用。这套流程也称作惰性求值 (lazy evaluation),目的是最小化计算机要做的工作。
在大规模数据时,一次性处理往往抵消而且不方便,而惰性求值解决了这个问题,它把计算的具体步骤延迟到了要实际用该数据的时候。
接下来我们做个小实验,对比一下生成器和列表在运行简单操作时的用的时间和占的内存。
首先引入记录运行时间的 time 和占内存大小的 sys。
import timeimport sys
做 1 千万次 x+1 的操作,生成列表 l 和生成器 g。
%time l = [x+1 for x in range(10000000)]print(sys.getsizeof(l))
%time g = (x+1 for x in range(10000000))print(sys.getsizeof(g))
Wall time: 1.28 s
81528056
Wall time: 0 ns
120
比较两者用时和占用内存,谁优谁劣一目了然。
2.迭代器
在介绍迭代器 (Iterator) 之前,先介绍可迭代对象 (Iterable)。
2.1
可迭代对象
任何只要可以循环的东西就可称之可迭代对象 (iterable)。容器类型数据 (str, tuple, list, dict, set) 都可以被 for 循环,因此它们都是可迭代对象。
判断方法一
在 Python 里万物皆对象,如果真要判断一个对象是否是可迭代对象,我们可以用 isinstance(x, Iterable)。
from collections import Iterable
print(isinstance([1,2,3], Iterable)) # listprint(isinstance({'1':23}, Iterable)) # dictprint(isinstance((1,2,3), Iterable)) # tupleprint(isinstance({1,2,3}, Iterable)) # setprint(isinstance('123', Iterable)) # strprint(isinstance(123, Iterable)) # intprint(isinstance(123.0, Iterable)) # float
True
True
True
True
True
False
False
结果正常,str, tuple, list, dict, set 都是可迭代对象,而 int 和 float 不是。
判断方法二
在 Python 里万物皆对象,那么我们可以查看该对象对应的类里面的属性,用 dir() 函数。里面有 __iter__() 魔法方法 (magic method) 的对象就是可迭代对象。不清楚上面这些知识可参考〖Python 特别篇 - 面向对象编程〗一贴。
列表 [1, 2, 3] 里有 __iter__() 魔法方法,它是可迭代对象。
print(dir([1,2,3]))
整数 123 里没有 __iter__() 魔法方法,它不是可迭代对象。
print(dir(123))
2.2迭代器
可被 for 循环的列表、字典、元组、集合和字符串都是可迭代对象,但实际上 for 循环里真正的对象是迭代器。
首先用 isinstance(x, Iterator) 来判断它们 5 个是不是迭代器。
from collections import Iterator
print(isinstance([1,2,3], Iterator)) # listprint(isinstance({'1':23}, Iterator)) # dictprint(isinstance((1,2,3), Iterator)) # tupleprint(isinstance({1,2,3}, Iterator)) # setprint(isinstance('123', Iterator)) # str
False
False
False
False
False
都不是的!那它们怎么能被 for 循环呢?原来 for 循环先用 __iter__() 方法将它们都转成迭代器,再开始遍历它们的每个元素。
print(isinstance(iter([1,2,3]), Iterator)) # listprint(isinstance(iter({'1':23}), Iterator)) # dictprint(isinstance(iter((1,2,3)), Iterator)) # tupleprint(isinstance(iter({1,2,3}), Iterator)) # setprint(isinstance(iter('123'), Iterator)) # str
True
True
True
True
True
被 __iter__() 方法包装之后,列表、字典、元组、集合和字符串都是迭代器了。注意 iter(x) 和 x.__iter__() 是等价的,后者太难看因此习惯用前者。
既然能被迭代,那该对象里面肯定有 __next__() 魔法方法,要不然怎么可以一个元素一个元素往下走下去啊?来验证一波。
定义列表 nums,可以被 for 循环遍历。
nums = [1, 2, 3]
for num in nums: print(num)
1
2
3
但是 nums 不是迭代器 (只是 for 循环在遍历前将其转换成迭代器了),用 next() 会不错,错误信息是“列表对象不是迭代器”。
print(next(nums))
用 iter() 将可迭代对象 nums 转换成迭代器 i_nums,打印其本身发现 i_nums 是 list_iterator,打印其下属性,找到了 __next__。
i_nums = iter(nums)print(i_nums)print(dir(i_nums))
除了 __next__,迭代器里还有 __iter__,而 __iter__ 是判断可迭代对象的标准。因此可得
只要是迭代器就是可迭代对象,但反过来不成立。
现在两者关系梳理清楚了吧,我们来用 next() 一个个打印迭代器 i_nums 里的值。
i_nums = iter(nums)
print( next(i_nums) )print( next(i_nums) )print( next(i_nums) )
1
2
3
如果打印 4 遍呢?
i_nums = iter(nums)
print( next(i_nums) )print( next(i_nums) )print( next(i_nums) )print( next(i_nums) )
1
2
3
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-98-cb9222563d74> in <module>
4 print( next(i_nums) )
5 print( next(i_nums) )
----> 6 print( next(i_nums) )
StopIteration:
第 6 行,在获取第 4 个元素时报错,StopIteration 。
有了 StopIteration 这个提示,我们甚至可以自己写代码来实现用 for 循环来打印。
i_nums = iter(nums)
while True: try: print(next(i_nums)) except StopIteration: break
1
2
3
2.3
自定义迭代器
上节讲了用 iter() 函数可以将可迭代对象转换成迭代器,本节再介绍两种定义迭代器的方法:1. 用类;2. 生成器。
用类来定义
我们已经知道迭代器里面有 __iter__ 和 __next__ 方法,那我们只需在类里自定义这两个方法了。以 MyRange 为例。
class MyRange: def __init__(self, start, end): self.value = start self.end = end def __iter__(self): return self def __next__(self): if self.value >= self.end: raise StopIteration current = self.value self.value += 1 return current
测试一下用 MyRange 类定义的迭代器 nums。
nums = MyRange(1, 5)
for num in nums: print(num)
1
2
3
4
可以被 for 循环。
nums = MyRange(1, 5)
print(next(nums))print(next(nums))print(next(nums))print(next(nums))
1
2
3
4
也可以使用 next() 函数。
用生成器来定义
用类定义迭代器没毛病,就是代码太冗长了。而用生成器定义迭代器真的简洁,代码如下。
def range_generator(start, end): current = start while current < end: yield current current += 1
代码太简单了有没有。首先将 start 复制给 current,只要 current 比 end 小,就 yield 一个 current,再加个 1 更新它。
测试一下用 range_generator 生成器定义的迭代器 nums。
nums = range_generator(1, 5)
for num in nums: print(num)
1
2
3
4
可以被 for 循环。
nums = range_generator(1, 5)
print(next(nums))print(next(nums))print(next(nums))print(next(nums))
1
2
3
4
也可以使用 next() 函数。
思考下面生成器的输出是什么?
def range_generator(start): current = start while True yield current current += 1
从 start 开始,每次加 1 没有终点 (不建议在电脑上实验!)
2.4内置迭代器
在 Python 里有不少内置的迭代器,用起来非常方便,我们会介绍 count, cycle, repeat, combinations, permudations, product 和 chain。
首先引用 itertools,顾名思义就知道它里面有很多迭代器的工具。
import itertools
print(dir(itertools))
count
counter = itertools.count()
print(next(counter))print(next(counter))print(next(counter))print(next(counter))
0
1
2
3
count() 就像是计数器,不停的往前更新。它可用在给不知道大小的数据标注索引。比如我们在收集交易数据,未来有多少个我们不知道,我们就可以用 count() 来不停更新索引值。
下列以 4 个数据点 (假设是阿里巴巴每分钟的股票价格) 为例,用 zip 函数来将 count() 的输出给股价标注索引。
data = [170.1, 170.8, 171.4, 170.5]
minute_data = zip( itertools.count(), data)print(minute_data)
<zip object at 0x00000179D362FC88>
这时得到是个 zip 对象,可将其转换成 list 打印其内容。
print(list(minute_data))
[(0, 170.1), (1, 170.8), (2, 171.4), (3, 170.5)]
data 里面有 4 个点,count() 就返回 4 个值,如果data 里面有 4000000 个点,count() 就返回 4000000 个值。体会到使用 count() 的便利了吗?
当然,我们可以设置起始值和间隔值,请参考下面两段代码。
counter = itertools.count(start=5)
print(next(counter))print(next(counter))print(next(counter))print(next(counter))
5
6
7
8
counter = itertools.count(start=5, step=-2)
print(next(counter))print(next(counter))print(next(counter))print(next(counter))
5
3
1
-1
cycle
cycle = itertools.cycle(('on','off'))
print(next(cycle))print(next(cycle))print(next(cycle))print(next(cycle))
on
off
on
off
cycle() 作用是循环遍历!
repeat
repeat = itertools.repeat(2, times=3)
print(next(repeat))print(next(repeat))print(next(repeat))print(next(repeat))
2
2
2
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-49-4e464e5eee1b> in <module>
4 print(next(repeat))
5 print(next(repeat))
----> 6 print(next(repeat))
StopIteration:
repeat() 作用是重复,但是一旦设置 times 参数比如 3,那么不能打印次数不能超过 3,否则会报错。
repeat() 还可以和其他高阶函数一起用,如下列 map 函数。将 pow 操作 (第一个参数) 作用在 [0,1,2,3,4] 上,指数为 2。用 repeat() 好处是重复的次数会跟前面 range(n) 匹配。
square = map( pow, range(5), itertools.repeat(2) )square
<map at 0x179d3724908>
list(square)
[0, 1, 4, 9, 16]
combinations & permutations
接下来看看用于排列 (permutation) 和组合 (combination) 的迭代器。
首先创建一个列表 letters,一个元组 numbers 和一个集合 names。
letters = ['a', 'b', 'c', 'd']numbers = (1, 2, 3, 4)names = {'Steven', 'Sherry'}
从 letters 里面 4 个元素取出 2 个来组合 (元素位置不重要)。
results = itertools.combinations(letters, 2)
for items in results: print(items)
('a', 'b')
('a', 'c')
('a', 'd')
('b', 'c')
('b', 'd')
('c', 'd')
从 letters 里面 4 个元素取出 2 个来排列 (元素位置重要)。
results = itertools.permutations(letters, 2)
for items in results: print(items)
('a', 'b')
('a', 'c')
('a', 'd')
('b', 'a')
('b', 'c')
('b', 'd')
('c', 'a')
('c', 'b')
('c', 'd')
('d', 'a')
('d', 'b')
('d', 'c')
product
product() 是穷举出所有情况,本例只从 numbers 里面 4 个元素取出 2 个,不同位置元素可以重复。
results = itertools.product(numbers, repeat=2)
for items in results: print(items)
(1, 1)
(1, 2)
(1, 3)
(1, 4)
(2, 1)
(2, 2)
(2, 3)
(2, 4)
(3, 1)
(3, 2)
(3, 3)
(3, 4)
(4, 1)
(4, 2)
(4, 3)
(4, 4)
上面结果去除一些「位置不同但元素相同」,比如 (1, 2) 和 (2, 1) 只保留一个,就是 combinations_with_replacement() 的结果,验证如下。
results = itertools.combinations_with_replacement(numbers, 2)
for items in results: print(items)
(1, 1)
(1, 2)
(1, 3)
(1, 4)
(2, 2)
(2, 3)
(2, 4)
(3, 3)
(3, 4)
(4, 4)
上面结果再去除「重复元素」,比如 (1, 1), (2, 2), ... ,就是 combinations() 的结果验证如下。
results = itertools.combinations(numbers, 2)
for items in results: print(items)
(1, 2)
(1, 3)
(1, 4)
(2, 3)
(2, 4)
(3, 4)
chain
用于三个不同类型的数据格式,列表 letters,元组 numbers 和集合 names,我们可用 chain() 将它们串起来成一个迭代器,再逐个遍历它里面的元素。
代码如下。
results = itertools.chain(letters, numbers, names)
for items in results: print(items)
a
b
c
d
1
2
3
4
Sherry
Steven
3.总结
在 Python 里,
当你想要个可用惰性计算的可迭代对象时,考虑用迭代器。
当你想创建迭代器时,考虑用生成器。
当你想创建生成器时,考虑用生成函数 (用 yield) 或生成表达式 (用小括号 ())。
当你想 ...,别想了,在看-转发-留言吧,千万不要赞赏我。下篇讲装饰器 (Decorator)。
Stay Tuned!