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

所謂上下文

計(jì)算機(jī)上下文(Context)對(duì)于我而言酬滤,一直是一個(gè)很抽象的名詞镜廉。就像形而上一樣弄诲,經(jīng)常聽見有人說,但是無法和現(xiàn)實(shí)認(rèn)知世界相結(jié)合娇唯。

最直觀的上下文齐遵,莫過于小學(xué)的語(yǔ)文課,經(jīng)常會(huì)問聯(lián)系上下文塔插,推測(cè)...梗摇,回答...,表明作者...想许。文章里的上下文比較好懂伶授,無非就是断序。

直到了解了計(jì)算機(jī)的執(zhí)行狀態(tài),程式的運(yùn)行糜烹,才稍微對(duì)計(jì)算機(jī)的上下文(context)有了一定的認(rèn)識(shí)逢倍,多半還是只可意會(huì),不可言傳景图。本文所討論的上下文较雕,簡(jiǎn)而言之,就是程式所執(zhí)行的環(huán)境狀態(tài)挚币,或者說程式運(yùn)行的情景亮蒋。

關(guān)于上下文的定義,我就不在多言妆毕,具體通過程式來理解慎玖。既然提及上下文,就不可避免的涉及Python中關(guān)于上下文的魔法笛粘,即上下文管理器(contextor)趁怔。

資源的創(chuàng)建和釋放場(chǎng)景

上下文管理器的常用于一些資源的操作,需要在資源的獲取與釋放相關(guān)的操作薪前,一個(gè)典型的例子就是數(shù)據(jù)庫(kù)的連接润努,查詢,關(guān)閉處理示括。先看如下一個(gè)例子:

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()

上述的代碼很簡(jiǎn)單铺浇,針對(duì)Database這個(gè)數(shù)據(jù)庫(kù)類,提供了connect queryclose 三種常見的db交互接口垛膝△⒙拢客戶端的代碼中,需要查詢數(shù)據(jù)庫(kù)并處理查詢結(jié)果吼拥。當(dāng)然這個(gè)操作之前倚聚,需要連接數(shù)據(jù)庫(kù)(db.connect())和操作之后關(guān)閉數(shù)據(jù)庫(kù)連接( db.close())。上述的代碼可以work凿可,可是如果很多地方有類似handle_query的邏輯惑折,連接和關(guān)閉這樣的代碼就得copy很多遍,顯然不是一個(gè)優(yōu)雅的設(shè)計(jì)矿酵。

對(duì)于這樣的場(chǎng)景唬复,在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():
    ...

編寫一個(gè)dbconn的裝飾器全肮,然后在針對(duì)handle_query進(jìn)行裝飾即可敞咧。使用裝飾器,復(fù)用了很多數(shù)據(jù)庫(kù)連接和釋放的代碼邏輯辜腺,看起來不錯(cuò)休建。

裝飾器解放了生產(chǎn)力乍恐。可是测砂,每個(gè)裝飾器都需要事先定義一下db的資源句柄茵烈,看起來略丑,不夠優(yōu)雅砌些。

優(yōu)雅的With as語(yǔ)句

Python提供了With語(yǔ)句語(yǔ)法呜投,來構(gòu)建對(duì)資源創(chuàng)建與釋放的語(yǔ)法糖。給Database添加兩個(gè)魔法方法:

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類實(shí)例的時(shí)候存璃,使用with語(yǔ)句仑荐。一切正常work。比起裝飾器的版本纵东,雖然多寫了一些字符粘招,但是代碼可讀性變強(qiáng)了。

上下文管理協(xié)議

前面初略的提及了上下文偎球,那什么又是上下文管理器呢洒扎?與python黑魔法---迭代器類似,實(shí)現(xiàn)了迭代協(xié)議的函數(shù)/對(duì)象即為迭代器衰絮。實(shí)現(xiàn)了上下文協(xié)議的函數(shù)/對(duì)象即為上下文管理器袍冷。

