首页
学习
活动
专区
圈层
工具
发布

盘一盘 Python 系列特别篇 - 两大利「器」

引言

本文作为 Python 系列的特别篇第 3 篇,主要介绍 Python 里的两大利「器」,生成器 (generator) 和迭代器 (iterator)。

抱歉两大利器这个标题有点标题党,因为此器非彼器,我只能用干货内容来弥补。接下来 2 节来详细介绍这两大利器。

1.生成器

定义生成器 (generator) 有两种方法:

  1. 使用函数 (function)
  2. 使用表达式 (expression)

1.1第一种方法

首先看一个简单函数 square,计算列表里每个数的平方值。

代码语言:javascript
复制
def square(nums):    results = []    for num in nums:        results.append(num*num)    return results

输入 [1, 2, 3, 4, 5],输出 [1, 4, 9, 16, 25]

代码语言:javascript
复制
my_nums = square([1, 2, 3, 4, 5])print(my_nums)
代码语言:javascript
复制
[1, 4, 9, 16, 25]

在 square 函数里面做点两个小修改:

  1. 不要 return (即不需要定义列表 results)
  2. 使用 yield (给定一个 num 来 yield 一个 num*num)
代码语言:javascript
复制
def square(nums):    for num in nums:        yield num*num

这时 square 不再是函数了,而是一个生成器了。因此记住,用关键词 return 的是函数,用关键词 yield 的是生成器。打印来看看。

代码语言:javascript
复制
my_nums = square([1, 2, 3, 4, 5])print(my_nums)
代码语言:javascript
复制
<generator object square at 0x00000179D3629ED0>

和函数 square 的输出相比,生成器 square 的输出不是一个列表,而是一段乏味的信息。

如何来看生成器里的元素呢?有两种方法:1. 转换成 list;2. 用 next()。

转换成 list

代码语言:javascript
复制
list(my_nums)
代码语言:javascript
复制
[1, 4, 9, 16, 25]

虽然打印出了结果,但这不是生成器的用法。这样做的话还不如直接用列表解析式呢。

生成器中真正有特点的用法是用 next() 把不断获得下一个返回值

用 next()

先打印出一个元素。

代码语言:javascript
复制
print(next(my_nums))
代码语言:javascript
复制
1

这时候生成器内部状态 (state) 已经更新到第 2 个元素了,即 [1, 4, 9, 16, 25] 里面的 4。接着打印四遍 next()。

代码语言:javascript
复制
print(next(my_nums))print(next(my_nums))print(next(my_nums))print(next(my_nums))
代码语言:javascript
复制
4
9
16
25

这时候生成器内部状态已经更新到第 5 个元素,即最后一个元素了,再用 next() 会发生什么呢?

代码语言:javascript
复制
print(next(my_nums))

果然,报错了!StopIteration 意思就是迭代 (iteration) 停止 (stop) 了。等等,迭代?生成器可以被迭代?那生成器可不可以叫做迭代器?可以的!至于迭代器是第 2 节的内容。

我们知道 for 循环就是遍历一个迭代器里的每个元素的,那试试用 for 循环来遍历生成器 my_nums。

代码语言:javascript
复制
my_nums = square([1, 2, 3, 4, 5])
for num in my_nums:    print(num)
代码语言:javascript
复制
1
4
9
16
25

总结:生成器可以用生成函数 (generator function) 来定义,记住要用 yield 而不是 return。

1.2第二种方法

复习〖Python 入门篇 (下)〗第 5 节回忆一下列表解析式。注意用中括号 [] 来定义列表解析式。

代码语言:javascript
复制
my_nums = [x*x for x in [1,2,3,4,5]]print(my_nums)
代码语言:javascript
复制
[1, 4, 9, 16, 25]

要定义生成器,只需要把中括号 [] 换成小括号 ()。

代码语言:javascript
复制
my_nums = (x*x for x in [1,2,3,4,5])print(my_nums)
代码语言:javascript
复制
<generator object <genexpr> at 0x00000179D3C4B048>
代码语言:javascript
复制
print(list(my_nums))
代码语言:javascript
复制
[1, 4, 9, 16, 25]

