Python 基础之生成器和迭代器

背景

假如我现在有一个列表 l=['a', 'b', 'c', 'd', 'e'],我想取列表中的内容,有几种方式?

首先,我可以通过索引取值 l[0],其次我们是不是还可以用for循环来取值呀?

你有没有仔细思考过,用索引取值和for循环取值是有着微妙区别的。

如果用索引取值,你可以取到任意位置的值,前提是你要知道这个值在什么位置。

如果用for循环来取值,我们把每一个值都取到,不需要关心每一个值的位置,因为只能顺序的取值,并不能跳过任何一个直接去取其他位置的值。

但你有没有想过,我们为什么可以使用for循环来取值?for循环内部是怎么工作的呢?

迭代器

Python 中的 for 循环

要了解for循环是怎么回事儿,还是要从代码的角度出发。首先,我们对一个列表进行for循环。

1
2
for i in [1,2,3,4]:  
print(i)

上面这段代码肯定是没有问题的,但是我们换一种情况,来循环一个数字1234试试

1
2
3
4
5
6
7
8
for i in 1234
print(i)

结果:
Traceback (most recent call last):
File "test.py", line 4, in <module>
for i in 1234:
TypeError: 'int' object is not iterable

报错了TypeError: 'int' object is not iterable,说int类型不是一个iterable(可迭代的)

迭代和可迭代协议

迭代

首先,从报错来分析,好像之所以 1234 不可以 for 循环,是因为它不可迭代。那么如果“可迭代”,就应该可以被 for 循环了。

这个我们知道呀,字符串、列表、元组、字典、集合都可以被for循环,说明他们都是可迭代的。我们怎么来证明这一点呢?

1
2
3
4
5
6
7
8
9
10
11
from collections import Iterable

l = [1,2,3,4]
t = (1,2,3,4)
d = {1:2,3:4}
s = {1,2,3,4}

print(isinstance(l,Iterable)) # true
print(isinstance(t,Iterable)) # true
print(isinstance(d,Iterable)) # true
print(isinstance(s,Iterable)) # true

综上,迭代就是我们刚刚说的,可以将某个数据集内的数据“一个挨着一个的取出来”,就叫做迭代

可迭代协议

但是,for怎么知道谁是可迭代的呢?那就是实现了可迭代协议

可迭代协议,就是内部实现了__iter__方法。

接下来我们就来验证一下:

1
2
3
4
print(dir([1,2]))   # 结果太长省略,结果的列表中有 '__iter__'
print(dir((2,3))) # 结果太长省略,结果的列表中有 '__iter__'
print(dir({1:2})) # 结果太长省略,结果的列表中有 '__iter__'
print(dir({1,2})) # 结果太长省略,结果的列表中有 '__iter__'

总结一下我们现在所知道的:可以被for循环的都是可迭代的,要想可迭代,内部必须有一个__iter__方法。那么,__iter__方法做了什么事情呢?

1
print([1,2].__iter__())     # <list_iterator object at 0x1024784a8>

执行了 list([1,2])__iter__方法,我们得到了一个 list_iterator,现在我们又得到了一个新名词iterator(计算机中的专属名词,叫做迭代器)。

迭代器协议

虽然我们不知道什么叫迭代器,但是我们现在已经有一个迭代器了,一个列表的迭代器。

我们来看看这个列表的迭代器比起列表来说实现了哪些新方法

1
2
3
# print(dir([1,2].__iter__()))                      # dir([1,2].__iter__())是列表迭代器中实现的所有方法,dir([1,2])是列表中实现的所有方法,都是以列表的形式返回给我们的
# print(dir([1,2])) # 为了看的更清楚,我们分别把他们转换成集合,然后取差集。
print(set(dir([1,2].__iter__()))-set(dir([1,2]))) # {'__length_hint__', '__next__', '__setstate__'}

我们看到在列表迭代器中多了三个方法,那么这三个方法都分别做了什么事呢?

