Python三大利器 — 生成器


什么是生成器

之前我们学习过迭代器,它的好处之一就是可以节省能存,在某些情况下,我们需要自己定义一个方法去实现迭代器功能,这个方法就是生成器。
在Python中生成器又分成两类:

  1. 生成器函数
  2. 生成器表达式

生成器函数

生成器Generator:
  本质:迭代器(所以自带了iter方法和next方法,不需要我们去实现)
  特点:惰性运算,开发者自定义

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#   只要含有yield关键字的函数都是生成器函数
# yield 必须写在函数里,且无法和return共用
def generator():
a = 1
yield a
b = 2
yield b

g = generator() # 得到一个“生成器”作为返回值
print(g) # <generator object generator at 0x0000000001E9A308> generator 生成器
# g.__next__ # 生成器带有__next__方法和__iter__方法
# g.__iter__ # 生成器是迭代器 用next方法取值
print(g.__next__()) # 1
print(g.__next__()) # 2

运行过程总结:

  1. 由于函数中有yield,所以现在内存中会有一个生成器函数 generator
  2. g = generator() 发生了函数调用,生成器函数的特点:函数中的代码不执行
  3. g 得到了一个生成器
  4. 生成器里面即有iter方法也有next方法,说明它其实是一个迭代器
  5. 生成器就可以使用next方法取值,这时程序才第一次触发了生成器里面的代码
  6. yield 不会结束函数,return会直接结束

生成器函数的使用

生成器的最大好处就是不会在内存中一次性的生成所有数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def factoy():
for i in range(100):
yield '生成%s次'%i

g = factoy()
# __next__() ,一次一次的提取
print(g.__next__())
print(g.__next__())
print(g.__next__())

# for循环遍历提取
for i in g:
print(i)

# 取50次
g = factoy()
count = 0
for i in g:
count += 1
print(i)
if count > 50:
break
print('*****',g.__next__()) # ***** 生成51次 可以继续从生成器中取值

列表为什么不能继续取值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 列表是可迭代的,并不是一个迭代器,在两次for循环的时候会产生两个迭代器
# for循环自动将可迭代的转换成迭代器
l = [1,2,3,4,5]
for i in l:
print(i) # 1,2
if i == 2:
break

for i in l:
print(i) # 1,2,3,4,5

# 获取两个生成器
l = [1,2,3,4,5]
def generator():
for i in l:
yield i
g = generator()
g1 = generator()
print(g,g1) # <generator object generator at 0x0000000001F7A150> <generator object generator at 0x0000000001F7A200>
print(g.__next__()) # 1
print(g1.__next__()) # 1 拿到两个生成器,自己执行自己的

监听文件的输入

1
2
3
4
5
6
7
8
9
10
11
12
def tail(filename):
f = open(filename, encoding='utf-8')
while True:
line = f.readline() # 每次读一行
if line.strip(): # 不为空就打印
# print('****',line.strip('\n'))
yield line.strip() # 返回这行

g = tail('file') # 获取生成器
for i in g:
if 'python' in i:
print('*****',i,'*****') # 可以对这个结果做任何操作了,用生成器实现就可以想要的结果

爬虫时的使用

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
def parse_one_page(html):
rule = re.compile(
'<dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)".*?name.*?a.*?>(.*?)</a>.*?star.*?>(.*?)</p>.*?'
'releasetime.*?>(.*?)</p>.*?integer.*?>(.*?)</i>.*?fraction.*?>(.*?)</i>.*?</dd>'
,re.S)
items = re.findall(rule, html) # 通过findall方法根据规则得到html文本
# print(items)
for item in items:
yield {
'index':item[0],
'image':item[1],
'title':item[2].strip(),
'actor':item[3].strip(),
'time':item[4].strip(),
'score':item[5].strip() + item[6].strip()
}

# 循环整个html文本列表,每一条数据都生成yield返回一个字典,里面拼接成想要的数据类型
# 使用的时候传递一个页面进去,循环调用生成器,item里就是生成的每条数据

def main(offset):
url = 'https://maoyan.com/board/4?offset=' + str(offset)
html = get_one_page(url)
# print(html)
for item in parse_one_page(html):
print(item)
write_to_file(item)

生成器函数的进阶

数据类型的强制转换 — 列表(生成器)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def generator():
for i in range(20):
yield 'wahaha%s'%i
g = generator()
# list(g)
# list是列表,代表将g生成器直接转换成列表,列表中的每一个值都是实际存在的
# 一个一个从生成器里取出来,全部取完放入列表,列表会在内存中生成
print(list(g))
# ['wahaha0', 'wahaha1', 'wahaha2', 'wahaha3'...'wahaha19']

# 从生成器取值的几个方法:
# next
# for
# 数据类型的强制转换 (不推荐,占用内存)
1
2
3
4
5
6
7
8
9
10
11
12
def generator():
print(123)
yield 1
print(456)
yield 2
print(789)

g = generator() # 得到一个生成器
ret = g.__next__()
print('***',ret) # 先打印123,然后拿到yield返回的1
print('***',ret)
print('***',ret) # 执行了789,由于后面没有yield,会报错StopIteration

生成器函数 — send

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def generator():
print(123)
send_msg = yield 1
print('======',send_msg)
yield 2

g = generator()
ret = g.__next__()
print('***',ret)
ret = g.send('send_hello')
print('***',ret)

# 123
# *** 1
# ====== send_hello
# *** 2

# send用法总结
# 1. send的获取下一个值的效果与next基本一致
# 2. 只是在获取下一个值的时候给上一个yield的位置,传递一个数据

# 使用send的注意事项
# 1. 第一次使用生成器的时候,必须使用next获取下一个值
# 2. 最后一个yield 不能接收外部的值,但是可以在接收arg=yield 2...最后返回一个空yield

