《Python有什麼好學的》之上下文管理器

煎魚不可能有BUG發表於2019-02-16

“Python有什麼好學的”這句話可不是反問句,而是問句哦。

主要是煎魚覺得太多的人覺得Python的語法較為簡單,寫出來的程式碼只要符合邏輯,不需要太多的學習即可,即可從一門其他語言跳來用Python寫(當然這樣是好事,誰都希望入門簡單)。

於是我便記錄一下,如果要學Python的話,到底有什麼好學的。記錄一下Python有什麼值得學的,對比其他語言有什麼特別的地方,有什麼樣的程式碼寫出來更Pythonic。一路回味,一路學習。

引上下文管理器

太極生兩儀,兩儀為陰陽。

道有陰陽,月有陰晴,人有生死,門有開關。

你看這個門,它能開能關,就像這個物件,它能建立能釋放。(扯遠了

程式設計這行,幾十年來都繞不開記憶體洩露這個問題。記憶體洩露的根本原因,就是把某個物件建立了,但是卻沒有去釋放它。直到程式結束前那一刻,這個未被釋放的物件還一直佔著記憶體,即使程式已經不用這個物件了。洩露的量少的話還好,量大的話就直接打滿記憶體,然後程式就被kill了。

聰明的程式設計師經過了這十幾年的努力,創造出很多高階程式語言,這些程式語言已經不再需要讓程式設計師過度關注記憶體的問題了。但是在程式設計時,一些常見的物件釋放、流關閉還是要程式設計師顯式地寫出來。

最常見的就是檔案操作了。

常見的檔案操作方式

原始的Python檔案操作方式,很簡單,也很common(也很java):

def read_file_1():
    f = open(`file_demo.py`, `r`)
    try:
        print(f.read())
    except Exception as e:
        pass
    f.close()

就是這麼簡簡單單的,先open然後讀寫再close,中間讀寫加個異常處理。

其中close就是釋放資源了,在這裡如果不close,可能:

  1. 資源不釋放,直到不可控的垃圾回收來了,甚至直到程式結束
  2. 中間對檔案修改時,修改的資訊還沒來得及寫入檔案
  3. 整個程式碼顯得不規範

因此寫上close函式理論上已經必須的了,可是xxx.close()這樣寫上去,在邏輯複雜的時候讓人容易遺漏,同時也顯得不雅觀。

這時,各種語言生態有各種解決方案。

像Java,就直接jvm+依賴注入,直接把物件的生命週期管理接管了,只留下物件的使用功能給程式設計師;像golang,defer一下就好。而python最常用的則是with,即上下文管理器

使用上下文管理器

用with之後的檔案讀寫會變成:

def read_file_2():
    with open(`file_demo.py`, `r`) as f:
        print(f.read())

我們看到用了with之後,程式碼沒有了open建立,也沒有了close釋放。而且也沒有了異常處理,這樣子我們一看到程式碼,難免會懷疑它的健壯性。

為了更好地理解上下文管理器,我們先實現試試。

實現上下文管理器

我們先感性地對with進行猜測。

從呼叫with的形式上看,with像是一個函式,包裹住了open和close:

# 大概意思而已 with = open + do + close
def with():
    open(xxxx)
    doSomething(xxxx)
    close(xxxx)

而Python的庫中已有的方案(contextmanager)也和上面的虛擬碼具有一定的相似性:

from contextlib import contextmanager

@contextmanager
def c(s):
    print(s + `start`)
    yield s
    print(s + `end`)

“列印start”相當於open,而“列印end”相當於close,yield語法和修飾器(@)不熟悉的同學可以複習一下這些文章:生成器修飾器

然後我們呼叫這個上下文管理器試試,注意煎魚還給上下文管理器加了引數s,輸出的時候會帶上:

def test_context():
    with c(`123`) as cc:
        print(`in with`)
        print(type(cc))

if __name__ == `__main__`:
    test_context()

我們看到,start和end前都有實參s=123。

現實一個上下文管理器就是這麼簡單。

異常處理

但是我們必須要注重異常處理,假如上面的上下文管理器中拋異常了怎麼辦呢:

def test_context():
    with c(`123`) as cc:
        print(`in with`)
        print(type(cc))
        raise Exception

結果:

顯然,這樣弱雞的異常處理,煎魚時忍不了的。而且最重要的是,後面的close釋放居然沒有執行!

我們可以在實現上下管理器時,接入異常處理:

@contextmanager
def c():
    print(`start`)
    try:
        yield
    finally:
        print(`end`)
        
def test_except():
    try:
        with c() as cc:
            print(`in with`)
            raise Exception

    except:
        print(`catch except`)

呼叫test_except函式輸出:

我們在上下文管理器的實現中加入了try-finally,保證出現異常的時候,上下文管理器也能執行close。同時在呼叫with前也加入try結構,保證整個函式的正常執行。

然而,加入了這些東西之後,整個函式變得複雜又難看。

因此,煎魚覺得,想要程式碼好看,抽象的邏輯需要再次昇華,即從函式的層面升為物件(類)的層面。

實現上下文管理器類

其實用類實現上下文管理器,從邏輯理解上簡單了很多,而且不需要引入那一個庫:

class ContextClass(object):
    def __init__(self, s):
        self.s = s

    def __enter__(self):
        print(self.s + `call enter`)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(self.s + `call exit`)

    def test(self):
        print(self.s + `call test`)

從程式碼的字面意思上,我們就能感受得出來,__enter__即為我們理解的open函式,__exit__就是close函式。

接下來,我們呼叫一下這個上下文管理器:

def test_context():
    with ContextClass(`123`) as c:
        print(`in with`)
        c.test()
        print(type(c))
        print(isinstance(c, ContextClass))

    print(``)
    c = ContextClass(`123`)
    print(type(c))
    print(isinstance(c, ContextClass))

if __name__ == `__main__`:
    test_context()

輸出結果:

功能上和直接用修飾器一致,只是在實現的過程中,邏輯更清晰了。

異常處理

回到我們原來的話題:異常處理。

直接用修飾器實現的上下文管理器處理異常時可以說是很難看了,那麼我們的類選手表現又如何呢?

為了方便比較,煎魚把未進行異常處理的和已進行異常處理的一起寫出來,然後煎魚呼叫一個不存在的方法來拋異常:

class ContextClass(object):
    def __init__(self, s):
        self.s = s

    def __enter__(self):
        print(self.s + `call enter`)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(self.s + `call exit`)

class ContextExceptionClass(object):
    def __init__(self, s):
        self.s = s

    def __enter__(self):
        print(self.s + `call enter`)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(self.s + `call exit`)
        return True
        
def test_context():
    with ContextExceptionClass(`123`) as c:
        print(`in with`)
        t = c.test()
        print(type(t))

    # with ContextClass(`456`) as c:
        # print(`in with`)
        # t = c.test()
        # print(type(t))

if __name__ == `__main__`:
    test_context()

輸出不一樣的結果:

結果發現,看了半天,兩個類只有最後一句不一樣:異常處理的類中__exit__函式多一句返回,而且還是return了True。

而且這兩個類都完成了open和close兩部,即使後者拋異常了。

而在__exit__中加return True的意思就是不把異常丟擲。

如果想要詳細地處理異常,而不是像上面治標不治本的隱藏異常,則需要在__exit__函式中處理異常即可,因為該函式中有著異常的資訊。

不信?稍微再改改:

class ContextExceptionClass(object):
    def __init__(self, s):
        self.s = s

    def __enter__(self):
        print(self.s + `call enter`)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(self.s + `call exit`)
        print(str(exc_type) + ` ` + str(exc_val) + ` ` + str(exc_tb))
        return True

輸出與預期異常資訊一致:

先這樣吧

若有錯誤之處請指出,更多地請關注造殼

相關文章