1
2
3
4
5
iter_l = [1,2,3,4,5,6].__iter__()
print(iter_l.__length_hint__()) # 获取迭代器中元素的长度
print('*',iter_l.__setstate__(4)) # 根据索引值指定从哪里开始迭代
print('**',iter_l.__next__()) # 一个一个的取值
print('***',iter_l.__next__())

这三个方法中,能让我们一个一个取值的神奇方法是谁?没错!就是__next__

在for循环中,就是在内部调用了 __next__方法才能取到一个一个的值。

那接下来我们就用迭代器的next方法来写一个不依赖for的遍历。

1
2
3
4
5
6
7
8
9
10
11
12
l = [1,2,3,4]
l_iter = l.__iter__()
item = l_iter.__next__()
print(item)
item = l_iter.__next__()
print(item)
item = l_iter.__next__()
print(item)
item = l_iter.__next__()
print(item)
item = l_iter.__next__()
print(item)

这是一段会报错的代码,如果我们一直取next取到迭代器里已经没有元素了,就会抛出一个异常StopIteration,告诉我们,列表中已经没有有效的元素了。

这个时候,我们就要使用异常处理机制来把这个异常处理掉。

1
2
3
4
5
6
7
8
l = [1,2,3,4]
l_iter = l.__iter__()
while True:
try:
item = l_iter.__next__()
print(item)
except StopIteration:
break

那现在我们就使用while循环实现了原本for循环做的事情,我们是从谁那儿获取一个一个的值呀?是不是就是l_iter?好了,这个l_iter就是一个迭代器。

迭代器遵循迭代器协议:必须拥有__iter__方法和 __next__ 方法。

关于迭代器和生成器的方法我们清楚了两个,最后我们来看看range()是个啥。首先,它肯定是一个可迭代的对象,但是它是否是一个迭代器?我们来测试一下

1
2
3
4
5
print('__next__' in dir(range(12)))    # 查看'__next__'是不是在range()方法执行之后内部是否有__next__
print('__iter__' in dir(range(12))) # 查看'__next__'是不是在range()方法执行之后内部是否有__iter__

from collections import Iterator
print(isinstance(range(100000000),Iterator)) # 验证range执行之后得到的结果不是一个迭代器

为什么要有for循环

基于上面讲的列表这一大堆遍历方式,聪明的你立马看除了端倪,有了下标的访问方式,我可以这样遍历一个列表啊

1
2
3
4
5
l=[1,2,3]
index=0
while index < len(l):
print(l[index])
index+=1

没错,序列类型字符串,列表,元组都有下标,你用上述的方式访问,perfect!但是你可曾想过非序列类型像字典,集合,文件对象的感受,所以,for循环就是基于迭代器协议提供了一个统一的可以遍历所有对象的方法,即在遍历之前,先调用对象的__iter__方法将其转换成一个迭代器,然后使用迭代器协议去实现循环访问,这样所有的对象就都可以通过for循环来遍历了,而且你看到的效果也确实如此,这就是无所不能的for循环,而且for循环自带异常处理,当__next__取完值后的报错StopIteration,for循环会自动处理

1
2
3
4
# 迭代器的好处:
# 从容器类型中一个一个的取值,会把所有的值都取到,而且只取一次
# 节省内存空间
# 迭代器并不会在内存中再占用一大块内存,而是随着循环 每次生成一个,每执行一次next,生产一个

生成器

初识生成器

我们知道的迭代器有两种:一种是调用方法直接返回的,一种是可迭代对象通过执行 iter 方法得到的,迭代器有的好处是可以节省内存。 如果在某些情况下,我们也需要节省内存,就只能自己写。我们自己写的这个能实现迭代器功能的东西就叫生成器。

Python中提供的生成器:

  1. 生成器函数:常规函数定义,但是,使用 yield 语句而不是 return 语句返回结果。yield语句一次返回一个结果,在每个结果中间,挂起函数的状态,以便下次重它离开的地方继续执行
  2. 生成器表达式:类似于列表推导,但是,生成器返回按需产生结果的一个对象,而不是一次构建一个结果列表

生成器Generator

本质:迭代器(所以自带了__iter__方法和__next__方法,不需要我们去实现)

