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

所謂上下文html

 

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

 

最直觀的上下文,莫過於小學的語文課,常常會問聯繫上下文,推測...,回答...,代表做者...。文章裏的上下文比較好懂,無非就是前與後。數據庫

 

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

 

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

 

資源的建立和釋放場景app

 

上下文管理器的經常使用於一些資源的操做,須要在資源的獲取與釋放相關的操做,一個典型的例子就是數據庫的鏈接,查詢,關閉處理。先看以下一個例子:異步

 

class Database(object):async

 

    def __init__(self):函數

        self.connected = Falsetornado

 

    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黑魔法—裝飾器中有討論如何優雅的處理。下面使用裝飾器進行改寫以下:

 

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添加兩個魔法方法:

 

class Database(object):

 

    ...

 

    def __enter__(self):

        self.connect()

        return self

 

    def __exit__(self, exc_type, exc_val, exc_tb):

        self.close()

 

而後修改handle_query函數以下:

 

def handle_query():

    with Database() as db:

        print 'handle ---', db.query()

 

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

 

上下文管理協議

 

前面初略的說起了上下文,那什麼又是上下文管理器呢?與python黑魔法—迭代器相似,實現了迭代協議的函數/對象即爲迭代器。實現了上下文協議的函數/對象即爲上下文管理器。

 

迭代器協議是實現了__iter__方法。上下文管理協議則是__enter__和__exit__。對於以下代碼結構:

 

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語句:

 

  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爲了更優雅,還專門提供了一個模塊用於實現更函數式的上下文管理器用法。

 

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返回。最後退出上下文環境的時候,在except代碼塊中執行了__exit__方法。固然咱們能夠手動模擬上述代碼的執行的細節。

 

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。看下面一個例子:

 

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的異常。

 

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

 

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。固然,上述的代碼也能夠改寫以下:

 

@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異步上下文管理器中介紹。

 

參見:http://www.cnblogs.com/wumingxiaoyao/p/7132181.html

相關文章
相關標籤/搜索