python中,yield關鍵字的作用:1、將一個函數修改為生成器,利用生成器可以有效地節省系統資源,避免不必要的記憶體佔用;2、用於定義上下文管理器;3、協程;4、配合from形成yield from用於消費子產生器並傳遞訊息。
yield 的用法有以下四個常見的情況:
一個是生成器,
概括的話就是:生成器內部的程式碼執行到yield會返回,返回的內容為yield後的表達式。下次再執行生成器的內部程式碼時將從上次的狀態繼續開始。透過yield關鍵字,我們可以很方便的將一個函數修改為生成器。
二是用來定義上下文管理器,
#三是協程,
四是配合from 形成yield from 用於消費子產生器並傳遞訊息。
這四種用法,其實都源自於yield 所具有的暫停的特性,也就說程式在運行到yield 所在的位置result = yield expr 時,先執行yield expr將產生的值傳回給呼叫產生器的caller,然後暫停,等待caller 再次啟動並恢復程式的執行。而根據復原程式所使用的方法不同,yield expr 運算式的結果值 result 也會跟著變化。如果使用 __next()__ 來調用,則 yield 表達式的值 result 是 None;如果使用 send() 來調用,則 yield 表達式的值 result 是透過 send 函數傳送的值。以下是官方文件介紹yield 表達式時的一個例子[1],能夠很好地說明關鍵字 yield 的特性和用法:
>>> def echo(value=None): ... print("Begin...") ... try: ... while True: ... try: ... value = (yield value) ... except Exception as e: ... value = e ... finally: ... print("Clean up!!!") ... >>> generator = echo(1) >>> print(next(generator)) Begin... 1 >>> print(next(generator)) None >>> print(generator.send(2)) 2 >>> generator.throw(TypeError, "spam") TypeError('spam') >>> generator.close() Clean up!!!
上面這段程式碼的說明如下圖所示:
執行第一個next(generator) 的時候,也就是預啟動產生器,生成器開始執行,列印Begin... 字串,執行到value = (yield value) 的位置時,首先呼叫yield value 產生數字1,然後生成器在yield 的位置暫停。
接著呼叫第2 個next(generator) 的時候,生成器恢復執行,由於使用next() 來呼叫生成器函數, value 的值會變成None ,因此生成當器函數繼續執行到yield value 時,會將value 的值None 傳回給解釋器,然後再次暫停。
接著使用 send(2) 方法繼續呼叫產生器,value 接收到傳入的數位 2,並繼續到執行 value = (yield value) ,將數位 2 回傳給解譯器後暫停。
此後,解釋器再次透過throw(TypeError, "spam") 方法調用,生成器恢復執行,並拋出異常,生成器捕獲到異常,並將異常TypeError( 'spam') 賦值給變數value,然後程式再次執行到value = (yield value) ,將TypeError('spam') 回傳給解釋器。
最後,程式呼叫 close() 方法,在產生器函數的位置拋出 GeneratorExit ,異常被拋出,產生器正常退出,並最終執行最外層try 語句對應的finally 分支,列印輸出 Clean up。
python中有一個非常有用的語法叫做生成器,所利用的關鍵字就是yield。有效利用生成器這個工具可以有效地節省系統資源,避免不必要的記憶體佔用。
生成器
不出意外,你最先遇到 yield 一定會是一個生成器函數裡面。生成器是一個用於不斷產生數字或其他類型的值的函數,可以透過 for 迴圈或 next() 函數逐一呼叫。這裡要強調的是,生成器包含的是一個沒有賦值的yield 表達式,所以下面兩種形式是等價的[2]:
def integers_1(): for i in range(4): yield i + 1def integers_2(): for i in range(4): value = yield i + 1
這裡之所以強調第二種形式,是為了在理解透過send() 方法發送value 時,能夠更好地理解yield。同時,也能夠更正確地說明,呼叫生成器傳回的值是 yield 關鍵字右邊的表達式 i 1 的值,而不是 yield 表達式本身的結果值。
我們試著呼叫一下:
>>> for n in integers_1(): ... print(n) ... 1 2 3 4 >>> for n in integers_2(): ... print(n) ... 1 2 3 4
上下文管理器
配合Python 的contexlib 模組裡的@contextmanager 裝飾器,yield 也可以用於定義上下文管理器,以下是Python Tricks 書中的一個例子[3]:
from contextlib import contextmanager @contextmanager def managed_file(name): try: f = open(name, 'w') yield f finally: f.close()
上面透過裝飾器和yield 關鍵字定義的上下文管理器和下面類別的方法定義等同:
class ManagedFile: def __init__(self, name): self.name = name def __enter__(self): self.file = open(self.name, 'w') return self.file def __exit__(self, exc_type, exc_val, exc_tb): if self.file: self.file.close()
可以利用下面的方法分別進行呼叫:
>>> with ManagedFile('hello.txt') as f: ... f.write('hello, world!') ... f.write('bye now') >>> with managed_file('hello.txt') as f: ... f.write('hello, world!') ... f.write('bye now')
協程
協程的概念充滿了美感,非常符合人的辦事模式,想要完全掌握卻還是需要花費一些功夫。不過這些功夫是值得的,因為有時多線程所帶來的麻煩會遠遠比協程多。下面是 Python Cookbook 中的一個只用 yield 表達式寫的協程實例[4]:
from collections import deque # Two simple generator functions def countdown(n): while n > 0: print('T-minus', n) yield n -= 1 print('Blastoff!') def countup(n): x = 0 while x < n: print('Counting up', x) yield x += 1 class TaskScheduler: def __init__(self): self._task_queue = deque() def new_task(self, task): ''' Admit a newly started task to the scheduler ''' self._task_queue.append(task) def run(self): ''' Run until there are no more tasks ''' while self._task_queue: task = self._task_queue.popleft() try: # Run until the next yield statement next(task) self._task_queue.append(task) except StopIteration: # Generator is no longer executing pass # Example use sched = TaskScheduler() sched.new_task(countdown(2)) sched.new_task(countup(5)) sched.run()
运行上面的脚本,可以得到以下输出:
T-minus 2 Counting up 0 T-minus 1 Counting up 1 Blastoff! Counting up 2 Counting up 3 Counting up 4
countdown 和 countup 两个任务交替执行,主程序在执行到 countdown 函数的 yield 表达式时,暂停后将被重新附加到队列里面。然后,countup 任务从队列中取了出来,并开始执行到 yield 表达式的地方后暂停,同样将暂停后的协程附加到队列里面,接着从队列里取出最左边的任务 countdown 继续执行。重复上述过程,直到队列为空。
上面的协程可以利用 Python3.7 中的 asyncio 库改写为:
import asyncio async def countdown(n): while n > 0: print('T-minus', n) await asyncio.sleep(0) n -= 1 print('Blastoff!') async def countup(n): x = 0 while x < n: print('Counting up', x) await asyncio.sleep(0) x += 1 async def main(): await asyncio.gather(countdown(2), countup(5)) asyncio.run(main())
可以看到利用 asyncio 库编写的协程示例比用 yield 来编写的协程要优雅地多,也简单地多,更容易被人理解。
yield from
说实话,yield from 实在有点令人费解,让人摸不着头脑。yield from 更多地被用于协程,而 await 关键字的引入会大大减少 yield from 的使用频率。yield from 一方面可以迭代地消耗生成器,另一方面则建立了一条双向通道,可以让调用者和子生成器便捷地通信,并自动地处理异常,接收子生成器返回的值。下面是 Python Cookbook 书里的一个例子,用于展开嵌套的序列[5]:
from collections.abc import Iterable def flatten(items, ignore_types=(str, bytes)): for x in items: if isinstance(x, Iterable) and not isinstance(x, ignore_types): yield from flatten(x) else: yield x items = [1, 2, [3, 4, [5, 6], 7], 8] # Produces 1 2 3 4 5 6 7 8 for x in flatten(items): print(x)
而 yield from 用于建立双向通道的用法则可以参考 Fluent Python 里例子[6],这里就不详细地解释这段代码:
# BEGIN YIELD_FROM_AVERAGER from collections import namedtuple Result = namedtuple('Result', 'count average') # the subgenerator def averager(): total = 0.0 count = 0 average = None while True: term = yield if term is None: break total += term count += 1 average = total/count return Result(count, average) # the delegating generator def grouper(results, key): while True: results[key] = yield from averager() # the client code, a.k.a. the caller def main(data): results = {} for key, values in data.items(): group = grouper(results, key) next(group) for value in values: group.send(value) group.send(None) report(results) # output report def report(results): for key, result in sorted(results.items()): group, unit = key.split(';') print(f'{result.count:2} {group:5} averaging {result.average:.2f}{unit}') data = { 'girls;kg': [40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5], 'girls;m': [1.6, 1.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43], 'boys;kg': [39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3], 'boys;m': [1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46], } if __name__ == '__main__': main(data)
可能对于熟练掌握 Python 的程序员来说,yield 和 yield from 相关的语法充满了美感。但对于刚入门的我来说,除了生成器语法让我感觉到了美感,其他的语法都让我理解起来很是费解。不过还好,asyncio 库融入了 Python 的标准库里,关键字 async 和 await 的引入,将会让我们更少地在编写协程时去使用 yield 和 yield from。 但不管怎么样,yield 都是 Python 里非常特别的一个关键字,值得花时间好好掌握了解。
以上是Python中關鍵字yield有什麼作用的詳細內容。更多資訊請關注PHP中文網其他相關文章!