Skip to content

Fluent Python 讀書筆記(五)

  • Python

控制流程

  • 在大部分情況下,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 不只是一個糖衣語法,除了取代迴圈之外,它也是一個管道,連接外部產生器,接收外部產生器的值
  • allany 有一種重要的優化是 reduce 無法作到的,那就是 short-circuit,確定結果後就停止
  • sorted 可以接收任意的 Iterable
  • iter() 的另一個功能:傳入一個 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/elsewhile/elsetry/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 修飾器

協同程序 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() 或失去參考時會被記憶體回收

讓協同程序自行啟動的修飾器


  • asyncio.coroutine 修飾器是設計來與 yield from 搭配使用的,它不會自動啟動協同程序
  • 協同程序未被處理的例外會傳播到呼叫方身上,一旦例外發生,協同程序變終止(一律發出 StopIteration)
  • 呼叫方可透過 .throw() 把例外發到協同程序內,若協同程序無法處理,會一樣傳播回呼叫方
  • .close() 其實是把 GeneratorExit 例外發到協同程序
  • 由於無窮迴圈的協同程序只有在無法處理例外時才終止,若須在產生器中清理資源可以將協同程序包在 try/finally

會回傳 (return) 結果的協同程序

Python 3.4 之後才能這樣做

回傳值被當成 StopIteration 的屬性,私下傳給呼叫方


  • 當產生器 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 程序

更多離散模擬請參考 SimPy 函式庫


  • 產生器可以用來編寫三種不同風格的程式:推送(push)、拉取(pull)、工作程序(tasks)
  • yield from subgen() 會假設 subgen 未被預先準備,這裡的 subgen 會被自動啟動
  • 協同程序是一種合作多工,是「自願」交出控制權給主迴圈/中央排程程式;相較於實作執行緒的搶佔式的多工(preemptive multi-tasking),中央排程可以隨時暫停執行緒來讓路給其他執行緒
  • asyncio 的 await 功用很像 yield from,但只能在以 async def 定義的協同程序中使用(當中不得使用 yield 與 yield from)
  • 在程式語言中加入新的關鍵字,就像在西洋棋中加入新的旗子,會徹底改變它

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。