迭代器協(xié)議是實(shí)現(xiàn)了__iter__方法。上下文管理協(xié)議則是__enter____exit__岂傲。對(duì)于如下代碼結(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 實(shí)現(xiàn)了__enter____exit__這兩個(gè)上下文管理器協(xié)議难裆,當(dāng)Contextor調(diào)用/實(shí)例化的時(shí)候,則創(chuàng)建了上下文管理器contextor镊掖。類似于實(shí)現(xiàn)迭代器協(xié)議類調(diào)用生成迭代器一樣。

配合with語(yǔ)句使用的時(shí)候褂痰,上下文管理器會(huì)自動(dòng)調(diào)用__enter__方法亩进,然后進(jìn)入運(yùn)行時(shí)上下文環(huán)境,如果有as 從句缩歪,返回自身或另一個(gè)與運(yùn)行時(shí)上下文相關(guān)的對(duì)象归薛,值賦值給var。當(dāng)with_body執(zhí)行完畢退出with語(yǔ)句塊或者with_body代碼塊出現(xiàn)異常匪蝙,則會(huì)自動(dòng)執(zhí)行__exit__方法主籍,并且會(huì)把對(duì)于的異常參數(shù)傳遞進(jìn)來。如果__exit__函數(shù)返回True逛球。則with語(yǔ)句代碼塊不會(huì)顯示的拋出異常千元,終止程序,如果返回None或者False颤绕,異常會(huì)被主動(dòng)raise幸海,并終止程序祟身。

大致對(duì)with語(yǔ)句的執(zhí)行原理總結(jié)Python上下文管理器與with語(yǔ)句:

  1. 執(zhí)行 contextor 以獲取上下文管理器
  2. 加載上下文管理器的 exit() 方法以備稍后調(diào)用
  3. 調(diào)用上下文管理器的 enter() 方法
  4. 如果有 as var 從句,則將 enter() 方法的返回值賦給 var
  5. 執(zhí)行子代碼塊 with_body
  6. 調(diào)用上下文管理器的 exit() 方法物独,如果 with_body 的退出是由異常引發(fā)的袜硫,那么該異常的 type、value 和 traceback 會(huì)作為參數(shù)傳給 exit()挡篓,否則傳三個(gè) None
  7. 如果 with_body 的退出由異常引發(fā)婉陷,并且 exit() 的返回值等于 False,那么這個(gè)異常將被重新引發(fā)一次官研;如果 exit() 的返回值等于 True秽澳,那么這個(gè)異常就被無視掉,繼續(xù)執(zhí)行后面的代碼

了解了with語(yǔ)句和上下文管理協(xié)議阀参,或許對(duì)上下文有了一個(gè)更清晰的認(rèn)識(shí)肝集。即代碼或函數(shù)執(zhí)行的時(shí)候,調(diào)用函數(shù)時(shí)候有一個(gè)環(huán)境蛛壳,在不同的環(huán)境調(diào)用杏瞻,有時(shí)候效果就不一樣,這些不同的環(huán)境就是上下文衙荐。例如數(shù)據(jù)庫(kù)連接之后創(chuàng)建了一個(gè)數(shù)據(jù)庫(kù)交互的上下文捞挥,進(jìn)入這個(gè)上下文,就能使用連接進(jìn)行查詢忧吟,執(zhí)行完畢關(guān)閉連接退出交互環(huán)境砌函。創(chuàng)建連接和釋放連接都需要有一個(gè)共同的調(diào)用環(huán)境。不同的上下文溜族,通常見于異步的代碼中讹俊。

上下文管理器工具

通過實(shí)現(xiàn)上下文協(xié)議定義創(chuàng)建上下文管理器很方便,Python為了更優(yōu)雅煌抒,還專門提供了一個(gè)模塊用于實(shí)現(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 定義一個(gè)上下文管理器函數(shù),通過with語(yǔ)句寡壮,database調(diào)用生成一個(gè)上下文管理器贩疙,然后調(diào)用函數(shù)隱式的__enter__方法,并將結(jié)果通yield返回况既。最后退出上下文環(huán)境的時(shí)候这溅,在excepit代碼塊中執(zhí)行了__exit__方法。當(dāng)然我們可以手動(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語(yǔ)句

In [4]: db                          # as語(yǔ)句悲靴,返回 Database實(shí)例
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語(yǔ)句

In [8]: db
Out[8]: <__main__.Database at 0x107188a10>

In [9]: db.connected
Out[9]: False

上下文管理器的用法

既然了解了上下文協(xié)議和管理器,當(dāng)然是運(yùn)用到實(shí)踐啦降狠。通常需要切換上下文環(huán)境对竣,往往是在多線程/進(jìn)程這種編程模型庇楞。當(dāng)然,單線程異步或者協(xié)程的當(dāng)時(shí)否纬,也容易出現(xiàn)函數(shù)的上下文環(huán)境經(jīng)常變動(dòng)吕晌。

異步式的代碼經(jīng)常在定義和運(yùn)行時(shí)存在不同的上下文環(huán)境。此時(shí)就需要針對(duì)異步代碼做上下文包裹的hack临燃【Σ担看下面一個(gè)例子:


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()

運(yùn)行上述代碼得到如下結(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)的異常爪瓜,則無法捕捉蹬跃。原因就是定義的時(shí)候上下文為當(dāng)前的線程執(zhí)行環(huán)境,而使用了tornado的ioloop.add_callback方法铆铆,注冊(cè)了一個(gè)異步的調(diào)用蝶缀。當(dāng)callback異步執(zhí)行的時(shí)候,他的上下文已經(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()
        
運(yùn)行main之后的結(jié)果如下:

run async task
handler except
run callback
handler except
exception except in callback

可見谅猾,callback的函數(shù)的異常柄慰,在上下文管理器Contextor中被處理了,也就是說callback調(diào)用的時(shí)候税娜,把之前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)然薯蝎,也許有人會(huì)對(duì)StackContext這個(gè)tornado的模塊感到迷惑。其實(shí)他恰恰應(yīng)用上下文管理器的魔法的典范谤绳。查看StackContext的源碼,實(shí)現(xiàn)非常精秒袒哥,非常佩服tornado作者的編碼設(shè)計(jì)能力缩筛。至于StackContext究竟如何神秘,已經(jīng)超出了本篇的范圍堡称,將會(huì)在介紹tonrado異步上下文管理器中介紹

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末瞎抛,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子却紧,更是在濱河造成了極大的恐慌桐臊,老刑警劉巖胎撤,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異断凶,居然都是意外死亡伤提,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門认烁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肿男,“玉大人,你說我怎么就攤上這事却嗡〔芭妫” “怎么了?”我有些...
    開封第一講書人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵窗价,是天一觀的道長(zhǎng)如庭。 經(jīng)常有香客問我,道長(zhǎng)撼港,這世上最難降的妖魔是什么坪它? 我笑而不...
    開封第一講書人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮餐胀,結(jié)果婚禮上哟楷,老公的妹妹穿的比我還像新娘。我一直安慰自己否灾,他們只是感情好卖擅,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著墨技,像睡著了一般惩阶。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上扣汪,一...
    開封第一講書人閱讀 49,046評(píng)論 1 285
  • 那天断楷,我揣著相機(jī)與錄音,去河邊找鬼崭别。 笑死冬筒,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的茅主。 我是一名探鬼主播舞痰,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼诀姚!你這毒婦竟也來了响牛?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎呀打,沒想到半個(gè)月后矢赁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡贬丛,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年撩银,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瘫寝。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蜒蕾,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出焕阿,到底是詐尸還是另有隱情咪啡,我是刑警寧澤,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布暮屡,位于F島的核電站撤摸,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏褒纲。R本人自食惡果不足惜准夷,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望莺掠。 院中可真熱鬧衫嵌,春花似錦、人聲如沸彻秆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)唇兑。三九已至酒朵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間扎附,已是汗流浹背蔫耽。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留留夜,地道東北人匙铡。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像碍粥,于是被迫代替她去往敵國(guó)和親慰枕。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • contextlib — Context Manager Utilities contextlib - 上下文管理...
    英武閱讀 2,705評(píng)論 0 52
  • 異步異常與上下文 在Python黑魔法---上下文管理器最后關(guān)于上下文的使用即纲,提到了tornado的處理方式。本篇...
    人世間閱讀 4,778評(píng)論 1 8
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理博肋,服務(wù)發(fā)現(xiàn)低斋,斷路器蜂厅,智...
    卡卡羅2017閱讀 134,599評(píng)論 18 139
  • 轉(zhuǎn)載自:http://mp.weixin.qq.com/s/LO1yyFeUA6pR_YPyfDoSig 姓名:梅...
    虐先森閱讀 1,418評(píng)論 0 1
  • 前兩天在微信公眾號(hào)里看了一片文章,是個(gè)妹子寫的年度計(jì)劃膊畴,計(jì)劃條理分明掘猿,也甚合我意,于是唇跨,參照她的模板稠通,我開始著手寫...
    拾月家閱讀 196評(píng)論 0 0