关于python:如何正确使用yield

2次阅读

共计 5478 个字符,预计需要花费 14 分钟才能阅读完成。

在 Python 开发中,yield 关键字的应用其实较为频繁,例如大汇合的生成,简化代码构造、协程与并发都会用到它。

然而,你是否真正理解 yield 的运行过程呢?

这篇文章,咱们就来看一下 yield 的运行流程,以及在开发中哪些场景适宜应用 yield。

生成器
如果在一个办法内,蕴含了 yield 关键字,那么这个函数就是一个「生成器」。

生成器其实就是一个非凡的迭代器,它能够像迭代器那样,迭代输入办法内的每个元素。

咱们来看一个蕴含 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():敞开生成器

通过应用生成器的这些办法,咱们能够实现很多有意思的性能。

next
先来看生成器的 next 办法,咱们看上面这个例子。

# coding: utf8

def gen(n):
    for i in range(n):
        print('yield before')
        yield i
        print('yield after')

g = gen(3)      # 创立一个生成器
print(g.__next__())  # 0
print('----')
print(g.__next__())  # 1
print('----')
print(g.__next__())  # 2
print('----')
print(g.__next__())  # StopIteration

# Output:
# yield before
# 0
# ----
# yield after
# yield before
# 1
# ----
# yield after
# yield before
# 2
# ----
# yield after
# Traceback (most recent call last):
#   File "gen.py", line 16, in <module>
#     print(g.__next__())  # StopIteration
# StopIteration

在这个例子中,咱们定义了 gen 办法,这个办法蕴含了 yield 关键字。而后咱们执行 g = gen(3) 创立一个生成器,然而这次没有执行 for 去迭代它,而是屡次调用 g.__next__() 去输入生成器中的元素。

咱们看到,当执行 g.__next__()时,代码就会执行到 yield 处,而后返回 yield 前面的值,如果持续调用 g.__next__(),留神,你会发现,这次执行的开始地位,是上次 yield 完结的中央,并且它还保留了上一次执行的上下文,持续向后迭代。

这就是应用 yield 的作用,在迭代生成器时,每一次执行都能够保留上一次的状态,而不是像一般办法那样,遇到 return 就返回后果,下一次执行只能再次反复上一次的流程。

生成器除了能保留状态之外,咱们还能够通过其余形式,扭转其外部的状态,这就是上面要讲的 send 和 throw 办法。

send
下面的例子中,咱们只展现了在 yield 后有值的状况,其实还能够应用 j = yield i 这种语法,咱们看上面的代码:

# coding: utf8

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

此时如果咱们执行上面的代码:

for i in gen():
    print(i)
    time.sleep(1)

输入后果会是 1 2 4 8 16 32 64 … 始终循环上来,直到咱们杀死这个过程能力进行。

这段代码始终循环的起因在于,它无奈执行到 j == -1 这个分支里 break 进去,如果咱们想让代码执行到这个中央,如何做呢?

这里就要用到生成器的 send 办法了,send 办法能够把内部的值传入生成器外部,从而扭转生成器的状态。

代码能够像上面这样写:

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 办法:

# 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 办法也比较简单,就是手动敞开这个生成器,敞开后的生成器无奈再进行操作。

>>> g = gen()
>>> g.close() # 敞开生成器
>>> g.__next__() # 无奈迭代数据
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

close 办法咱们在开发中应用得比拟少,理解一下就好。

应用场景
理解了 yield 和生成器的应用形式,那么 yield 和生成器个别用在哪些业务场景中呢?

上面我介绍几个例子,别离是大汇合的生成、简化代码构造、协程与并发,你能够参考这些应用场景来应用 yield。

大汇合的生成
如果你想生成一个十分大的汇合,如果应用 list 创立一个汇合,这会导致在内存中申请一个很大的存储空间,例如想上面这样:

# coding: utf8

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

# 一次性在内存中生成大汇合 内存占用十分大
for i in big_list():
    print(i)

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

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

# coding: utf8

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

# 只有在迭代时 才顺次生成元素 缩小内存占用
for i in big_list():
    print(i)

简化代码构造
咱们在开发时还常常遇到这样一种场景,如果一个办法要返回一个 list,但这个 list 是多个逻辑块组合后能力产生的,这就会导致咱们的代码构造变得很简单:

# 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,代码就简洁很多:

# 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 实现一个协程生产者、消费者的例子:

# 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
...

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

  1. c = consumer() 创立一个生成器对象
  2. producer(c) 开始执行,c.__next()__ 会启动生成器 consumer 直到代码运行到 j = yield i 处,此时 consumer 第一次执行结束,返回
  3. producer 函数持续向下执行,直到 c.send(i) 处,这里利用生成器的 send 办法,向 consumer 发送数据
  4. consumer 函数被唤醒,从 j = yield i 处持续开始执行,并且接管到 producer 传来的数据赋值给 j,而后打印输出,直到再次执行到 yield 处,返回
  5. producer 持续循环执行下面的过程,顺次发送数据给 cosnumer,直到循环完结
  6. 最终 c.close() 敞开 consumer 生成器,程序退出

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

这个例子用到了下面讲到的 yield、生成器的 __next__、send、close 办法。如果不好了解,你能够多看几遍这个例子,最好本人测试一下。

咱们应用协程编写生产者、消费者的程序时,它的益处是:

  • 整个程序运行过程中无锁,不必思考共享变量的爱护问题,升高了编程复杂度
  • 程序在函数之间来回切换,这个过程是用户态下进行的,不像过程 / 线程那样,会陷入到内核态,这就缩小了内核态上下文切换的耗费,执行效率更高

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

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

总结
总结一下,这篇文章咱们次要讲了 yield 的应用形式,以及生成器的各种个性。

生成器是一种非凡的迭代器,它除了能够迭代数据之外,在执行时还能够保留办法中的状态,除此之外,它还提供了内部扭转外部状态的形式,把内部的值传入到生成器外部。

利用 yield 和生成器的个性,咱们在开发中能够用在大集成的生成、简化代码构造、协程与并发的业务场景中。

Python 的 yield 也是实现协程和并发的根底,它提供了协程这种用户态的编程模式,进步了程序运行的效率。

最近整顿了几百 G 的 Python 学习材料,收费分享给大家!想要的返回公~豪“Python 编程学习圈”,发送“J”即可收费取得

正文完
 0