总结:生成器可以用生成表达式 (generator expression) 来定义,记住和列表解析式很像,将 [] 改成 () 即可。

1.3生成器 vs 列表

两种用函数和表达式来定义生成器的方法介绍完了,现在思考,生成器好在哪里?好就好在生成器是按需求调用 (call-by-need) 的,你需要调用一个值,我就 yield 一个值,然后用 next() 更新内部状态,等待你下次调用。这套流程也称作惰性求值 (lazy evaluation),目的是最小化计算机要做的工作。

在大规模数据时,一次性处理往往抵消而且不方便,而惰性求值解决了这个问题,它把计算的具体步骤延迟到了要实际用该数据的时候。

接下来我们做个小实验,对比一下生成器和列表在运行简单操作时的用的时间占的内存

首先引入记录运行时间的 time 和占内存大小的 sys。

代码语言:javascript
复制
import timeimport sys

做 1 千万次 x+1 的操作,生成列表 l 和生成器 g。

代码语言:javascript
复制
%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))
代码语言:javascript
复制
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)。

代码语言:javascript
复制
from collections import Iterable
代码语言:javascript
复制
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
代码语言:javascript
复制
True
True
True
True
True
False
False

结果正常,str, tuple, list, dict, set 都是可迭代对象,而 int 和 float 不是。

判断方法二

在 Python 里万物皆对象,那么我们可以查看该对象对应的类里面的属性,用 dir() 函数。里面有 __iter__() 魔法方法 (magic method) 的对象就是可迭代对象。不清楚上面这些知识可参考〖Python 特别篇 - 面向对象编程〗一贴。

列表 [1, 2, 3] 里有 __iter__() 魔法方法,它是可迭代对象

代码语言:javascript
复制
print(dir([1,2,3]))

整数 123 里没有 __iter__() 魔法方法,它不是可迭代对象

代码语言:javascript
复制
print(dir(123))

2.2迭代器

可被 for 循环的列表、字典、元组、集合和字符串都是可迭代对象,但实际上 for 循环里真正的对象是迭代器

首先用 isinstance(x, Iterator) 来判断它们 5 个是不是迭代器

代码语言:javascript
复制
from collections import Iterator
代码语言:javascript
复制
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
代码语言:javascript
复制
False
False
False
False
False

都不是的!那它们怎么能被 for 循环呢?原来 for 循环先用 __iter__() 方法将它们都转成迭代器,再开始遍历它们的每个元素。

代码语言:javascript
复制
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
代码语言:javascript
复制
True
True
True
True
True

被 __iter__() 方法包装之后,列表、字典、元组、集合和字符串都是迭代器了。注意 iter(x) 和 x.__iter__() 是等价的,后者太难看因此习惯用前者。

既然能被迭代,那该对象里面肯定有 __next__() 魔法方法,要不然怎么可以一个元素一个元素往下走下去啊?来验证一波。

定义列表 nums,可以被 for 循环遍历。

代码语言:javascript
复制
nums = [1, 2, 3]
for num in nums:    print(num)
代码语言:javascript
复制
1
2
3

但是 nums 不是迭代器 (只是 for 循环在遍历前将其转换成迭代器了),用 next() 会不错,错误信息是“列表对象不是迭代器”。

代码语言:javascript
复制
print(next(nums))

用 iter() 将可迭代对象 nums 转换成迭代器 i_nums,打印其本身发现 i_nums 是 list_iterator,打印其下属性,找到了 __next__。

代码语言:javascript
复制
i_nums = iter(nums)print(i_nums)print(dir(i_nums))

除了 __next__,迭代器里还有 __iter__,而 __iter__ 是判断可迭代对象的标准。因此可得

只要是迭代器就是可迭代对象,但反过来不成立。

现在两者关系梳理清楚了吧,我们来用 next() 一个个打印迭代器 i_nums 里的值。

代码语言:javascript
复制
i_nums = iter(nums)
print( next(i_nums) )print( next(i_nums) )print( next(i_nums) )
代码语言:javascript
复制
1
2
3

