Iterators and Generators
Iterators and Generators
for element in iterable:
pass # do something with element
在 Python 如果要迭代某個容器,上面的程式碼是很常見的做法,只要這個物件是 iterable 都可以這樣處理,例如 list set dict 等,或者讀檔也有類似的寫法。那要怎樣才能產生 iterator 呢?我們可以透過 iter(iterable) 得到 iterator,再用 next(iterator) 依序查訪容器裡每個元素,直到 iterator 拋出 StopIteration exception,Python 則提供 for-loop 語法簡化這個流程。
generator 長得很像 iterator,實作的方法是寫一個 function,一般的 function 如果需要回傳值可以用 return 回傳結果給 caller,但 generator 用 yield 來回傳結果。下面提供一個經典範例:[1]
def fibonacci(n):
a, b = 0, 1
for i in range(n):
yield a
a, b = b, a + b
[x for x in fibonacci(10)] # print [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
generator 執行流程長得很像 coroutine,可以有多個 entry point,再透過 generator method 來控制執行流程。[2]
由於 generator 太過強大,導致筆者常常寫出佈滿臭蟲的 generator,不過用得好的話,也可以讓我們的程式碼看起來更簡潔。話不多說,再來看看一個簡單的範例:
g1 = (i for i in range(1, 10))
g2 = (i * pow(10, i) for i in g1)
g3 = (pow((1 + 1 / i), i) for i in g2)
[e for e in g3] # Euler's number
上面的 (generator expressions) 是一行版的 generator,並沒有用到 yield 語法。一層一層把 generator 傳下去,資料可以在每一層做相對應的處理。[3]
讀者應該可以漸漸感受到 generator 強大的魔力,接下來我們要開始用可怕的 yield 語法。免責聲明,下面兩個範例純屬示範 XD
大老二發牌
import itertools, random, collections
def coroutine(func):
"""
用來 activate generator
"""
def decorated(*args, **kwargs):
g = func(*args, **kwargs)
next(g)
return g
return decorated
def dealer(players):
"""
撲克牌大老二,每張牌用 (number, color) 來表示
"""
poker = [card for card in itertools.product(range(1, 13 + 1), range(4))]
random.shuffle(poker)
for i, card in enumerate(poker):
players[i % len(players)].send(card)
for p in players:
p.close()
@coroutine
def player(name=None):
player = dict(name=name, hand=[])
try:
while True:
card = (yield)
player['hand'].append(card)
finally:
sort_card().send(player)
print(player)
@coroutine
def sort_card():
player = (yield)
player['hand'] = sorted(player['hand'], key=lambda t: (14, 3) if t == (2, 3) else t)
yield 'end'
players = [player() for i in range(4)]
dealer(players)
十點半發牌
def dealer(players):
"""
撲克牌十點半
"""
poker = [card for card in itertools.product(range(1, 13 + 1), range(4))]
poker = [*filter(lambda card: card[1] in (0, 3), poker)] # 濾掉紅色的牌
random.shuffle(poker)
players.append(player(name='dealer', dealer=True)) # 莊家
compare_pipe = compare_with_dealer()
while len(players) > 0:
p = players[0]
players.rotate()
card = poker.pop()
feedback = p.send(card)
if feedback is False:
p.send(compare_pipe)
players.remove(p)
compare_pipe.close()
@coroutine
def player(name=None, dealer=False):
player_record = dict(name=name, hand=[], dealer=dealer)
while True:
draw = sum(0.5 if point > 10 else point for point, color in player_record['hand']) < 10.5 - 3
if draw:
card = (yield draw)
player_record['hand'].append(card)
else:
compare_pipe = (yield draw)
compare_pipe.send(player_record)
@coroutine
def compare_with_dealer():
player_records = []
try:
while True:
player_record = (yield)
player_record['score'] = sum(0.5 if point > 10 else point for point, color in player_record['hand'])
if player_record['dealer']:
dealer_record = player_record
else:
player_records.append(player_record)
finally:
for player_record in player_records:
if dealer_record['score'] > 10.5 > player_record['score'] or 10.5 >= player_record['score'] > dealer_record['score']:
print('winner is {name}'.format(**player_record))
dealer(collections.deque([player(name=i) for i in range(4)]))
打完收工(咦?),這兩個範例只是為了測試 generator.send 而想出來的練習,平常應該不會想要跟 next(generator) 一起使用。generator.send 除了可以傳值給 generator,也可以接收 generator 回傳的 feedback,但請小心使用。當 caller 用 generator.close 結束 generator 時,generator 裡面的 try … finally 語法可以在結束前執行一段程式碼。
以上程式碼皆在 Python 3.5.3 跑過,筆記先寫到這邊,希望各位讀者使用 generator 愉快 : )