控制流程
- 在大部分情況下,Python 社群將 Iterator 與 Generator 視為同義詞
- Python 所有集合都是可迭代的
- 內部的 for 迴圈、集合生成式、變數和引數的 Unpacking 都會用到 Iterator
iter()
會先參考__iter__
,其次才參考__getitem__
,都沒有的話,發出 TypeError 代表「該物件不可迭代」(此處__getitem__
的參考在以後可能被棄用)- 承上,可迭代物件不一定滿足
isinstance(C, abc.Iterable)
(在未實作__iter__
的情況下),為了避免這個誤區,要判斷物件是否可迭代,最準確的方式是呼叫iter()
看看 - 如果 iter() 會過,那物件是「Iterable」;實作
__iter__
,須回傳一個「Iterator 實體」—— Python 會跟 Iterable 索取 Iterator - Iterator 類別的標準介面:
__iter__
跟__next__
,__next__
負責回傳下一個項目或發起 StopIteration,__iter__
則單純回傳self
- 不要把 Iterable 跟 Iterator 混為一談,「Iterable 有一個
__iter__
方法,這個方法每次都會實例化一個新的 Iterator」 - Iterator 也是 Iterable,但 Iterable 不是 Iterator。Iterable 永遠不該扮演自己的 Iterator
- Iterator 獨立出來的用意是「每一個迭代器都能保存它自己的內部狀態」
- 除了回傳獨立的 Iterator 實體,也可以將
__iter__
變成一個「Generator 函式」,藉由回傳一個「Generator 實體」,以介面而言,Generator 是 Iterator,它會在內文結束時發出 StopIteration - 用一個「 lazy 的產生器」取代一個「儲存所有資料的迭代器實體」是更好的,因為只要在必要時(最後一刻)才產生值,可以節省大量記憶體
- Iterator 的另一個功能是「延緩工作」、「一次只產生一個項目」
- 「當你在用 Python3 想著『有更 lazy 的作法嗎?』的時候,答案通常都是『有』」
yield from
不只是一個糖衣語法,除了取代迴圈之外,它也是一個管道,連接外部產生器,接收外部產生器的值all
、any
有一種重要的優化是reduce
無法作到的,那就是 short-circuit,確定結果後就停止sorted
可以接收任意的 Iterableiter()
的另一個功能:傳入一個 Callable 及一個標記值(sentinel),當回傳值等同此標記時,停止迭代- 無論資料大小為何,Generator 提供一種有彈性的解決方案,把大型資料集當做資料流來處理
.send()
同樣會讓產生器進入下一個yield
,但是它也可以用來傳入資料,相較於next()
單純接收資料,.send()
可讓使用者與產生器雙向交換資料——變成協同程序 (coroutines)- 「在內文埋入一個 yield,不足以提醒那一個語意有如此不同」(但 Guido 討厭使用新的關鍵字)
- 以實作而言,Generator 是一種語言結構,以函式或表達式編寫,呼叫時回傳
GeneratorType
- 以概念而言,不管 Iterator 內部有多複雜(例如是一個樹狀資料結構),它的資料永遠只有一個來源(自己本身);至於產生器,則不一定只產生集合裡面的項目
- 「Iterator 最簡單的介面是由 First、Next、IsDone、CurrentItem 的操作組成」,在 Python 它的介面更精簡:
next()
跟StopIteration
Context Manager 與 else 區塊
- 因為其他語言不常見,Python 的 Context Manager 沒被善用且容易被忽視
with
陳述式設定了一個「暫時性的情境」,這個情境在 Context Manager 的控制之下,可以可靠地退出/卸除- 除了
if/else
陳述式,else
子句也可以用在for/else
、while/else
、try/else
——當 for、while 迴圈被break
終止時,else 的語句不會執行;當 try 區塊捕獲例外時,else 語句不會執行 - 對 for、while、try 陳述式來說,else 是很爛的關鍵字,應該要叫做 then 才合理
- 為了讓程式更明確,try 區塊的內文只應該放入會產生例外的陳述,其他部份應該放到 else 區塊
try/except
不只是一種錯誤處理,還是一種流程控制流程控制流程控制- Python 風格的 Easier to ask for forgiveness than permission (EAFP) ,鮮明對比 C 的 Look before you leap (LBYL) ,後者在呼叫或進行查找之前,會明確地測試狀態,在多執行緒的環境下,LBYL 方法可能會產生 race condition (介於 looking 與 leaping),例如,某執行緒在 mapping 查找某 key 前,該 key 已經被其他執行緒移除了
- Context Manager 的存在是為了控制
with
陳述式;with
的存在,是為了簡化try/finally
模式 - finally 區塊通常會釋出一種重要的資源,或恢復原本的狀態
- Context Manager 的協定是
__enter__
跟__exit__
,with
陳述式會依序呼叫這兩個方法 __exit__
若回傳一個明確的值(例如 True/False),代表它以經處理例外了,解譯器會抑制 (subpress) 例外;若回傳 None,則代表例外沒有被處理,解譯器不會對例外做抑制- 三個引數會被傳遞給
__exit__
: exc_type(例外類別)、exc_value(例外實例,裡面通常會存錯誤訊息)、traceback - Context Manager 是一種還滿新的功能,經典的案例有: sqlite3 的交易管理、執行緒的 lock / condition / semaphore、設定 Decimal 的算術環境、透過套用補丁來測試物件(unittest.mock.patch)
- 在
__exit__
避免例外被抑制的方式是回傳 None(或是什麼也不回傳);在@contextmanager
修飾的函式中,避免例外被抑制的方式是「明確發起一個新的例外」
Context Manager 的快速樣板: @contextmanager
修飾器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
from contextlib import contextmanager @contextmanager def session_scope(db): session = Session(db) yield session try: session.commit() except: session.rollback() # 不抑制例外 raise finally: session.close() |
協同程序 Coroutines
- yield 有兩個解釋:「產生」或「讓路」;我們可以產生一個值,此值被呼叫方透過
next()
接收,同時,產生器也讓路了,它暫停執行,交出控制權讓呼叫方繼續執行 - 協同程序不一定要有任何資料交換,重點在於它可以做控制流程,用以實現協同式多工
- 協同程序基於產生器,主要的 API 為
.send()
、.throw()
、.close()
- 現在已經可以在 generator function 裡回傳(return)值了
- 協同程序的四種狀態(可透過
inspect.getgeneratorstate
查詢):GEN_CREATED
(等待開始)、GEN_RUNNING
(目前被解譯器執行)、GEN_SUSPENED
(已交出控制,停在 yield)、GEN_CLOSED
(執行完成) - 第一次啟動協同程序時,還沒辦法任何資料給它(此時缺少 yield 語句的停留),啟動方式為
next(gen)
或gen.send(None)
- 使用無窮迴圈製作的協同程序只有在無法處理例外時才會終止(包含呼叫
.close()
) - 協同程序在
.close()
或失去參考時會被記憶體回收
讓協同程序自行啟動的修飾器
1 2 3 4 5 6 7 8 9 10 |
from functools import wraps def coroutine(func): @wraps(func) def primer(*args, **kwargs): gen = func(*args, **kwargs) next(gen) # start coroutine return gen return primer |
asyncio.coroutine
修飾器是設計來與yield from
搭配使用的,它不會自動啟動協同程序- 協同程序未被處理的例外會傳播到呼叫方身上,一旦例外發生,協同程序變終止(一律發出 StopIteration)
- 呼叫方可透過
.throw()
把例外發到協同程序內,若協同程序無法處理,會一樣傳播回呼叫方 .close()
其實是把GeneratorExit
例外發到協同程序- 由於無窮迴圈的協同程序只有在無法處理例外時才終止,若須在產生器中清理資源可以將協同程序包在
try/finally
會回傳 (return) 結果的協同程序
Python 3.4 之後才能這樣做
回傳值被當成 StopIteration 的屬性,私下傳給呼叫方
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
def averager(): total = count = avg = 0 while True: term = yield if term is None: break total += term count += 1 avg = total / count return (total, count, avg) gen = averager() next(gen) gen.send(100) gen.send(200) try: gen.send(None) except StopIteration as exc: total, count, avg = exc.value |
- 當產生器 gen 呼叫
yield from subgen()
時,subgen 會接手產生值給呼叫方,同時,gen 會被阻塞直到 subgen 終止 yield from
是一種委託的功能,能開啟一個雙向的管道,連接最外部的呼叫方(caller)與內部的副產生器(sub-generator),值可以直接在兩者之間傳送,例外也可以被一路丟進去,而不影響到中間的代理產生器(delegating-generator)val = yield from subgen()
運算式的值是副產生器終止,發出 StopIteration 的第一個引數- 如果代理產生器發生 GeneratorExit 或被
.close()
終止,副產生器的方法.close()
一樣會被呼叫
離散時間模擬(discrete event simulation)是一種模擬型態,模擬的時鐘不會以固定的增量來前進,而是直接進入下一個事件的時間,這種模擬類型,可以用多執行緒或單執行緒,以事件導向程式設計(例如 callbacks、event loop)來編寫,使用執行緒來處理實際時間平行發生的動作,來做持續模擬,無疑是比較自然的作法
模擬程式中的程序 (process) 泛指模型中每個實例的動作,不是 OS 程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
class TaxiSimulator: def __init__(self, procs_map): self.events = queue.PriorityQueue() self.procs = dict(procs_map) def run(self, end_time): """Schedule and display events until time is up""" # schedule every taxi's first event for _, proc in sorted(self.procs.items()): first_event = next(proc) self.events.put(first_event) t = 0 while self.events and t < end_time: t, proc_id, prev = self.events.get() proc = self.procs[proc_id] next_t = t + compute_duration(prev) try: next_event = proc.send(next_time) except StopIteration: del self.procs[proc_id] else: next_event.put(next_event) taxis = { 0: taxi_process(no=0, trips=2, start_time=0), 1: taxi_process(no=1, trips=4, start_time=5), 2: taxi_process(no=2, trips=6, start_time=10) } TaxiSimulator(taxis).run(1000) |
更多離散模擬請參考 SimPy 函式庫
- 產生器可以用來編寫三種不同風格的程式:推送(push)、拉取(pull)、工作程序(tasks)
yield from subgen()
會假設 subgen 未被預先準備,這裡的 subgen 會被自動啟動- 協同程序是一種合作多工,是「自願」交出控制權給主迴圈/中央排程程式;相較於實作執行緒的搶佔式的多工(preemptive multi-tasking),中央排程可以隨時暫停執行緒來讓路給其他執行緒
- asyncio 的
await
功用很像yield from
,但只能在以async def
定義的協同程序中使用(當中不得使用 yield 與 yield from) - 在程式語言中加入新的關鍵字,就像在西洋棋中加入新的旗子,會徹底改變它