Python yield用法淺析(stackoverflow)
這是stackoverflow上一個關於python中yield用法的帖子,這裏翻譯自投票最高的一個回答,原文鏈接 here
問題
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. 這是完整源代碼的鏈接: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):
```# 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
<p>這段代碼包含幾個高明的部分:</p>
<ul>
<li>這個循環對列表進行叠代,但是叠代中列表還在不斷擴展 :-) 這是一種遍歷嵌套數據的簡明方法,即使這樣有些危險,因為你可能會陷入死循環中。在這個例子中,<code>candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))</code>窮盡了生成器產生的所有值,但<code>while</code>不斷的創建新的生成器對象加入到列表,因為每個對象作用在不同節點上,所以每個生成器都將生成不同的值。</li>
<li>
<code>extend()</code>是一個列表(list)對象的方法,作用於可叠代對象(iterable),並將其值添加到列表裏。</li>
</ul>
<p>通常,通常我們將列表作為參數傳遞給它:</p>
>>> a = [1, 2]
>>> b = [3, 4]
>>> a.extend(b)
>>> print(a)
[1, 2, 3, 4]
<p>但是在你的代碼裏它接收到的是一個生成器(generator),這很好,因為:</p>
<ol>
<li>你不必重復讀取這些值</li>
<li>你可以有很多子對象,但不需要將它們都存儲在內存裏。</li>
</ol>
<p>它很有效,因為Python不關心一個方法的參數是否是列表,Python只希望他是一個可叠代對象,所以這個參數可以是列表,元組,字符串和生成器!這就是所謂的<code>duck typing </code>,這也是Python為何如此酷的原因之一,但這已經是另外一個問題了......</p>
<p>你可以在這裏停下,來看一些生成器的高級用法:</p>
<h3>控制生成器的窮盡 (Controlling a generator exhaustion)</h3>
>>> 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
...
<p><strong> 註意,對於Python 3,請使用 <code>print(corner_street_atm.__next__())</code> 或者 <code>print(next(corner_street_atm))</code> </strong></p>
<p>這在很多場景都非常有用,例如控制資源的獲取。</p>
<h3>Itertools,你最好的朋友 (Itertools, your best friend)</h3>
<p>itertools模塊包含很多處理可叠代對象的特殊方法。曾經想要復制一個生成器嗎?連接兩個生成器?用一行代碼將嵌套列表中的值進行分組?不創建另一個列表進行<code>Map/Zip</code>?</p>
<p>只需要<code>import itertools</code></p>
<p>需要一個例子?讓我們來看看4匹馬賽跑到達終點先後順序的所有可能情況:</p>
>>> 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
循環如何工作的更多信息:here
來源:https://segmentfault.com/a/1190000017405045
Python yield用法淺析(stackoverflow)