所謂上下文
計算機上下文(Context)對於我而言,一直是一個很抽象的名詞。就像形而上
一樣,經常聽見有人說,但是無法和現實認知世界相結合。
最直觀的上下文,莫過於小學的語文課,經常會問聯絡上下文,推測...,回答...,表明作者...
。文章裡的上下文比較好懂,無非就是前
與後
。
直到了解了計算機的執行狀態,程式的執行,才稍微對計算機的上下文(context)有了一定的認識,多半還是隻可意會,不可言傳。本文所討論的上下文,簡而言之,就是程式所執行的環境狀態,或者說程式執行的情景。
關於上下文的定義,我就不在多言,具體通過程式來理解。既然提及上下文,就不可避免的涉及Python中關於上下文的魔法,即上下文管理器(contextor)。
資源的建立和釋放場景
上下文管理器的常用於一些資源的操作,需要在資源的獲取與釋放相關的操作,一個典型的例子就是資料庫的連線,查詢,關閉處理。先看如下一個例子:
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 |
class Database(object): def __init__(self): self.connected = False def connect(self): self.connected = True def close(self): self.connected = False def query(self): if self.connected: return 'query data' else: raise ValueError('DB not connected ') def handle_query(): db = Database() db.connect() print 'handle --- ', db.query() db.close() def main(): handle_query() if __name__ == '__main__': main() |
上述的程式碼很簡單,針對Database
這個資料庫類,提供了connect
query
和close
三種常見的db互動介面。客戶端的程式碼中,需要查詢資料庫並處理查詢結果。當然這個操作之前,需要連線資料庫(db.connect())和操作之後關閉資料庫連線( db.close())。上述的程式碼可以work,可是如果很多地方有類似handle_query的邏輯,連線和關閉這樣的程式碼就得copy很多遍,顯然不是一個優雅的設計。
對於這樣的場景,在python黑魔法—裝飾器中有討論如何優雅的處理。下面使用裝飾器進行改寫如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class Database(object): ... def dbconn(fn): def wrapper(*args, **kwargs): db = Database() db.connect() ret = fn(db, *args, **kwargs) db.close() return ret return wrapper @dbconn def handle_query(db=None): print 'handle --- ', db.query() def main(): ... |
編寫一個dbconn的裝飾器,然後在針對handle_query進行裝飾即可。使用裝飾器,複用了很多資料庫連線和釋放的程式碼邏輯,看起來不錯。
裝飾器解放了生產力。可是,每個裝飾器都需要事先定義一下db的資源控制程式碼,看起來略醜,不夠優雅。
優雅的With as語句
Python提供了With語句語法,來構建對資源建立與釋放的語法糖。給Database新增兩個魔法方法:
1 2 3 4 5 6 7 8 9 10 |
class Database(object): ... def __enter__(self): self.connect() return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() |
然後修改handle_query函式如下:
1 2 3 |
def handle_query(): with Database() as db: print 'handle ---', db.query() |
在Database類例項的時候,使用with語句。一切正常work。比起裝飾器的版本,雖然多寫了一些字元,但是程式碼可讀性變強了。
上下文管理協議
前面初略的提及了上下文,那什麼又是上下文管理器呢?與python黑魔法—迭代器類似,實現了迭代協議的函式/物件即為迭代器。實現了上下文協議的函式/物件即為上下文管理器。
迭代器協議是實現了__iter__
方法。上下文管理協議則是__enter__
和__exit__
。對於如下程式碼結構:
1 2 3 4 5 6 7 8 9 10 11 |
class Contextor: def __enter__(self): pass def __exit__(self, exc_type, exc_val, exc_tb): pass contextor = Contextor() with contextor [as var]: with_body |
Contextor
實現了__enter__
和__exit__
這兩個上下文管理器協議,當Contextor呼叫/例項化的時候,則建立了上下文管理器contextor
。類似於實現迭代器協議類呼叫生成迭代器一樣。
配合with語句使用的時候,上下文管理器會自動呼叫__enter__
方法,然後進入執行時上下文環境,如果有as 從句,返回自身或另一個與執行時上下文相關的物件,值賦值給var。當with_body執行完畢退出with語句塊或者with_body程式碼塊出現異常,則會自動執行__exit__
方法,並且會把對於的異常引數傳遞進來。如果__exit__
函式返回True
。則with語句程式碼塊不會顯示的丟擲異常,終止程式,如果返回None或者False,異常會被主動raise,並終止程式。
大致對with語句的執行原理總結Python上下文管理器與with語句:
- 執行 contextor 以獲取上下文管理器
- 載入上下文管理器的 exit() 方法以備稍後呼叫
- 呼叫上下文管理器的 enter() 方法
- 如果有 as var 從句,則將 enter() 方法的返回值賦給 var
- 執行子程式碼塊 with_body
- 呼叫上下文管理器的 exit() 方法,如果 with_body 的退出是由異常引發的,那麼該異常的 type、value 和 traceback 會作為引數傳給 exit(),否則傳三個 None
- 如果 with_body 的退出由異常引發,並且 exit() 的返回值等於 False,那麼這個異常將被重新引發一次;如果 exit() 的返回值等於 True,那麼這個異常就被無視掉,繼續執行後面的程式碼
瞭解了with語句和上下文管理協議,或許對上下文有了一個更清晰的認識。即程式碼或函式執行的時候,呼叫函式時候有一個環境,在不同的環境呼叫,有時候效果就不一樣,這些不同的環境就是上下文。例如資料庫連線之後建立了一個資料庫互動的上下文,進入這個上下文,就能使用連線進行查詢,執行完畢關閉連線退出互動環境。建立連線和釋放連線都需要有一個共同的呼叫環境。不同的上下文,通常見於非同步的程式碼中。
上下文管理器工具
通過實現上下文協議定義建立上下文管理器很方便,Python為了更優雅,還專門提供了一個模組用於實現更函式式的上下文管理器用法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import contextlib @contextlib.contextmanager def database(): db = Database() try: if not db.connected: db.connect() yield db except Exception as e: db.close() def handle_query(): with database() as db: print 'handle ---', db.query() |
使用contextlib 定義一個上下文管理器函式,通過with語句,database呼叫生成一個上下文管理器,然後呼叫函式隱式的__enter__
方法,並將結果通yield返回。最後退出上下文環境的時候,在excepit程式碼塊中執行了__exit__
方法。當然我們可以手動模擬上述程式碼的執行的細節。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
In [1]: context = database() # 建立上下文管理器 In [2]: context In [3]: db = context.__enter__() # 進入with語句 In [4]: db # as語句,返回 Database例項 Out[4]: In [5]: db.query() Out[5]: 'query data' In [6]: db.connected Out[6]: True In [7]: db.__exit__(None, None, None) # 退出with語句 In [8]: db Out[8]: In [9]: db.connected Out[9]: False |
上下文管理器的用法
既然瞭解了上下文協議和管理器,當然是運用到實踐啦。通常需要切換上下文環境,往往是在多執行緒/程式這種程式設計模型。當然,單執行緒非同步或者協程的當時,也容易出現函式的上下文環境經常變動。
非同步式的程式碼經常在定義和執行時存在不同的上下文環境。此時就需要針對非同步程式碼做上下文包裹的hack。看下面一個例子:
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 33 34 |
import tornado.ioloop ioloop = tornado.ioloop.IOLoop.instance() def callback(): print 'run callback' raise ValueError('except in callback') def async_task(): print 'run async task' ioloop.add_callback(callback=callback) def main(): try: async_task() except Exception as e: print 'exception {}'.format(e) print 'end' main() ioloop.start() 執行上述程式碼得到如下結果 run async task end run callback ERROR:root:Exception in callback Traceback (most recent call last): ... raise ValueError('except in callback') ValueError: except in callback |
主函式中main中,定義了非同步任務函式async_task的呼叫。async_task中異常,在except中很容易catch,可是callback中出現的異常,則無法捕捉。原因就是定義的時候上下文為當前的執行緒執行環境,而使用了tornado的ioloop.add_callback方法,註冊了一個非同步的呼叫。當callback非同步執行的時候,他的上下文已經和async_task的上下文不一樣了。因此在main的上下文,無法catch非同步中callback的異常。
下面使用上下文管理器包裝如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class Contextor(object): def __enter__(self): pass def __exit__(self, exc_type, exc_val, exc_tb): if all([exc_type, exc_val, exc_tb]): print 'handler except' print 'exception {}'.format(exc_val) return True def main(): with tornado.stack_context.StackContext(Contextor): async_task() 執行main之後的結果如下: run async task handler except run callback handler except exception except in callback |
可見,callback的函式的異常,在上下文管理器Contextor中被處理了,也就是說callback呼叫的時候,把之前main的上下文儲存並傳遞給了callback。當然,上述的程式碼也可以改寫如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@contextlib.contextmanager def contextor(): try: yield except Exception as e: print 'handler except' print 'exception {}'.format(e) finally: print 'release' def main(): with tornado.stack_context.StackContext(contextor): async_task() |
效果類似。當然,也許有人會對StackContext這個tornado的模組感到迷惑。其實他恰恰應用上下文管理器的魔法的典範。檢視StackContext的原始碼,實現非常精秒,非常佩服tornado作者的編碼設計能力。至於StackContext究竟如何神祕,已經超出了本篇的範圍,將會在介紹tonrado非同步上下文管理器中介紹