python生成器

参考:

简介

生成器(generator)是一种返回一个值的迭代器,每次从该迭代器取下一个值。

生成器有两种表示:

  • 生成器表达式
  • 生成器函数

生成器函数,

还包括一下内容:

  • 通用生成器
  • 协程生成器
  • 委托生成器
  • 子生成器

生成器表达式

生成器表达式是用圆括号来创建,其语法与推导式相同,只是将[]换成了 () 。 生成器表达式会产生一个新的生成器对象。

>>>type([i for i in range(5)])
 list

>>>  type((i for i in range(5)))
   generator

这种生成器表达式被成为隐式生成器,它禁止使用 yield 和 yield from 表达式。

示例:

>>>a = (i for i in range(3))

>>>  next(a)
   0

>>>  next(a)
   1

>>>  next(a)
   2

>>>  next(a)

StopIteration                             Traceback (most recent call last)
<ipython-input-7-15841f3f11d4> in <module>
----> 1 next(a)

StopIteration:

>>>a = (i for i in range(3))

>>>  for i in a:
   ...:     print(i)
   ...:
0
1
2

这好像和迭代器没什么区别!(如果你不清楚什么是迭代器请阅读 Python 迭代器的使用

生成器函数

一个包含 yield 关键字的方法:

# coding: utf8

# 生成器
def gen(n):
    for i in range(n):
        yield i

g = gen(5)      # 创建一个生成器
print(g)        # <generator object gen at 0x10bb46f50>
print(type(g))  # <type 'generator'>

# 迭代生成器中的数据
for i in g:
    print(i)
    
# Output:
# 0 1 2 3 4

注意,在这个例子中,当我们执行 g = gen(5) 时,gen 中的代码其实并没有执行,此时我们只是创建了一个「生成器对象」,它的类型是 generator。

然后,当我们执行 for i in g,每执行一次循环,就会执行到 yield 处,返回一次 yield 后面的值。

这个迭代过程是和迭代器最大的区别。

换句话说,如果我们想输出 5 个元素,在创建生成器时,这个 5 个元素其实还并没有产生,什么时候产生呢?只有在执行 for 循环遇到 yield 时,才会依次生成每个元素。

此外,生成器除了和迭代器一样实现迭代数据之外,还包含了其他方法:

generator.__next__():执行 for 时调用此方法,每次执行到 yield 就会停止,然后返回 yield 后面的值,如果没有数据可迭代,抛出 StopIterator 异常,for 循环结束
generator.send(value):外部传入一个值到生成器内部,改变 yield 前面的值
generator.throw(type[, value[, traceback]]):外部向生成器抛出一个异常
generator.close():关闭生成器

函数如果包含 yield 指令,该函数调用的返回值是一个生成器对象,此时函数体中的代码并不会执行,只有显示或隐示地调用 next() 的时候才会真正执行里面的代码。yield可以暂停一个函数并返回此时的中间结果。该函数将保存执行环境并在下一次恢复。

>>>def fibonacci():
    		a, b = 0, 1
			while True:
		        yield a
		        a, b = b, a + b
   ...:

>>>  fun
   <function __main__.fun()>

>>>  fun()
   <generator object fun at 0x04F5D140>
>>> fib = fibonacci()
>>> for i in range(10):
			print(next(fib))

此函数将生成斐波那契数列的前10个数字。

在这个示例中,我们首先将fibonacci()函数分配给一个变量fib。然后,我们使用for循环和next()函数来迭代生成器并打印前10个数字。在每次迭代中,next()函数会从上一次离开的地方继续执行,生成下一个斐波那契数。

send

上面的例子中,我们只展示了在 yield 后有值的情况,其实还可以使用 j = yield i 这种语法,我们看下面的代码:

python3 复制代码
# coding: utf8

def gen():
    i = 1
    while True:
        j = yield i
        i *= 2
        if j == -1:
            break

此时如果我们执行下面的代码:

python3 复制代码
for i in gen():
    print(i)
    time.sleep(1)

输出结果会是 1 2 4 8 16 32 64 ... 一直循环下去, 直到我们杀死这个进程才能停止。

这段代码一直循环的原因在于,它无法使得 j == -1 ,如果我们想让代码执行到这个地方,如何做呢?

这里就要用到生成器的 send 方法了,send 方法可以把外部的值传入生成器内部,从而改变生成器的状态。

代码可以像下面这样写:

python3 复制代码
g = gen()   # 创建一个生成器
print(g.__next__())  # 1
print(g.__next__())  # 2
print(g.__next__())  # 4
# send 把 -1 传入生成器内部 走到了 j = -1 这个分支
print(g.send(-1))   # StopIteration 迭代停止

当我们执行 g.send(-1) 时,相当于把 -1 传入到了生成器内部,然后赋值给了 yield 前面的 j,此时 j = -1,然后执行 break ,不会继续迭代下去。

throw

外部除了可以向生成器内部传入一个值外,还可以传入一个异常,也就是调用 throw 方法:

python3 复制代码
# coding: utf8

def gen():
    try:
        yield 1
    except ValueError:
        yield 'ValueError'
    finally:
        print('finally')

g = gen()   # 创建一个生成器
print(g.__next__()) # 1
# 向生成器内部传入异常 返回ValueError
print(g.throw(ValueError))

# Output:
# 1
# ValueError
# finally

这个例子创建好生成器后,使用 g.throw(ValueError) 的方式,向生成器内部传入了一个异常,走到了生成器异常处理的分支逻辑。

close

生成器的 close 方法也比较简单,就是手动关闭这个生成器,关闭后的生成器无法再进行操作。

python3 复制代码
>>> g = gen()
>>> g.close() # 关闭生成器
>>> g.__next__() # 无法迭代数据
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

示例

>>>def fun():
   ...:     for i in range(3):
   ...:         print("Start...")
   ...:         yield i
   ...:

>>>  f = fun()

>>>  next(f)
Start...
   0

>>>  next(f)
Start...
   1

>>>  next(f)
Start...
   2

>>>   next(f)

StopIteration                             Traceback (most recent call last)
<ipython-input-8-aff1dd02a623> in <module>
----> 1 next(f)

StopIteration:

>>>def fun():
   ...:     for i in range(3):
   ...:         print("Start...")
   ...:         yield i
   ...:

>>>  for i in fun():
   ...:     print(i)
   ...:
Start...
0
Start...
1
Start...
2

详解

如果一个函数包含 yield 关键字,那么这个函数的返回值是一个生成器,但是这个函数仅是一个函数而已。使用这个函数返回一个生成器对象并使用它才是真正的目的。此时,在生成器函数中使用 return 对其并没有任何影响,这个函数返回的仍然是生成器对象。

  • 但是,如果没有 return 语句,则执行到函数完毕时将返回 StopIteration 异常。
  • 如果在执行过程中遇到 return 语句,则直接抛出 StopIteration 异常,终止迭代。
  • 如果在 return 后返回一个值,那么这个值作为 StopIteration 异常的说明,不是函数的返回值。

示例

>>>def fun():
   ...:     for i in range(5):
   ...:         print("Start...")
   ...:         yield i
   ...:
   ...:     return None
   ...:

>>>  fun
   <function __main__.fun()>

>>>  fun()
   <generator object fun at 0x04A05220>

当然,也可以通过close()手动关闭生成器函数,后面再调用生成器会直接返回 StopIteration异常。

示例:

>>>def fun():
   ...:     for i in range(5):
   ...:         print("Start...")
   ...:         yield i
   ...:

>>>  f = fun()

>>>  next(f)
Start...
   0

>>>  next(f)
Start...
   1

>>>  f.close()

>>>  next(f)
---------------------------------------------------------------------------

StopIteration                             Traceback (most recent call last)
<ipython-input-6-aff1dd02a623> in <module>
----> 1 next(f)

StopIteration:

为什么是这样

生成器其实是一种特殊的迭代器,不过这种迭代器更加优雅。它不需要像迭代器的类一样写__iter__()__next__()方法了,只需要一个yield关键字。当一个生成器函数被调用的时候,它返回一个迭代器,称为生成器 或者生成器对象。生成器一定是迭代器(反之不成立),因此任何生成器也是以一种惰性加载的模式生成器。什么意思呢,这个生成器对象控制生成器函数的执行。当这个生成器的某一个方法被调用的时候,生成器函数开始执行。这时会一直执行到第一个 yield 语句,在此执行被挂起,并给生成器的调用者返回值。挂起后,所有局部状态都被保留下来,包括局部变量的当前绑定、指令指针、内部求值、栈和任何异常处理的状态。通过调用生成器的某一个方法,生成器函数继续执行。

生成器对象可以通过 next()__next__()send() 方法恢复函数执行。

使用场景

了解了 yield 和生成器的使用方式,那么 yield 和生成器一般用在哪些业务场景中呢?

下面我介绍几个例子,分别是大集合的生成、简化代码结构、协程与并发,你可以参考这些使用场景来使用 yield。

大集合的生成

如果你想生成一个非常大的集合,如果使用 list 创建一个集合,这会在内存中申请很大的存储空间,例如想下面这样:

python3 复制代码
# coding: utf8

def big_list():
    result = []
    for i in range(10000000000):
        result.append(i)
    return result

# 一次性在内存中生成大集合 内存占用非常大
for i in big_list():
    print(i)

这种场景,我们使用生成器就能很好地解决这个问题。

因为生成器只有在执行到 yield 时才会迭代数据,这时只会申请需要返回元素的内存空间,代码可以这样写:

python3 复制代码
# coding: utf8

def big_list():
    for i in range(10000000000):
        yield i

# 只有在迭代时 才依次生成元素 减少内存占用
for i in big_list():
    print(i)
简化代码结构

我们在开发时还经常遇到这样一种场景,如果一个方法要返回一个 list,但这个 list 是多个逻辑块组合后才能产生的,这就会导致我们的代码结构变得很复杂:

python3 复制代码
# coding: utf8

def gen_list():
    # 多个逻辑块 组成生成一个列表
    result = []
    for i in range(10):
        result.append(i)
    for j in range(5):
        result.append(j * j)
    for k in [100, 200, 300]:
        result.append(k)
    return result
    
for item in gen_list():
    print(item)

这种情况下,我们只能在每个逻辑块内使用 append 向 list 中追加元素,代码写起来比较啰嗦。

此时如果使用 yield 来生成这个 list,代码就简洁很多:

python3 复制代码
# coding: utf8

def gen_list():
    # 多个逻辑块 使用yield 生成一个列表
    for i in range(10):
        yield i
    for j in range(5):
        yield j * j
    for k in [100, 200, 300]:
        yield k
        
for item in gen_list():
    print(i)

使用 yield 后,就不再需要定义 list 类型的变量,只需在每个逻辑块直接 yield 返回元素即可,可以达到和前面例子一样的功能。

我们看到,使用 yield 的代码更加简洁,结构也更清晰,另外的好处是只有在迭代元素时才申请内存空间,降低了内存资源的消耗。

协程与并发

还有一种场景是 yield 使用非常多的,那就是「协程与并发」。

如果我们想提高程序的执行效率,通常会使用多进程、多线程的方式编写程序代码,最常用的编程模型就是「生产者-消费者」模型,即一个进程 / 线程生产数据,其他进程 / 线程消费数据。

在开发多进程、多线程程序时,为了防止共享资源被篡改,我们通常还需要加锁进行保护,这样就增加了编程的复杂度。

在 Python 中,除了使用进程和线程之外,我们还可以使用「协程」来提高代码的运行效率。

什么是协程?

简单来说,由多个程序块组合协作执行,称之为「协程」。

而在 Python 中使用「协程」,就需要用到 yield 关键字来配合。

可能这么说还是不太好理解,我们用 yield 实现一个协程生产者、消费者的例子:

python3 复制代码
# coding: utf8

def consumer():
    i = None
    while True:
        # 拿到 producer 发来的数据
        j = yield i 
        print('consume %s' % j)

def producer(c):
    c.__next__()
    for i in range(5):
        print('produce %s' % i)
        # 发数据给 consumer
        c.send(i)
    c.close()

c = consumer()
producer(c)

# Output:
# produce 0
# consume 0
# produce 1
# consume 1
# produce 2
# consume 2
# produce 3
# consume 3
...

这个程序的执行流程如下:

  • c = consumer() 创建一个生成器对象
  • producer© 开始执行,c.next() 会启动生成器 consumer 直到代码运行到 j = yield i 处,此时 consumer 第一次执行完毕,返回。
  • producer 函数继续向下执行,直到 c.send(i) 处,这里利用生成器的 send 方法,向 consumer 发送数据
  • consumer 函数被唤醒,从 j = yield i 处继续开始执行,并且接收到 producer 传来的数据赋值给 j,然后打印输出,直到再次执行到 yield 处,返回
  • producer 继续循环执行上面的过程,依次发送数据给 cosnumer,直到循环结束
  • 最终 c.close() 关闭 consumer 生成器,程序退出

在这个例子中我们发现,程序在 producer 和 consumer 这 2 个函数之间来回切换执行,相互协作,完成了生产任务、消费任务的业务场景,最重要的是,整个程序是在单进程单线程下完成的。

这个例子用到了上面讲到的 yield、生成器的 next、send、close 方法。如果不好理解,你可以多看几遍这个例子,最好自己测试一下。

我们使用协程编写生产者、消费者的程序时,它的好处是:

整个程序运行过程中无锁,不用考虑共享变量的保护问题,降低了编程复杂度

程序在函数之间来回切换,这个过程是用户态下进行的,不像进程 / 线程那样,会陷入到内核态,这就减少了内核态上下文切换的消耗,执行效率更高

所以,Python 的 yield 和生成器实现了协程的编程方式,为程序的并发执行提供了编程基础。

Python 中的很多第三方库,都是基于这一特性进行封装的,例如 gevent、tornado,它们都大大提高了程序的运行效率。

扩展

如果生成器表达式包含 async for 子句或 await 表达式,则称为异步生成器表达式。 异步生成器表达式会返回一个新的异步生成器对象,此对象属于异步迭代器。

如果在一个 async def 定义的函数体内使用 yield 表达式会让协程函数变成异步的生成器。

注:在 Python 3.8版本中, yield 和 yield from 在隐式嵌套的作用域中已被禁用

yield from 生成器

yield from 允许一个生成器将其部分操作委派给另一个生成器。

示例:

>>>def f1(x):
   ...:     receive = yield x
   ...:     return receive
   ...:

>>>  def f2():
   ...:     for i in range(5):
   ...:         rlt = yield from f1("你好")
   ...:         print(rlt)
   ...:

>>>  f = f2()

>>>  next(f)
   '你好'

>>>  f.send("世界")
	世界
	'你好'

f1 、f2 是两个生成器函数,生成器函数 f2 通过 yield from 委托给生成器函数 f1,当生成器函数恢复执行时通过 return 返回相应值给 f2 ,这里 f2 被称为委托生成器,f1 被称为子生成器。通俗的说含有 yield from 关键字的函数是委托生成器, yield from 后的函数是子生成器。

yield from 为调用者和子生成器之间提供了一个透明的双向通道,包括从子生成器获取数据以及向子生成器传送数据。

虽然 yield from 主要设计用来向子生成器委派操作任务,但 yield from 可以向任意的迭代器委派操作。

示例:

>>>def fun():
   ...:     yield from range(3)
   ...:

>>>  f = fun()

>>>  next(f)
   0

>>>  next(f)
   1

>>>  next(f)
   2

>>>   next(f)

StopIteration                             Traceback (most recent call last)
<ipython-input-8-aff1dd02a623> in <module>
----> 1 next(f)

StopIteration:

yield from iterable 本质上等于 for item in iterable: yield item

委托生成器的作用:在调用方和子生成器之间行成双向通道。

双向通道的含义:调用方可以通过 send() 直接发送消息给子生成器,而子生成器 yield 的值,也是直接返回给调用方。只有子生成器结束(return)了,yield from 左边的变量才会被赋值,委托生成器后面的代码才会执行。

yield from 允许子生成器直接从调用者接收其发送的信息或者抛出调用时遇到的异常,并且返回给委托生产器一个值。

注意

如果对子生成器的调用产生 StopIteration 异常,委托生成器恢复继续执行 yield from 后面的语句;

示例

>>>def f1():
   ...:     return
   ...:     yield "hello"
   ...:

>>>  def f2():
   ...:     for i in range(3):
   ...:         yield from f1()
   ...:         print("f2")
   ...:

>>>  f = f2()

>>>  next(f)
f2
f2
f2

StopIteration                             Traceback (most recent call last)
<ipython-input-4-aff1dd02a623> in <module>
----> 1 next(f)

StopIteration:

若子生成器产生其他任何异常,则都传递给委托生成器。如果 GeneratorExit 异常被抛给委托生成器,或者委托生成产器的 close()方法被调用,如果子生成器有close() 的话也将被调用。

示例:

>>>def f1(x):
   ...:     yield x
   ...:

>>>  def f2():
   ...:     for i in range(5):
   ...:         yield from f1("f2")
   ...:

>>>  f11 =f1("f1")

>>>  f22 = f2()

>>>  next(f11)
   'f1'

>>>  next(f22)
   'f2'

>>>  f22.close()
>>>   next(f11)

StopIteration                             Traceback (most recent call last)
<ipython-input-8-28c9d6bd1302> in <module>
----> 1 next(f11)

StopIteration:

>>> next(f22)

StopIteration                             Traceback (most recent call last)
<ipython-input-9-ba7a330f783c> in <module>
----> 1 next(f22)

StopIteration:

当子生成器结束并抛出异常时,yield from 表达式的值是其 StopIteration 异常中的第一个参数。

示例:

>>>def f1():
   ...:     yield
   ...:     return "Hi"
   ...:

>>>  def f2():
   ...:     rlt = None
   ...:     for i in range(5):
   ...:         rlt =  yield from f1()
   ...:     print(rlt)
   ...:

>>>  f = f2()

>>>  next(f)

>>>  next(f)

>>>  next(f)

>>>  next(f)

>>>  next(f)

>>>  next(f)

Hi

StopIteration                             Traceback (most recent call last)
<ipython-input-9-aff1dd02a623> in <module>
----> 1 next(f)

StopIteration:
相关推荐
_.Switch2 分钟前
Python机器学习:自然语言处理、计算机视觉与强化学习
python·机器学习·计算机视觉·自然语言处理·架构·tensorflow·scikit-learn
sukalot13 分钟前
windows C++-使用任务和 XML HTTP 请求进行连接(一)
c++·windows
JUNAI_Strive_ving15 分钟前
番茄小说逆向爬取
javascript·python
彤银浦16 分钟前
python学习记录7
python·学习
落落落sss19 分钟前
MybatisPlus
android·java·开发语言·spring·tomcat·rabbitmq·mybatis
ぃ扶摇ぅ30 分钟前
Windows系统编程(三)进程与线程二
c++·windows
简单.is.good37 分钟前
【测试】接口测试与接口自动化
开发语言·python
Yvemil71 小时前
MQ 架构设计原理与消息中间件详解(二)
开发语言·后端·ruby
程序员是干活的1 小时前
私家车开车回家过节会发生什么事情
java·开发语言·软件构建·1024程序员节
Envyᥫᩣ1 小时前
Python中的自然语言处理:从基础到高级
python·自然语言处理·easyui