Python中的yield關(guān)鍵字

Python中的yield關(guān)鍵字

來源: https://stackoverflow.com/questions/231767/what-does-the-yield-keyword-do

這是stackoverflow上一個(gè)關(guān)于yield關(guān)鍵字的問題以及它被推薦次數(shù)最高的一個(gè)答案

問題:

Python中的yield關(guān)鍵字是什么?它是用來做什么的?
例如,下面的這份代碼:

def _get_child_candidates(self, distance, min_dist, max_dist):
    if self._leftchild and distance - max_dist < self._median:
        yield self._leftchild
    if self._rightchild and distance + max_dist >= self._median:
        yield self._rightchild 

調(diào)用方式:

result, candidates = [], [self]
while candidates:
    node = candidates.pop()
    distance = node._get_dist(obj)
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
return result

當(dāng)_get_child_candidates方法被調(diào)用時(shí),發(fā)生了什么?是一個(gè)list對象被返回了?還是一個(gè)單獨(dú)的元素?或者是它再次被調(diào)用了?這個(gè)調(diào)用什么時(shí)候會(huì)結(jié)束呢?


答案

在理解yield關(guān)鍵字之前,需要先理什么是迭代器(iterables)

迭代器(Iterables)

當(dāng)創(chuàng)建一個(gè)列表后,你就可以一個(gè)接著一個(gè)地讀取它的元素, 這種操作就叫做迭代(iteration):

>>> mylist = [1, 2, 3]
>>> for i in mylist:
...    print(i)
1
2
3

mylist就是一個(gè)可迭代對象。當(dāng)使用列表推導(dǎo)式時(shí)來創(chuàng)建一個(gè)列表的時(shí)候,就創(chuàng)建了一個(gè)可迭代對象,如下:

>>> mylist = [x*x for x in range(3)]
>>> for i in mylist:
...    print(i)
0
1
4

所有可以使用 for ... in ...語法的對象,都是一個(gè)可迭代對象: 例如 lists、string、file等等
由于可以隨意地讀取這些對象中的元素,這些可迭代對象使用起來非常方便,但是這樣做會(huì)把可迭代對象所有的值都存在內(nèi)存中,當(dāng)有很多個(gè)元素的時(shí)候,這樣做可能并不合適。

生成器(Generators)

生成器是可以迭代的,但它是一個(gè)只可以讀取一次的迭代器。生成器并不會(huì)把所有的值都存在內(nèi)存中,而是實(shí)時(shí)地生成數(shù)據(jù)

>>> mygenerator = (x*x for x in range(3))
>>> for i in mygenerator:
...    print(i)
0
1
4

除了把[]換成()之外,生成器看起來和列表推導(dǎo)式?jīng)]有什么不同。但是,調(diào)用之后,你不能再次使用for i in mygenerator,因?yàn)樯善髦豢梢员坏淮危核?jì)算出0,然后計(jì)算出1,同時(shí)丟棄掉0,然后最終計(jì)算出4,丟棄掉1,一個(gè)接著一個(gè)。

Yield 關(guān)鍵字

yield是一個(gè)類似與return的關(guān)鍵字,只是這個(gè)函數(shù)會(huì)返回的是一個(gè)生成器(Generator)

>>> def createGenerator():
...    mylist = range(3)
...    for i in mylist:
...        yield i*i
...
>>> mygenerator = createGenerator() # create a generator
>>> print(mygenerator) # mygenerator is an object!
<generator object createGenerator at 0xb7555c34>
>>> for i in mygenerator:
...     print(i)
0
1
4

這個(gè)例子并沒有什么用處,但是,我們可以看出,createGenerator函數(shù)會(huì)返回一大批你只需要讀取一次的值。

為了理解yield,你必須理解: 當(dāng)你調(diào)用這個(gè)函數(shù)時(shí),你寫在函數(shù)體內(nèi)部的代碼并沒有運(yùn)行。這個(gè)函數(shù)只是返回了一個(gè)生成器對象(這可能有一點(diǎn)tricky)。
然后,每次當(dāng)你使用for進(jìn)行迭代的時(shí)候,你的代碼才會(huì)執(zhí)行。

