所謂上下文
計算機上下文(Context)對于我而言,一直是一個很抽象的名詞。就像形而上一樣,經(jīng)常聽見有人說,但是無法和現(xiàn)實認(rèn)知世界相結(jié)合。
最直觀的上下文,莫過于小學(xué)的語文課,經(jīng)常會問聯(lián)系上下文,推測...,回答...,表明作者...。文章里的上下文比較好懂,無非就是前與后。
直到了解了計算機的執(zhí)行狀態(tài),程式的運行,才稍微對計算機的上下文(context)有了一定的認(rèn)識,多半還是只可意會,不可言傳。本文所討論的上下文,簡而言之,就是程式所執(zhí)行的環(huán)境狀態(tài),或者說程式運行的情景。
關(guān)于上下文的定義,我就不在多言,具體通過程式來理解。既然提及上下文,就不可避免的涉及Python中關(guān)于上下文的魔法,即上下文管理器(contextor)。
資源的創(chuàng)建和釋放場景
上下文管理器的常用于一些資源的操作,需要在資源的獲取與釋放相關(guān)的操作,一個典型的例子就是數(shù)據(jù)庫的連接,查詢,關(guān)閉處理。先看如下一個例子:
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這個數(shù)據(jù)庫類,提供了connect query 和close 三種常見的db交互接口??蛻舳说拇a中,需要查詢數(shù)據(jù)庫并處理查詢結(jié)果。當(dāng)然這個操作之前,需要連接數(shù)據(jù)庫(db.connect())和操作之后關(guān)閉數(shù)據(jù)庫連接( db.close())。上述的代碼可以work,可是如果很多地方有類似handle_query的邏輯,連接和關(guān)閉這樣的代碼就得copy很多遍,顯然不是一個優(yōu)雅的設(shè)計。
對于這樣的場景,在python黑魔法---裝飾器中有討論如何優(yōu)雅的處理。下面使用裝飾器進(jìn)行改寫如下:
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進(jìn)行裝飾即可。使用裝飾器,復(fù)用了很多數(shù)據(jù)庫連接和釋放的代碼邏輯,看起來不錯。
裝飾器解放了生產(chǎn)力??墒牵總€裝飾器都需要事先定義一下db的資源句柄,看起來略丑,不夠優(yōu)雅。
優(yōu)雅的With as語句
Python提供了With語句語法,來構(gòu)建對資源創(chuàng)建與釋放的語法糖。給Database添加兩個魔法方法:
class Database(object):
...
def __enter__(self):
self.connect()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
然后修改handle_query函數(shù)如下:
def handle_query():
with Database() as db:
print 'handle ---', db.query()
在Database類實例的時候,使用with語句。一切正常work。比起裝飾器的版本,雖然多寫了一些字符,但是代碼可讀性變強了。
上下文管理協(xié)議
前面初略的提及了上下文,那什么又是上下文管理器呢?與python黑魔法---迭代器類似,實現(xiàn)了迭代協(xié)議的函數(shù)/對象即為迭代器。實現(xiàn)了上下文協(xié)議的函數(shù)/對象即為上下文管理器。
迭代器協(xié)議是實現(xiàn)了__iter__方法。上下文管理協(xié)議則是__enter__和__exit__。對于如下代碼結(jié)構(gòu):
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 實現(xiàn)了__enter__和__exit__這兩個上下文管理器協(xié)議,當(dāng)Contextor調(diào)用/實例化的時候,則創(chuàng)建了上下文管理器contextor。類似于實現(xiàn)迭代器協(xié)議類調(diào)用生成迭代器一樣。
配合with語句使用的時候,上下文管理器會自動調(diào)用__enter__方法,然后進(jìn)入運行時上下文環(huán)境,如果有as 從句,返回自身或另一個與運行時上下文相關(guān)的對象,值賦值給var。當(dāng)with_body執(zhí)行完畢退出with語句塊或者with_body代碼塊出現(xiàn)異常,則會自動執(zhí)行__exit__方法,并且會把對于的異常參數(shù)傳遞進(jìn)來。如果__exit__函數(shù)返回True。則with語句代碼塊不會顯示的拋出異常,終止程序,如果返回None或者False,異常會被主動raise,并終止程序。
大致對with語句的執(zhí)行原理總結(jié)Python上下文管理器與with語句:
- 執(zhí)行 contextor 以獲取上下文管理器
- 加載上下文管理器的 exit() 方法以備稍后調(diào)用
- 調(diào)用上下文管理器的 enter() 方法
- 如果有 as var 從句,則將 enter() 方法的返回值賦給 var
- 執(zhí)行子代碼塊 with_body
- 調(diào)用上下文管理器的 exit() 方法,如果 with_body 的退出是由異常引發(fā)的,那么該異常的 type、value 和 traceback 會作為參數(shù)傳給 exit(),否則傳三個 None
- 如果 with_body 的退出由異常引發(fā),并且 exit() 的返回值等于 False,那么這個異常將被重新引發(fā)一次;如果 exit() 的返回值等于 True,那么這個異常就被無視掉,繼續(xù)執(zhí)行后面的代碼
了解了with語句和上下文管理協(xié)議,或許對上下文有了一個更清晰的認(rèn)識。即代碼或函數(shù)執(zhí)行的時候,調(diào)用函數(shù)時候有一個環(huán)境,在不同的環(huán)境調(diào)用,有時候效果就不一樣,這些不同的環(huán)境就是上下文。例如數(shù)據(jù)庫連接之后創(chuàng)建了一個數(shù)據(jù)庫交互的上下文,進(jìn)入這個上下文,就能使用連接進(jìn)行查詢,執(zhí)行完畢關(guān)閉連接退出交互環(huán)境。創(chuàng)建連接和釋放連接都需要有一個共同的調(diào)用環(huán)境。不同的上下文,通常見于異步的代碼中。
上下文管理器工具
通過實現(xiàn)上下文協(xié)議定義創(chuàng)建上下文管理器很方便,Python為了更優(yōu)雅,還專門提供了一個模塊用于實現(xiàn)更函數(shù)式的上下文管理器用法。
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 定義一個上下文管理器函數(shù),通過with語句,database調(diào)用生成一個上下文管理器,然后調(diào)用函數(shù)隱式的__enter__方法,并將結(jié)果通yield返回。最后退出上下文環(huán)境的時候,在excepit代碼塊中執(zhí)行了__exit__方法。當(dāng)然我們可以手動模擬上述代碼的執(zhí)行的細(xì)節(jié)。
In [1]: context = database() # 創(chuàng)建上下文管理器
In [2]: context
<contextlib.GeneratorContextManager object at 0x107188f10>
In [3]: db = context.__enter__() # 進(jìn)入with語句
In [4]: db # as語句,返回 Database實例
Out[4]: <__main__.Database at 0x107188a10>
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]: <__main__.Database at 0x107188a10>
In [9]: db.connected
Out[9]: False
上下文管理器的用法
既然了解了上下文協(xié)議和管理器,當(dāng)然是運用到實踐啦。通常需要切換上下文環(huán)境,往往是在多線程/進(jìn)程這種編程模型。當(dāng)然,單線程異步或者協(xié)程的當(dāng)時,也容易出現(xiàn)函數(shù)的上下文環(huán)境經(jīng)常變動。
異步式的代碼經(jīng)常在定義和運行時存在不同的上下文環(huán)境。此時就需要針對異步代碼做上下文包裹的hack??聪旅嬉粋€例子:
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()
運行上述代碼得到如下結(jié)果
run async task
end
run callback
ERROR:root:Exception in callback <tornado.stack_context._StackContextWrapper object at 0x1098cb7e0>
Traceback (most recent call last):
...
raise ValueError('except in callback')
ValueError: except in callback
主函數(shù)中main中,定義了異步任務(wù)函數(shù)async_task的調(diào)用。async_task中異常,在except中很容易catch,可是callback中出現(xiàn)的異常,則無法捕捉。原因就是定義的時候上下文為當(dāng)前的線程執(zhí)行環(huán)境,而使用了tornado的ioloop.add_callback方法,注冊了一個異步的調(diào)用。當(dāng)callback異步執(zhí)行的時候,他的上下文已經(jīng)和async_task的上下文不一樣了。因此在main的上下文,無法catch異步中callback的異常。
下面使用上下文管理器包裝如下:
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之后的結(jié)果如下:
run async task
handler except
run callback
handler except
exception except in callback
可見,callback的函數(shù)的異常,在上下文管理器Contextor中被處理了,也就是說callback調(diào)用的時候,把之前main的上下文保存并傳遞給了callback。當(dāng)然,上述的代碼也可以改寫如下:
@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()
效果類似。當(dāng)然,也許有人會對StackContext這個tornado的模塊感到迷惑。其實他恰恰應(yīng)用上下文管理器的魔法的典范。查看StackContext的源碼,實現(xiàn)非常精秒,非常佩服tornado作者的編碼設(shè)計能力。至于StackContext究竟如何神秘,已經(jīng)超出了本篇的范圍,將會在介紹tonrado異步上下文管理器中介紹