send实例 — 计算移动平均值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 接收一次值计算平均值
# 移动平均值
# num: 10 20 30
# avg: 10 15 20
# 公式: avg = num / count

def avg_generator():
sum = 0
count = 0
avg = 0

num = yield # 第一次返回空,为了后面send传值(num)进来,10
sum += num # 总数有更新 10
count += 1 # 次数更新
avg = sum / count
yield avg # send执行到这

avg_g = avg_generator()
avg_g.__next__() # 使用send第一次必须next,得到空
avg1 = avg_g.send(10) # 传值(num)10进去
print(avg1)

那么如何多次计算呢,需要加上循环

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
# 移动平均值
# num: 10 20 30
# avg: 10 15 20
# 公式: avg = num / count

def avg_generator():
sum = 0
count = 0
avg = 0
while 1:
# num = yield # 第一次返回空,为了后面send传值(num)进来,10
num = yield avg # 第一次的avg = 0 ,num = 传值
sum += num # 总数有更新 10
count += 1 # 次数更新
avg = sum / count

avg_g = avg_generator()
avg_g.__next__() # 使用send第一次必须next,得到空
avg = avg_g.send(10) # 传值(num)10进去
avg = avg_g.send(20)
avg = avg_g.send(30)
print(avg)

# 每次计算方法:
# 如果我加上while循环,现在我有两个yield,第一次结束到yield avg,第二次执行什么?
# 如果执行next num = yield 相当于 num = 0
# 下面再用一次send 传值20 再返回打印

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

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
# 计算移动平均值
# 用装饰器 激活__next__()
def init(func):
def inner(*args,**kwargs):
g = func(*args,**kwargs) # g = generator() 拿到装饰器
g.__next__() # 执行__next__()
return g # 返回装饰器
return inner

@init # avg_generator = init(avg_generator) ==> inner
def avg_generator():
sum = 0
count = 0
avg = 0
while True:
# num = yield
num = yield avg # num = 10,20,30
sum += num # sum = 10,30,60
count += 1 # count = 1,2,3
avg = sum / count # avg = 10,15,20

g = avg_generator() # inner() # 执行这里 得到一个执行过next的装饰器
# g.__next__() # 我不在这调用 而是在装饰器里
avg = g.send(10) # 开始向生成器里里传值
avg = g.send(20)
avg = g.send(30)
print(avg)

yield from

yield from : 从一个容器类型里取值,不需要一个个返回,而是集体返回接收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# python 3
# 将结果按个返回
def generator():
a = 'abcde'
b = '12345'
# 单个字符串返回
for i in a:
yield i
for i in b:
yield i

g = generator()
# print(g.__next__())
for i in g:
print(i)

1
2
3
4
5
6
7
8
9
10
11
12
13
# yield from 将结果按个返回
def generator():
a = 'abcde'
l = [1,2,3,4,5]
# 单个字符串返回
yield from a # 生成器函数语法
yield from l

g = generator()
for i in g:
print(i)

# yield from 从一个容器类型里取值,不需要一个个返回,而是集体返回接收
1
2
3
4
5
6
7
# 将两个类型的数据list转化成同一个
def generator():
yield from range(0,5)
yield from 'abcde'

l = list(generator())
print(l) # [0, 1, 2, 3, 4, 'a', 'b', 'c', 'd', 'e']

生成器表达式

列表推导式

我们先写一个获取鸡蛋的程序

1
2
3
4
egg_list = []
for i in range(10):
egg_list.append('鸡蛋%s'%i)
print(egg_list)

在这里循环获取得到一个鸡蛋筐(列表),里面存着10个鸡蛋,列表推导式的写法如下

1
2
3
4
5
egg_list = ['鸡蛋%s' %i for i in range(10)]
print(egg_list)
# 1. for i in range(10) 循环
# 2. 将想要的 放在for前面
# 3. 用列表括起来

列表推导式可以做一些简单的循环工作,那么这个时候我们就想,列表生成后可是存在内存里的,那如果是大数据怎么办,很占用内存,占用内存我们就想到了 生成器

生成器推导式

生成器表达式 与 列表表达式 的不同

  1. 括号不一样
  2. 返回的值不一样
  3. 列表推导式得到的还是一个列表,一次性得到所有的值,占用内存
  4. 生成器表达式几乎不占用内存,但是不能直接应用,需要遍历循环取值,程序应该更关心内存
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    # 生成器表达式
    g = (i for i in range(10))
    print(g) # <generator object <genexpr> at 0x0000000001EB92B0> 生成器
    for i in g:
    print(i)

    # 获取鸡蛋例子
    egg_g = ('鸡蛋%s'%i for i in range(10)) # 生成器表达式
    for i in egg_g: # 相当于老母鸡,然后下蛋
    print(i)

    # 每个数字都取平方
    # g里面的代码一句话没执行,直到for循环取值__next__,for循环每走一次,上面的range10的循环才走一次
    g = (i*i for i in range(10))
    for i in g:
    print(i)

    #列表解析
    sum([i for i in range(100000000)])#内存占用大,机器容易卡死
    #生成器表达式
    sum(i for i in range(100000000))#几乎不占内存

迭代器与生成器总结

可迭代对象:

  1. 拥有__iter__方法
  2. 特点:惰性运算
    例如: range(), str, list, tuple, dict, set

迭代器Iterator:

  1. 拥有__iter__方法和__next__方法
    例如: iter(range()), iter(str), iter(list), iter(tuple), iter(dict), iter(set), reversed(list_o), map(func,list_o), filter(func, list_o), file_o

生成器Generator:
本质:迭代器,所以拥有__iter__方法和__next__方法
特点:惰性运算, 开发者自定义

使用生成器的优点:

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