接下來是關(guān)鍵的部分
使用for第一次調(diào)用從你的函數(shù)中創(chuàng)建出的生成器對象時(shí),將會(huì)執(zhí)行從函數(shù)起始位置到yield所在位置的代碼并返回這個(gè)循環(huán)(函數(shù)內(nèi)部定義)的第一個(gè)值。隨后的每一次調(diào)用(for)都會(huì)繼續(xù)執(zhí)行你的函數(shù)內(nèi)部的下一輪循環(huán),并返回下一個(gè)值,直到?jīng)]有值可以返回為止。

原文:
The first time the for calls the generator object created from your function, it will run the code in your function from the beginning until it hits yield, then it'll return the first value of the loop. Then, each other call will run the loop you have written in the function one more time, and return the next value, until there is no value to return.

如果生成器內(nèi)部沒有yield關(guān)鍵字,那么這個(gè)生成器將會(huì)被認(rèn)為是空的。這可能是因?yàn)檠h(huán)結(jié)束了,或者是沒有滿足if/else判斷條件

代碼解析

  • 生成器
# Here you create the method of the node object that will return the generator
# 在node對象中創(chuàng)建一個(gè)返回生成器的方法
def _get_child_candidates(self, distance, min_dist, max_dist):

    # Here is the code that will be called each time you use the generator object:
    # 下面的代碼將會(huì)在每次調(diào)用生成器對象是調(diào)用

    # If there is still a child of the node object on its left
    # AND if distance is ok, return the next child
    # 如果節(jié)點(diǎn)有下一個(gè)左孩子,并且距離是符合要求的,返回下一個(gè)左孩子
    if self._leftchild and distance - max_dist < self._median:
        yield self._leftchild

    # If there is still a child of the node object on its right
    # AND if distance is ok, return the next child
    # 如果節(jié)點(diǎn)有下一個(gè)右孩子,并且距離是符合要求的,返回下一個(gè)右孩子
    if self._rightchild and distance + max_dist >= self._median:
        yield self._rightchild

    # If the function arrives here, the generator will be considered empty
    # there is no more than two values: the left and the right children
    # 當(dāng)函數(shù)走到這里時(shí),生成器將被認(rèn)為是空的
    # 這個(gè)節(jié)點(diǎn)最多只有一個(gè)子節(jié)點(diǎn)
  • 調(diào)用者
# Create an empty list and a list with the current object reference
# 創(chuàng)建一個(gè)空列表以及一個(gè)帶有當(dāng)前對象引用的列表
result, candidates = list(), [self]

# Loop on candidates (they contain only one element at the beginning)
# 在candidates中循環(huán)(在初始時(shí)值含有一個(gè)元素。根節(jié)點(diǎn))
while candidates:

    # Get the last candidate and remove it from the list
    # 獲取最近的一個(gè)candidate并將它從列表中移除
    node = candidates.pop()

    # Get the distance between obj and the candidate
    # 獲取obj和candidate的距離
    distance = node._get_dist(obj)

    # If distance is ok, then you can fill the result
    # 如果距離符合要求,填入result中
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)

    # Add the children of the candidate in the candidates list
    # so the loop will keep running until it will have looked
    # at all the children of the children of the children, etc. of the candidate
    # 將candidates列表中candidate的子節(jié)點(diǎn)添加到candidates列表中
    # 以確保遍歷了candidate所有的子節(jié)點(diǎn)以及子節(jié)點(diǎn)的子節(jié)點(diǎn)。。。
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))

return result

這份代碼包含了幾個(gè)巧妙的地方:

  • 我們對一個(gè)列表進(jìn)行迭代的同時(shí),列表還在不斷地?cái)U(kuò)展。盡管這樣做有可能導(dǎo)致無限迭代,但是它是一個(gè)簡單的遍歷所有數(shù)據(jù)的方式。在這個(gè)case中,candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))窮盡了生成器的所有值,但是while循環(huán)一直在創(chuàng)建新的生成器對象,由于傳入的節(jié)點(diǎn)不同,這些生成器對象會(huì)產(chǎn)生不同的值。
  • extend() 是一個(gè)迭代器方法,作用于迭代器本身并把值添加到列表中

