天天看點

Python yield用法淺析(stackoverflow)

這是stackoverflow上一個關于python中yield用法的文章,這裡翻譯自投票最高的一個回答,原文連結 here

問題

Python中

yield

關鍵字的用途是什麼?它有什麼作用?

例如,我試圖了解以下代碼 &sup1:

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 
           

這是調用者(caller):

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
           

當調用方法

_get_child_candidates

時會發生什麼?傳回了一個清單(list)?還是傳回了一個元素?然後被重複調用了嗎?調用何時結束?

&sup1 :代碼來自 Jochen Schulz (jrschulz), who made a great Python library for metric spaces. 這是完整源代碼的連結:

Module mspace

.

回答

要想了解

yield

的作用,你必須了解什麼是生成器(generators),在這之前,我們先來看可疊代對象(iterables)。

可疊代對象 (iterables)

當你建立了一個清單,你可以周遊這個清單讀取它的每一個元素,逐個讀取清單元素稱為疊代(iteration)。

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

mylist

就是一個可疊代對象(iterable)。當你使用清單生成式(list comprehension)建立一個清單(list),即建立了一個可疊代對象。

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

可以使用

for... in...

的所有對象都是可疊代對象:清單(lists)、字元串、檔案...

這些可疊代對象使用很友善,因為你可以根據需要如你所願的讀取其中的元素。但是,當你有大量資料時把所有值都存儲在記憶體中,這樣往往不是你想要的( but you store all the values in memory and this is not always what you want when you have a lot of values.)。

生成器 (Generators)

生成器是疊代器(iterators),但是隻能疊代一次,生成器不會将所有值存儲在記憶體中,而是實時的生成這些值:

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

看上去除了用

()

替換了原來的

[]

外,它們沒什麼不同。但是,你不可以再次使用

for i in mygenerator

,因為生成器隻能被疊代一次:計算出0,然後并不儲存結果和狀态繼續計算出1,最後計算出4,逐一生成。

yield

yield

是一個類似

return

的關鍵字,不同的是這個函數将傳回一個生成器。

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

這個例子沒有什麼實際作用。但是當你知道你的函數将傳回大量你隻需要讀取一次的值時,使用生成器是一個有效的做法。

要掌握

yeild

,你必須要知道當你調用這個函數時,你在函數體中編寫的代碼并沒有立馬執行。

該函數僅僅傳回一個生成器對象,這有點棘手 :-)

然後,你的代碼将從

for

循環每次使用生成器停止的位置繼續執行。

現在到了關鍵部分:

for

第一次調用從函數建立的生成器對象,函數将從頭開始執行直到遇到

yeild

,然後傳回

yield

後的值作為第一次疊代的傳回值。接下來每次調用都會再次執行你在函數中定義的循環,并傳回(return)下一個值,直到沒有值可以傳回(return)。

當循環結束,或者不滿足

if/else

條件,導緻函數運作但不會執行(not hit)

yeild

,此時生成器被認為是空的。

問題代碼的解釋 (Your code explained)

生成器 (Generator):

# Here you create the method of the node object that will return the generator
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:

    # If there is still a child of the node object on its left
    # AND if distance is ok, return the next child
    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
    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
           

調用者 (Caller):

result, candidates = list(), [self]

# Loop on candidates (they contain only one element at the beginning)
while candidates:

    # Get the last candidate and remove it from the list
    node = candidates.pop()

    # Get the distance between obj and the candidate
    distance = node._get_dist(obj)

    # If distance is ok, then you can fill the 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.extend(node._get_child_candidates(distance, min_dist, max_dist))

return result
           

這段代碼包含幾個高明的部分:

  • 這個循環對清單進行疊代,但是疊代中清單還在不斷擴充 :-) 這是一種周遊嵌套資料的簡明方法,即使這樣有些危險,因為你可能會陷入死循環中。在這個例子中,

    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))

    窮盡了生成器産生的所有值,但

    while

    不斷的建立新的生成器對象加入到清單,因為每個對象作用在不同節點上,是以每個生成器都将生成不同的值。
  • extend()

    是一個清單(list)對象的方法,作用于可疊代對象(iterable),并将其值添加到清單裡。

通常,通常我們将清單作為參數傳遞給它:

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

但是在你的代碼裡它接收到的是一個生成器(generator),這很好,因為:

  1. 你不必重複讀取這些值
  2. 你可以有很多子對象,但不需要将它們都存儲在記憶體裡。

它很有效,因為Python不關心一個方法的參數是否是清單,Python隻希望他是一個可疊代對象,是以這個參數可以是清單,元組,字元串和生成器!這就是所謂的

duck typing

,這也是Python為何如此酷的原因之一,但這已經是另外一個問題了......

你可以在這裡停下,來看一些生成器的進階用法:

控制生成器的窮盡 (Controlling a generator exhaustion)

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

注意,對于Python 3,請使用

print(corner_street_atm.__next__())

或者

print(next(corner_street_atm))

這在很多場景都非常有用,例如控制資源的擷取。

Itertools,你最好的朋友 (Itertools, your best friend)

itertools子產品包含很多處理可疊代對象的特殊方法。曾經想要複制一個生成器嗎?連接配接兩個生成器?用一行代碼将嵌套清單中的值進行分組?不建立另一個清單進行

Map/Zip

隻需要

import itertools

需要一個例子?讓我們來看看4匹馬賽跑到達終點先後順序的所有可能情況:

>>> 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)]
           

了解疊代的内部機制 (Understanding the inner mechanisms of iteration)

疊代是一個實作可疊代對象(實作的是 __iter__() 方法)和疊代器(實作的是 __next__() 方法)的過程。你可以擷取一個疊代器的任何對象都是可疊代對象,疊代器可以讓你疊代周遊一個可疊代對象(Iterators are objects that let you iterate on iterables.) .

在這篇文章中有關于

for

循環如何工作的更多資訊:

來源:

https://segmentfault.com/a/1190000017405045