如果打印 4 遍呢?

代码语言:javascript
复制
i_nums = iter(nums)
print( next(i_nums) )print( next(i_nums) )print( next(i_nums) )print( next(i_nums) )
代码语言:javascript
复制
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 循环来打印。

代码语言:javascript
复制
i_nums = iter(nums)
while True:    try:        print(next(i_nums))    except StopIteration:        break
代码语言:javascript
复制
1
2
3
  • 第 1 行:将列表转成迭代器。
  • 第 4-5 行:如果程序没报错,打印下一个元素
  • 第 6-7 行:如果程序报 StopIteration 错,说明已经遍历结束,用 break 语句跳出。

2.3

自定义迭代器

上节讲了用 iter() 函数可以将可迭代对象转换成迭代器,本节再介绍两种定义迭代器的方法:1. 用类;2. 生成器。

用类来定义

我们已经知道迭代器里面有 __iter__ 和 __next__ 方法,那我们只需在类里自定义这两个方法了。以 MyRange 为例。

代码语言:javascript
复制
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
  • 第 3-6 行:每个类里都要有 __init__ 来构建对象,参数 start 和 end 分别代表是首尾位置,将 start 赋值给 self.value,将 end 赋值给 self.end。
  • 第 7-8 行:因为我们会用 __next__ 来使得 MyRange 是个迭代器,那么 __iter__ 返回它本身就好了。
  • 第 10-15 行:第 11-12 行在停止条件时提出 StopIteration 。第 13-14 行是将现有状态的值 self.value 赋给变量 current,将 self.value 前进 1 步。第 15 行返回 current。

测试一下用 MyRange 类定义的迭代器 nums。

代码语言:javascript
复制
nums = MyRange(1, 5)
for num in nums:    print(num)
代码语言:javascript
复制
1
2
3
4

可以被 for 循环。

代码语言:javascript
复制
nums = MyRange(1, 5)
print(next(nums))print(next(nums))print(next(nums))print(next(nums))
代码语言:javascript
复制
1
2
3
4

也可以使用 next() 函数。

用生成器来定义

用类定义迭代器没毛病,就是代码太冗长了。而用生成器定义迭代器真的简洁,代码如下。

代码语言:javascript
复制
def range_generator(start, end):    current = start    while current < end:        yield current        current += 1

代码太简单了有没有。首先将 start 复制给 current,只要 current 比 end 小,就 yield 一个 current,再加个 1 更新它。

测试一下用 range_generator 生成器定义的迭代器 nums。

代码语言:javascript
复制
nums = range_generator(1, 5)
for num in nums:    print(num)
代码语言:javascript
复制
1
2
3
4

可以被 for 循环。

代码语言:javascript
复制
nums = range_generator(1, 5)
print(next(nums))print(next(nums))print(next(nums))print(next(nums))
代码语言:javascript
复制
1
2
3
4

也可以使用 next() 函数。


思考下面生成器的输出是什么?

代码语言:javascript
复制
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,顾名思义就知道它里面有很多迭代器的工具。

代码语言:javascript
复制
import itertools
代码语言:javascript
复制
print(dir(itertools))

count

代码语言:javascript
复制
counter = itertools.count()
代码语言:javascript
复制
print(next(counter))print(next(counter))print(next(counter))print(next(counter))
代码语言:javascript
复制
0
1
2
3

count() 就像是计数器,不停的往前更新。它可用在给不知道大小的数据标注索引。比如我们在收集交易数据,未来有多少个我们不知道,我们就可以用 count() 来不停更新索引值。

下列以 4 个数据点 (假设是阿里巴巴每分钟的股票价格) 为例,用 zip 函数来将 count() 的输出给股价标注索引。

代码语言:javascript
复制
data = [170.1, 170.8, 171.4, 170.5]
minute_data = zip( itertools.count(), data)print(minute_data)
代码语言:javascript
复制
<zip object at 0x00000179D362FC88>

这时得到是个 zip 对象,可将其转换成 list 打印其内容。

