目录
生成式
- 列表生成式
- 字典生成式
- 集合生成式
- 生成式的嵌套
生成器
- 生成器和生成式的差异
- 写一个生成器函数
- next 和 send 函数
生成式
Python的这个叫做生成式(也有叫推导式的)的东西,实际上是对标准 for 循环的一种简写语法。生成式与等价的 for 循环可以完成相同的工作,区别仅在于生成式执行的更快,代码更简洁。并不是所有 for 循环都可以简写成生成式(如果是的话还要for循环这个东西干嘛),只有符合一定模式的才可以。常见的生成式有列表生成式、字典生成式和集合生成式(其实集合生成式不太常见-_-)。你不要看前面列表、字典、集合都有生成式就想着给元组也搞一个,我告诉你这事不行!因为元组的数据特点决定了它不配拥有生成式,就像你不刷十万就不配线下见到乔碧罗一样(狗头)…
列表生成式
看名字就知道这是一个用来生成、创建列表的一个式子。没错,列表生成式常用来从一个现有序列里生成一个新的列表。那如果我想创建一个元素1-10的列表我该咋办?
客官,咱该这么办!
L1 = []
for i in range(1,11):
L1.append(i)
print(L1)
# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
搞事情?我能不知道用 for 循环是这样创建的吗?我是问用生成式怎么创建!
呃呃呃,小的愚钝,小的这就告诉您
L1 = [i for i in range(1, 11)]
print(L1)
# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
怎么样客官,原来三行代码,现在一行搞定,而且速度更快。是不是感觉很棒棒。事实上只要符合上面 for 循环这种模式的代码都可以简写成生成式的形式。
下面咱来个稍复杂点的生成式,加个循环嵌套,在加个判断,您说怎么样?
# 生成式
L1 = [i+j for i in range(1,3) for j in range(1, 3) if i+j > 2]
print(L1)
# 等价 for 循环
L2 = []
for i in range(1, 3):
for j in range(1, 3):
if i+j > 2:
L2.append(i+j)
print(L2)
# [3, 3, 4]
# [3, 3, 4]
通过这个例子,咱们可以很好的理解生成式代码的执行过程。现在依照上面的例子来做一个比喻,把生成式代码分为四个部分。第一个部分是x+y
,把它比作一个小鸡,第二个部分是for i in range(1,3) for j in range(1, 3)
,把它比作老母鸡,第三个部分是if i+j > 2
,这是一个孵化器(其实是叫过滤器),第四个部分是L1
,这是养鸡场。那么生成式的执行过程就可以看作:鸡蛋从老母鸡哪里生产出来,然后放在孵化器里孵化,只有成功孵化(符合过滤器要求)的鸡蛋才能变成小鸡,被放到养鸡场(赋值号左边的对象)里,而没有孵化的鸡蛋只能做成钢花蛋被吃掉(其实是丢掉了)。
字典生成式
字典生成式,跟列表生成式差不多嘛,那个用来生成列表,这个就用来生成字典喽。但也有区别,比如列表生成式用中括号 '[] '括起代码,而字典则用大括号 '{} ',另外字典的键和值之间要用 ': '冒号隔开。其实本质是差不多滴,只不过字典鸡生的蛋是双胞胎
D = {'one' : 1, 'two' : 2, 'three' : 3, 'four' : 4}
D1 = {v : k for k, v in D.items()}
print(D1)
# {1: 'one', 2: 'two', 3: 'three', 4: 'four'}
执行过程都是一样的,只不过字典有其独特的结构(键和键值),还有那充满曲线的大括号。当然,字典也可以有自己的过滤器,但与列表没啥区别,就不浪费客官时间了
集合生成式
事实集合的生成式并是很常见,但是客官您是一个要识广的人,对于这些不咋用的东西咱也要有些了解,反正生成式本就一个套路,现在有了对列表和字典生成式的理解,要理解集合生成式也就分分钟的事。
集合生成式与字典生成式相像的一个地方就是他们都用大括号 '{}'括起代码,但这两种生成式并不难区分。因为字典有两个量(键和值),而集合只有一个。
L1 = [1, 4, 3, 3, 4, 2, 3]
S = {i for i in L1}
print(S)
生成式的嵌套
这是一种比较复杂的生成式用法了,但其实也就那样,无非是在写生成式的时候用到了其他的生成式。
L1 = [i for i in range(1, 11)]
# 列表生成式
我们可以这么理解这个生成式代码:赋值号右边的代码[i for i in range(1, 11)]
执行的结果是产生一个列表,然后这个列表通过赋值号被赋予的L1。我们可以通过变量L1访问生成列表的每一个元素,那么我们也可以通过代码[i for i in range(1, 11)]
访问列表中的每一个元素,就像下面
L1 = [i for i in range(1, 11)]
print(L1)
# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
那么也就是说for i in L1
与for i in [i for i in range(1, 11)]
效果是一样的。说到这客官您就应该明了了。
# 嵌套的生成式
L2 = [j for j in [i for i in range(1, 11)] if j > 5]
print(L2)
# [6, 7, 8, 9, 10]
客官你千万别说我zz,生成个[6, 7, 8, 9, 10]也要搞个嵌套生成式。这只是个极为简单例子,关于生成式的运用当然有更复杂的,这就要客官您自己去探索了。
算了,还是给个绕一点的例子把…
D1 = {'one': 1, 'two': 2, '一': 1, '二': 2}
D2 = {k2 : {k1 for k1, v1 in D1.items() if v1 == k2} for k2 in set(D1.values())}
print(D2)
# {1: {'一', 'one'}, 2: {'two', '二'}}
生成器
当圆括号 '()'看到大括号和中括号都可以括起代码,并起一个相当霸气的名字之后,它暗自下定决心,我一定也要括起一段代码,并取一个向亮的名字!于是乎,这就有了生成器。生成器,生成式,傻傻分不清(刚才我检查上面有没有把生成式打成生成器的,果真还就找到了一个…)看下面
for j in [i for i in range(1, 11)]:
print(j, end=' ')
print('\n')
for j in (i for i in range(1, 11)):
print(j, end=' ')
# 1 2 3 4 5 6 7 8 9 10
# 1 2 3 4 5 6 7 8 9 10
他们的输出结果完全相同啊!(i for i in range(1, 11)
一定是一个元组生成式,它产生了一个元组,并通过for循环把结果打印了出来!客官你先别急着下定论。他们的输出结果是完全相同,但这只是表面,事实上,他们的执行过程是有很大区别的。先看看他们返回了什么东西
print([i for i in range(1, 11)])
print(i for i in range(1, 11))
# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# <generator object <genexpr> at 0x000001678B554AC8>
列表生成式返回的自然是一个列表,那么下面这个假的“元组生成式”(实际上是一个生成器)返回的是个啥玩意?generator object,即是一个生成器对象,它返回的信息还包括这个生成器对象的存放地址。
生成器和生成式的差异
ok,现在知道小括号括起的代码叫做生成器了(本小括号在上面就告诉你了,哼),那么生成器和生成式除了返回值不同之外还有什么区别呢,为什么他们可以输出同样的内容呢?
生成器和生成式的主要区别在于他们的执行过程,即:生成式会计算出所有需要的值,然后一起返回,就像把所有可以孵化的鸡蛋都孵化成小鸡之后,再一起把小鸡送进养鸡场;而生成器不同,他是孵出一个小鸡,就送一个小鸡进养鸡场,即每产生一个值,就返回一个值。
上面那段话说出了生成器和生成式的区别,也说明了生成器在某一方面优于生成式,即生成器的循环响应性优于生成式。生成式让你一次拥有全部,而生成器让你避免等待,从而可以及时的处理生成的数据。此外生成器还可以节省空间(你就想生成式是一次性把小鸡放进养鸡场的,那么在放进养鸡场之前这些小鸡就得找个地方先暂时放着,这就需要空间。而生成器则不需要这些按时存放的空间)。
写一个生成器
事实上我们会更多的用函数来写生成器。下面我们就写一个生成器并把它封装在函数当中。在这之前我们得明确,生成器是产生一个值就返回一个值,而我们所了解的在函数中可以返回对象的语句是 return 语句,但是这个语句不能满足生成器不断返回值的要求,因为 return 语句执行一次函数就会结束运行了,那又怎么一次次返回生成的值呢。
Python中有一个关键字能够满足生成器的要求,那就是 yield 关键字。它可以在不结束函数的情况下返回对象给调用方。(也不能说没结束,执行 yield 语句后函数确实被跳出了,但这种跳出更像是一种挂起,因为当再次调用该函数时,他不是从头开始执行函数的代码,而是从上次结束的地方,即 yield 语句的下面开始执行。return 语句结束后,再次调用则是从头开始执行函数的代码)
# 输出斐波那契数列
def generat(n):
i, j, k = 0, 1, 1
for index in range(n):
yield k
k = i+j
i, j = j, k
for fb in generat(10):
print(fb, end=' ')
# 1 1 2 3 5 8 13 21 34 55
斐波那契数列,就是今天的汤等于昨天的汤加前天的汤,额不对,是F(n) = F(n-1) + F(n-2)。上面的代码就是利用生成器实现斐波那契数列的计算。
在上面的代码中, for 循环语句执行时,他会先调用 generat 生成器函数,生成器函数会执行到yield k
语句,然后带着 k 的值返会到 for 循环里并把 k 值赋予 fb 。此时 for 循环会执行打印 fb 的语句,然后再次调用 generat 生成器函数(事实上这里说调用不太恰当,因为除了第一次是调用生成器之外,其他的调用更像是一种唤醒,即把 generat 生成器从挂起的状态唤醒,让它继续执行它的代码)。然后就是重复了,直到 for 循环结束。
next 和 send 函数
yield 是一个暂停键,把生成器函数暂停,然后去处理其他事情(上面的print(fb, end=' ')
)。那么当我们需要再次用到生成器产生数据时,应该怎么按下它的播放键把它从挂起的状态唤醒呢?答案就是用 next 函数或 send 函数。
# 例如下
k = generat(10)
# 创建一个生成 10 个斐波那契数列的生成器对象 k
for fb in range(5):
print(next(k), end=' ')
# 在循环里使用 next 函数 5 次,即输出5个数
print('\n', next(k))
# 在这里再次唤醒生成器
# 1 1 2 3 5
# 8
注意不要试图使用next(generat(10))
,因为这样是在调用生成器函数,而不是唤醒他。如果你把上面的next(k)
都改成next(generat(10))
那么他就会只输出1(相当于每次next(generat(10))
都调用了一个新的生成器对象,只不过他们的参数都是10)。你可能会有疑惑,为什么for fb in generat(10)
看起来像是循环一次就调用了一次生成器函数?实际上这就是 for 的强大之处,它可以自动的把一个生成器转化为一个生成器对象,就相当于自动执行k = generat(10)
语句,下面来验证一下
for fb in generat(10):
print(fb, end=' ')
print('\n')
k = generat(10)
for fb in k:
print(fb, end=' ')
# 1 1 2 3 5 8 13 21 34 55
# 1 1 2 3 5 8 13 21 34 55
for 循环不仅可以自动把生成器转化为生成对象,还可以捕获生成器函数结束后产生的 StopIteration 异常,从而自动结束循环。
send 函数不仅可以唤醒生成器函数,还可以向函数传递参数。但是使用 send 函数之前,跟 next 函数一样,要先把生成器转化成生成器对象。其实就是一个赋值语句(这里有点像类的实例化)。还有一点,即如果生成器还没有启动,那么第一次 send 函数时传进的参数必须是 None,因为没有上一个 yield ,send 传进的参数无处安放。其实 next 函数也有传进参数,只不过一直是 None,所以 next 函数就相当于 send(None)
def generat(n):
i, j, k = 0, 1, 1
for index in range(n):
sen = yield k
print("这里是'yield k'的值:", sen)
k = i+j
i, j = j, k
k = generat(10)
for fn in range(3):
print(next(k))
print(k.send(2))
print(k.send(3))
'''
1
这里是'yield k'的值: None
1
这里是'yield k'的值: None
2
这里是'yield k'的值: 2
3
这里是'yield k'的值: 3
5
'''