特点:惰性运算,开发者自定义

生成器的优点:

  1. 延迟计算,一次返回一个结果。也就是说,它不会一次生成所有的结果,这对于大数据量处理,将会非常有用。
  2. 提高代码可读性
  3. 节约内存,不会一次生成所有数据而占用空间

yield

yieldreturn 一样,执行到这里时返回相应值,并且hang住,等待下一次的执行 __next__

yield from

1
2
3
4
5
6
7
8
9
10
11
12
13
def gen1():
for c in 'AB':
yield c
for i in range(3):
yield i

print(list(gen1())) # ['A', 'B', 0, 1, 2]

def gen2():
yield from 'AB'
yield from range(3)

print(list(gen2())) # ['A', 'B', 0, 1, 2]

send

send 是先把值赋值给上一次的hang住的 yield,然后往下执行__next__

注意:1、第一次使用next,2、最后一次yield收不到值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def generator():
print(123)
content = yield 1
print('=======',content)
print(456)
yield2

g = generator()
ret = g.__next__()
print('***',ret)
ret = g.send('hello') # send的效果和next一样
print('***',ret)

# send 获取下一个值的效果和 next 基本一致
# 只是在获取下一个值的时候,给上一 yield 的位置传递一个数据
# 使用 send 的注意事项
# 第一次使用生成器的时候 是用 next 获取下一个值
# 最后一个 yield 不能接受外部的值

生成器函数

一个包含 yield 关键字的函数就是一个生成器函数。yield可以为我们从函数中返回值,但是yield又不同于return,return的执行意味着程序的结束,调用生成器函数不会得到返回的具体的值,而是得到一个可迭代的对象。每一次获取这个可迭代对象的值(也就是执行 next),就能推动函数的执行,获取新的返回值。直到函数执行结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import time
def genrator_fun1():
a = 1
print('现在定义了a变量')
yield a
b = 2
print('现在又定义了b变量')
yield b

g1 = genrator_fun1()
print('g1 : ',g1) # 打印g1可以发现g1就是一个生成器
print('-'*20) # 我是华丽的分割线
print(next(g1))
time.sleep(1) # sleep一秒看清执行过程
print(next(g1))

生成器有什么好处呢?就是不会一下子在内存中生成太多数据

假如我想让工厂给学生做校服,生产2000000件衣服,我和工厂一说,工厂应该是先答应下来,然后再去生产,我可以一件一件的要,也可以根据学生一批一批的拿。

而不能是一说要生产2000000件衣服,工厂就先去做2000000件衣服,等回来做好了,学生都毕业了。。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 初识生成器二
def produce():
"""生产衣服"""
for i in range(2000000):
yield "生产了第%s件衣服"%i

product_g = produce()
print(product_g.__next__()) # 要一件衣服
print(product_g.__next__()) # 再要一件衣服
print(product_g.__next__()) # 再要一件衣服
num = 0
for i in product_g: # 要一批衣服,比如5件
print(i)
num +=1
if num == 5:
break

# 到这里我们找工厂拿了8件衣服,我一共让我的生产函数(也就是produce生成器函数)生产2000000件衣服。
# 剩下的还有很多衣服,我们可以一直拿,也可以放着等想拿的时候再拿

更多应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import time
def tail(filename):
f = open(filename)
f.seek(0, 2) # 从文件末尾算起
while True:
line = f.readline() # 读取文件中新的文本行
if not line:
time.sleep(0.1)
continue
yield line

tail_g = tail('tmp')
for line in tail_g:
print(line)

计算移动平均值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def averager():
total = 0.0
count = 0
average = None
while True:
term = yield average
total += term
count += 1
average = total/count

g_avg = averager()
next(g_avg)
print(g_avg.send(10))
print(g_avg.send(30))
print(g_avg.send(5))

计算移动平均值(2)_预激协程的装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def init(func):                   # 在调用被装饰生成器函数的时候首先用next激活生成器
def inner(*args,**kwargs):
g = func(*args,**kwargs)
next(g)
return g
return inner

