python黑魔法---上下文管理器(contextor)

發表於2017-02-03

所謂上下文

計算機上下文(Context)對於我而言,一直是一個很抽象的名詞。就像形而上一樣,經常聽見有人說,但是無法和現實認知世界相結合。

最直觀的上下文,莫過於小學的語文課,經常會問聯絡上下文,推測...,回答...,表明作者...。文章裡的上下文比較好懂,無非就是

直到了解了計算機的執行狀態,程式的執行,才稍微對計算機的上下文(context)有了一定的認識,多半還是隻可意會,不可言傳。本文所討論的上下文,簡而言之,就是程式所執行的環境狀態,或者說程式執行的情景。

關於上下文的定義,我就不在多言,具體通過程式來理解。既然提及上下文,就不可避免的涉及Python中關於上下文的魔法,即上下文管理器(contextor)。

資源的建立和釋放場景

上下文管理器的常用於一些資源的操作,需要在資源的獲取與釋放相關的操作,一個典型的例子就是資料庫的連線,查詢,關閉處理。先看如下一個例子:

上述的程式碼很簡單,針對Database這個資料庫類,提供了connect queryclose 三種常見的db互動介面。客戶端的程式碼中,需要查詢資料庫並處理查詢結果。當然這個操作之前,需要連線資料庫(db.connect())和操作之後關閉資料庫連線( db.close())。上述的程式碼可以work,可是如果很多地方有類似handle_query的邏輯,連線和關閉這樣的程式碼就得copy很多遍,顯然不是一個優雅的設計。

對於這樣的場景,在python黑魔法—裝飾器中有討論如何優雅的處理。下面使用裝飾器進行改寫如下:

編寫一個dbconn的裝飾器,然後在針對handle_query進行裝飾即可。使用裝飾器,複用了很多資料庫連線和釋放的程式碼邏輯,看起來不錯。

裝飾器解放了生產力。可是,每個裝飾器都需要事先定義一下db的資源控制程式碼,看起來略醜,不夠優雅。

優雅的With as語句

Python提供了With語句語法,來構建對資源建立與釋放的語法糖。給Database新增兩個魔法方法:

然後修改handle_query函式如下:

在Database類例項的時候,使用with語句。一切正常work。比起裝飾器的版本,雖然多寫了一些字元,但是程式碼可讀性變強了。

上下文管理協議

前面初略的提及了上下文,那什麼又是上下文管理器呢?與python黑魔法—迭代器類似,實現了迭代協議的函式/物件即為迭代器。實現了上下文協議的函式/物件即為上下文管理器。

迭代器協議是實現了__iter__方法。上下文管理協議則是__enter____exit__。對於如下程式碼結構:

Contextor 實現了__enter____exit__這兩個上下文管理器協議,當Contextor呼叫/例項化的時候,則建立了上下文管理器contextor。類似於實現迭代器協議類呼叫生成迭代器一樣。

配合with語句使用的時候,上下文管理器會自動呼叫__enter__方法,然後進入執行時上下文環境,如果有as 從句,返回自身或另一個與執行時上下文相關的物件,值賦值給var。當with_body執行完畢退出with語句塊或者with_body程式碼塊出現異常,則會自動執行__exit__方法,並且會把對於的異常引數傳遞進來。如果__exit__函式返回True。則with語句程式碼塊不會顯示的丟擲異常,終止程式,如果返回None或者False,異常會被主動raise,並終止程式。

大致對with語句的執行原理總結Python上下文管理器與with語句:

  1. 執行 contextor 以獲取上下文管理器
  2. 載入上下文管理器的 exit() 方法以備稍後呼叫
  3. 呼叫上下文管理器的 enter() 方法
  4. 如果有 as var 從句,則將 enter() 方法的返回值賦給 var
  5. 執行子程式碼塊 with_body
  6. 呼叫上下文管理器的 exit() 方法,如果 with_body 的退出是由異常引發的,那麼該異常的 type、value 和 traceback 會作為引數傳給 exit(),否則傳三個 None
  7. 如果 with_body 的退出由異常引發,並且 exit() 的返回值等於 False,那麼這個異常將被重新引發一次;如果 exit() 的返回值等於 True,那麼這個異常就被無視掉,繼續執行後面的程式碼

瞭解了with語句和上下文管理協議,或許對上下文有了一個更清晰的認識。即程式碼或函式執行的時候,呼叫函式時候有一個環境,在不同的環境呼叫,有時候效果就不一樣,這些不同的環境就是上下文。例如資料庫連線之後建立了一個資料庫互動的上下文,進入這個上下文,就能使用連線進行查詢,執行完畢關閉連線退出互動環境。建立連線和釋放連線都需要有一個共同的呼叫環境。不同的上下文,通常見於非同步的程式碼中。

上下文管理器工具

通過實現上下文協議定義建立上下文管理器很方便,Python為了更優雅,還專門提供了一個模組用於實現更函式式的上下文管理器用法。

使用contextlib 定義一個上下文管理器函式,通過with語句,database呼叫生成一個上下文管理器,然後呼叫函式隱式的__enter__方法,並將結果通yield返回。最後退出上下文環境的時候,在excepit程式碼塊中執行了__exit__方法。當然我們可以手動模擬上述程式碼的執行的細節。

上下文管理器的用法

既然瞭解了上下文協議和管理器,當然是運用到實踐啦。通常需要切換上下文環境,往往是在多執行緒/程式這種程式設計模型。當然,單執行緒非同步或者協程的當時,也容易出現函式的上下文環境經常變動。

非同步式的程式碼經常在定義和執行時存在不同的上下文環境。此時就需要針對非同步程式碼做上下文包裹的hack。看下面一個例子:

主函式中main中,定義了非同步任務函式async_task的呼叫。async_task中異常,在except中很容易catch,可是callback中出現的異常,則無法捕捉。原因就是定義的時候上下文為當前的執行緒執行環境,而使用了tornado的ioloop.add_callback方法,註冊了一個非同步的呼叫。當callback非同步執行的時候,他的上下文已經和async_task的上下文不一樣了。因此在main的上下文,無法catch非同步中callback的異常。

下面使用上下文管理器包裝如下:

可見,callback的函式的異常,在上下文管理器Contextor中被處理了,也就是說callback呼叫的時候,把之前main的上下文儲存並傳遞給了callback。當然,上述的程式碼也可以改寫如下:

效果類似。當然,也許有人會對StackContext這個tornado的模組感到迷惑。其實他恰恰應用上下文管理器的魔法的典範。檢視StackContext的原始碼,實現非常精秒,非常佩服tornado作者的編碼設計能力。至於StackContext究竟如何神祕,已經超出了本篇的範圍,將會在介紹tonrado非同步上下文管理器中介紹

相關文章