歡迎光臨
每天分享高質量文章

Python yield用法淺析

 

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

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

問題

Python中yield關鍵字的用途是什麼?它有什麼作用?
例如,我試圖理解以下程式碼:

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)?還是傳回了一個元素?然後被重覆呼叫了嗎?呼叫何時結束?

 

程式碼來自 Jochen Schulz (jrschulz), who made a great Python library for metric spaces.

回答

要想理解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!
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):

 

# Create an empty list and a list with the current object reference
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())
'exceptions.StopIteration'

>
>>> wall_street_atm = hsbc.create_atm() # It’s even true for new ATMs
>>> print(wall_street_atm.next())
‘exceptions.StopIteration’>
>>> hsbc.crisis = False # The trouble is, even post-crisis the ATM remains empty
>>> print(corner_street_atm.next())
‘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)
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迴圈如何工作的更多資訊:

http://effbot.org/zone/python-for-statement.htm

贊(0)

分享創造快樂