通常,我們會(huì)傳一個(gè)list參數(shù)

>>> a = [1, 2]
>>> b = [3, 4]
>>> a.extend(b)
>>> print(a)
[1, 2, 3, 4]

但是,上面的代碼是一個(gè)生成器,這樣做很巧妙,因?yàn)椋?/p>

  1. 你不需要讀取每個(gè)值兩次
  2. 你可以有很多子對象,但是不必將他們都存儲在內(nèi)存里面

這份代碼是可以運(yùn)行的,因?yàn)?code>Python并不關(guān)心方法的參數(shù)是否是一個(gè)list對象,Python只期望它是一個(gè)可迭代的對象,所以參數(shù)可以是列表、元組、字符串、生成器...這叫做duck typing,這也是Python如此棒的原因之一,但這是題外話了...

關(guān)于代碼的解釋就到此為止了,下面是生成器的一些高級用法:

控制生成器的窮盡

>>> class Bank(): # Let's create a bank, building ATMs
...    crisis = False
...    def create_atm(self):
...        while not self.crisis:
...            yield "$100"
>>> hsbc = Bank() # When everything's ok the ATM gives you as much as you want
>>> corner_street_atm = hsbc.create_atm()
>>> print(corner_street_atm.next())
$100
>>> print(corner_street_atm.next())
$100
>>> print([corner_street_atm.next() for cash in range(5)])
['$100', '$100', '$100', '$100', '$100']
>>> hsbc.crisis = True # Crisis is coming, no more money!
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> wall_street_atm = hsbc.create_atm() # It's even true for new ATMs
>>> print(wall_street_atm.next())
<type 'exceptions.StopIteration'>
>>> hsbc.crisis = False # The trouble is, even post-crisis the ATM remains empty
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> brand_new_atm = hsbc.create_atm() # Build a new one to get back in business
>>> for cash in brand_new_atm:
...    print cash
$100
$100
$100
$100
$100
$100
$100
$100
$100
...

這能被用來做資源訪問權(quán)限控制

Itertools,你最好的朋友

itertools模塊包含了很多特殊的迭代方法,你是否曾經(jīng)想過復(fù)制一個(gè)迭代器?鏈接兩個(gè)迭代器?將嵌套的列表分組?執(zhí)行不創(chuàng)建新列表的zip/map操作?

所有的這些,只需要import itertools

來一個(gè)例子?讓我們看看比賽中4匹馬到達(dá)終點(diǎn)的先后順序的所有可能情況:

>>> horses = [1, 2, 3, 4]
>>> races = itertools.permutations(horses)
>>> print(races)
<itertools.permutations object at 0xb754f1dc>
>>> print(list(itertools.permutations(horses)))
[(1, 2, 3, 4),
 (1, 2, 4, 3),
 (1, 3, 2, 4),
 (1, 3, 4, 2),
 (1, 4, 2, 3),
 (1, 4, 3, 2),
 (2, 1, 3, 4),
 (2, 1, 4, 3),
 (2, 3, 1, 4),
 (2, 3, 4, 1),
 (2, 4, 1, 3),
 (2, 4, 3, 1),
 (3, 1, 2, 4),
 (3, 1, 4, 2),
 (3, 2, 1, 4),
 (3, 2, 4, 1),
 (3, 4, 1, 2),
 (3, 4, 2, 1),
 (4, 1, 2, 3),
 (4, 1, 3, 2),
 (4, 2, 1, 3),
 (4, 2, 3, 1),
 (4, 3, 1, 2),
 (4, 3, 2, 1)]

了解迭代器的內(nèi)部原理

迭代是一個(gè)實(shí)現(xiàn)可迭代對象(通過實(shí)現(xiàn)__iter__() 方法)和迭代器(通過__next__() 方法)的過程??傻鷮ο笫峭ㄟ^它可以獲取到一個(gè)迭代器的任一對象。迭代器是那些允許你迭代可迭代對象的對象。
更多細(xì)節(jié)可以閱讀 http://effbot.org/zone/python-for-statement.htm

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容