yield是什么魔鬼生成器

生成器的理解

生成器与列表相关
我们知道一行代码就可以生成一个列表 [x for x in range(num)]

1
2
my_list = [[x, x % 2] for x in range(10)]
print(my_list)

[[0, 0], [1, 1], [2, 0], [3, 1], [4, 0], [5, 1]]

1
2
my_list = ([x, x % 2] for x in range(6))
print(my_list)

<generator object at 0x069B8970>

第二个代码输出了一个 generator(生成器) 对象
区别是:
第一个代码用的[]
第二个代码用的()
这就是生成器,不直接输出列表
可以通过 next() 方法调用这个生成器里的元素

1
2
3
4
5
6
7
print(next(my_list))
print(next(my_list))
print(next(my_list))
print(next(my_list))
print(next(my_list))
print(next(my_list))
print(next(my_list))

[0, 0]
[1, 1]
[2, 0]
[3, 1]
[4, 0]
[5, 1]

这有什么区别呢?
现在把运算加大,
考虑这两行代码的运行时间,谁长谁短呢?
来个装饰器计时,看看两者的区别

1
2
my_list1 = [[x, x % 2] for x in range(6666666)]
my_list2 = ([x, x % 2] for x in range(6666666))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import time

def log_func_time(func):
def wrapper():
start_time = time.perf_counter()
result = func()
end_time = time.perf_counter()
print('方法 {} , 消耗了 {} ms'.format(func.__name__, (end_time - start_time) * 1000))
return result

return wrapper

@log_func_time
def calculate_func_1():
my_list1 = [[x, x % 2] for x in range(6666666)]

@log_func_time
def calculate_func_2():
my_list2 = ([x, x % 2] for x in range(6666666))

calculate_func_1()
calculate_func_2()

方法 calculate_func_1 , 消耗了 3356.3896 ms
方法 calculate_func_2 , 消耗了 0.0181000000001319 ms

从返回结果看出,方法二,也就是生成器所消耗的时间远小于方法一
这是因为
方法一:直接生成了 6666666 个元素并封闭进列表,耗时并占用大量内存
方法二:只记录了设定的算法规则,这里的 for x in range(6666666) 就是不断 +1 的规则
生成器只需记住这个规则,不需要一次性地将列表封装,
等在需要的时候,再按规则运算一下,然后将结果返回
几乎不占内存

总结,这就是生成器
它是根据一定的规律算法生成的,
当我们去遍历它的时候,
它可以通过特定的算法不断的推算出相应的元素,
边运行边推算结果,从而节省了很多空间。

继续体会这两者的区别
方法1将结果直接存入列表,for 循环只是将现存列表简单遍历打印出来
方法2是需要的时候才运算提取,每次 for 循环调用一次规则:([x, x % 2] for x in range(15))

1
2
3
4
5
6
7
8
9
10
11
@log_func_time
def calculate_func_1():
my_list1 = [[x, x % 2] for x in range(15)]
for item in my_list1:
print(item)

@log_func_time
def calculate_func_2():
my_list2 = ([x, x % 2] for x in range(15))
for item in my_list2:
print(item)

魔法关键词 yield

yield 与 return 关键词都是运用于函数中返回值
return 在函数中执行这个关键词就会返回相应的结果终止函数的运行

1
2
3
4
5
6
7
def foo():
a = 1
b = 2
return a + b
print("ooxx-ooxx-oooooxxxxx.....")

print(foo())

3

函数遇到 return 关键字就返回,而后的语句不再被执行到

再来看 yield 关键字

1
2
3
4
5
6
7
8
9
10
11
12
def foo():
print('1111')
yield
print('2222')
yield
print('3333')
yield
print('4444')
yield

f = foo()
print(type(f))

<class ‘generator’>

直接调用生成器函数没有返回结果
打印出对象类型为 generator(生成器)

一个函数里定义了 yield 关键字,那么这个函数就是一个生成器

可以用 next 方法使用生成器

1
2
3
4
5
next(f)
next(f)
next(f)
next(f)
next(f)

1111
2222
3333
4444
StopIteration
前四个 next 依次返回打印
第五个 next 没有数据了,返回 StopIteration

再遍历一下看看:

1
2
for item in f:
print(item)

1111
None
2222
None
3333
None
4444
None

返回了 None, 这是因为 yield 行并没有具体返回结果

再来看 yield 后加上返回内容的情形:

1
2
3
4
5
6
7
8
9
10
def foo():
yield '第1次 yield'
yield '第2次 yield'
yield '第3次 yield'
yield '第4次 yield'

f = foo()

for item in f:
print(item)

第1次 yield
第2次 yield
第3次 yield
第4次 yield

以上仅仅展示简单的示例,也求快速理解 yield
生成器是按一定的规律算法生成的列表
当我们去遍历它的时候,它可以通过特定的算法不断的推算出相应的元素,
边运行边推算结果,从而节省了很多空间。

所以 yield 一般是用在 for 循环中以一定规则生成
简单运用举例:输出18以内的偶数

1
2
3
4
5
6
def foo():
for num in range(10):
yield num * 2

for item in foo():
print(item)

0
2
4
6
8
10
12
14
16
18

另外,yield 后面可以跟常用的数据类型,
比如:string、int、dict 等
比如:

1
2
3
4
5
yield {
'text': text,
'class_attrib': format_html(' class="column-{}', field_name),
'sortable': False
}

再比如 Scrapy 爬虫框架中经常要 yield item

1
2
3
4
for img in response.xpath('//img[@referrerpolicy]'):
item['alt'] = img.xpath('./@alt').extract()
item['data_original'] = img.xpath('./@data-original').extract()
yield item