代码语言:javascript
复制
print(list(minute_data))
代码语言:javascript
复制
[(0, 170.1), (1, 170.8), (2, 171.4), (3, 170.5)]

data 里面有 4 个点,count() 就返回 4 个值,如果data 里面有 4000000 个点,count() 就返回 4000000 个值。体会到使用 count() 的便利了吗?

当然,我们可以设置起始值间隔值,请参考下面两段代码。

代码语言:javascript
复制
counter = itertools.count(start=5)
print(next(counter))print(next(counter))print(next(counter))print(next(counter))
代码语言:javascript
复制
5
6
7
8
代码语言:javascript
复制
counter = itertools.count(start=5, step=-2)
print(next(counter))print(next(counter))print(next(counter))print(next(counter))
代码语言:javascript
复制
5
3
1
-1

cycle

代码语言:javascript
复制
cycle = itertools.cycle(('on','off'))
print(next(cycle))print(next(cycle))print(next(cycle))print(next(cycle))
代码语言:javascript
复制
on
off
on
off

cycle() 作用是循环遍历!

repeat

代码语言:javascript
复制
repeat = itertools.repeat(2, times=3)
print(next(repeat))print(next(repeat))print(next(repeat))print(next(repeat))
代码语言:javascript
复制
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) 匹配。

代码语言:javascript
复制
square = map( pow, range(5), itertools.repeat(2) )square
代码语言:javascript
复制
<map at 0x179d3724908>
代码语言:javascript
复制
list(square)
代码语言:javascript
复制
[0, 1, 4, 9, 16]

combinations & permutations

接下来看看用于排列 (permutation) 和组合 (combination) 的迭代器。

首先创建一个列表 letters,一个元组 numbers 和一个集合 names。

代码语言:javascript
复制
letters = ['a', 'b', 'c', 'd']numbers = (1, 2, 3, 4)names = {'Steven', 'Sherry'}

从 letters 里面 4 个元素取出 2 个来组合 (元素位置不重要)。

代码语言:javascript
复制
results = itertools.combinations(letters, 2)
for items in results:    print(items)
代码语言:javascript
复制
('a', 'b')
('a', 'c')
('a', 'd')
('b', 'c')
('b', 'd')
('c', 'd')

从 letters 里面 4 个元素取出 2 个来排列 (元素位置重要)。

代码语言:javascript
复制
results = itertools.permutations(letters, 2)
for items in results:    print(items)
代码语言:javascript
复制
('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 个,不同位置元素可以重复。

代码语言:javascript
复制
results = itertools.product(numbers, repeat=2)
for items in results:    print(items)
代码语言:javascript
复制
(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() 的结果,验证如下。

代码语言:javascript
复制
results = itertools.combinations_with_replacement(numbers, 2)
for items in results:    print(items)
代码语言:javascript
复制
(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() 的结果验证如下。

代码语言:javascript
复制
results = itertools.combinations(numbers, 2)
for items in results:    print(items)
代码语言:javascript
复制
(1, 2)
(1, 3)
(1, 4)
(2, 3)
(2, 4)
(3, 4)

chain

用于三个不同类型的数据格式,列表 letters,元组 numbers 和集合 names,我们可用 chain() 将它们串起来成一个迭代器,再逐个遍历它里面的元素。

代码如下。

代码语言:javascript
复制
results = itertools.chain(letters, numbers, names)
for items in results:    print(items)
代码语言:javascript
复制
a
b
c
d
1
2
3
4
Sherry
Steven

3.总结

在 Python 里,

  • 字典用来创建映射关系
  • 函数用来创建可调用对象
  • 生成器用来创建迭代器

当你想要个可用惰性计算的可迭代对象时,考虑用迭代器

当你想创建迭代器时,考虑用生成器

当你想创建生成器时,考虑用生成函数 (用 yield) 或生成表达式 (用小括号 ())。

当你想 ...,别想了,在看-转发-留言吧,千万不要赞赏我。下篇讲装饰器 (Decorator)。

Stay Tuned!

下一篇
举报
领券