@init
def averager():
total = 0.0
count = 0
average = None
while True:
term = yield average
total += term
count += 1
average = total/count

g_avg = averager()
# next(g_avg) 在装饰器中执行了next方法
print(g_avg.send(10))
print(g_avg.send(30))
print(g_avg.send(5))

列表推导式(list Comprehension)和生成器表达式

List Comprehension ,列表生成式(又称列表推导式、列表解析式)

1
2
3
4
5
6
list = ['%s' %i for i in range(10)]     # 列表推导式
laomuji = ('%s' %i for i in range(10)) # 生成器表达式
print(laomuji)
print(next(laomuji)) # next本质就是调用__next__
print(laomuji.__next__())
print(next(laomuji))
  1. 把列表推导式的[]换成()得到的就是生成器表达式
  2. 列表推导式与生成器表达式都是一种便利的编程方式,只不过生成器表达式更节省内存
  3. 生成器表达式是按需计算(或称惰性求值,延迟计算)需要的时候才计算
  4. 列表推导式是立即返回值

Python不但使用迭代器协议,让for循环变得更加通用。大部分内置函数,也是使用迭代器协议访问对象的。例如,sum 函数是 Python 的内置函数,该函数使用迭代器协议访问对象,而生成器实现了迭代器协议,所以,我们可以直接这样计算一系列值的和:

1
sum(x ** 2 for x in range(4))

而不用多此一举的先构造一个列表:

1
sum([x ** 2 for x in range(4)])

各种推导式详解

推导式的套路

除列表推导式和生成器表达式之外,其实还有字典推导式、集合推导式等等。

下面是一个以列表推导式为例的推导式详细格式,同样适用于其他推导式。

1
2
3
4
variable = [out_exp_res for out_exp in input_list if out_exp == 2]
out_exp_res: 列表生成元素表达式,可以是有返回值的函数。
for out_exp in input_list:迭代input_list将out_exp传入out_exp_res表达式中。
if out_exp == 2:根据条件过滤哪些值可以。

列表推导式[]

例一:30以内所有能被3整除的数

1
2
multiples = [i for i in range(30) if i % 3 is 0]
print(multiples) # Output: [0, 3, 6, 9, 12, 15, 18, 21, 24, 27]

例二:30以内所有能被3整除的数的平方

1
2
3
4
def squared(x):
return x*x
multiples = [squared(i) for i in range(30) if i % 3 is 0]
print(multiples)

例三:找到嵌套列表中名字含有两个‘e’的所有名字

1
2
3
names = [['Tom', 'Billy', 'Jefferson', 'Andrew', 'Wesley', 'Steven', 'Joe'],
['Alice', 'Jill', 'Ana', 'Wendy', 'Jennifer', 'Sherry', 'Eva']]
print([name for lst in names for name in lst if name.count('e') >= 2]) # 注意遍历顺序,这是实现的关键

字典推导式{}

例一:将一个字典的key和value对调

1
2
3
mcase = {'a': 10, 'b': 34}
mcase_frequency = {mcase[k]: k for k in mcase}
print(mcase_frequency)

例二:合并大小写对应的value值,将k统一成小写

1
2
3
mcase = {'a': 10, 'b': 34, 'A': 7, 'Z': 3}
mcase_frequency = {k.lower(): mcase.get(k.lower(), 0) + mcase.get(k.upper(), 0) for k in mcase.keys()}
print(mcase_frequency)

集合推导式{}

例:计算列表中每个值的平方,自带去重功能

1
2
3
squared = {x**2 for x in [1, -1, 2]}
print(squared)
# Output: set([1, 4])

注:迭代器和生成器可以切片

1
2
3
L = range(100)
l1 = list(range(100)[0:3]) # [0, 1, 2],推荐你使用这个,占用内存少
l1 = list(range(100))[0:3] # [0, 1, 2]

Python 基础之生成器和迭代器
https://flepeng.github.io/021-Python-42-核心概念-Python-基础之生成器和迭代器/
作者
Lepeng
发布于
2021年7月30日
许可协议