Python学习笔记 生成式和生成器

目录

  生成式

      - 列表生成式
      - 字典生成式
      - 集合生成式
      - 生成式的嵌套

  生成器

      - 生成器和生成式的差异
      - 写一个生成器函数
      - 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 L1for 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
'''
  • 10
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值