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

「Python有什麼好學的」這句話可不是反問句,而是問句哦。java

主要是煎魚以爲太多的人以爲Python的語法較爲簡單,寫出來的代碼只要符合邏輯,不須要太多的學習便可,便可從一門其餘語言跳來用Python寫(固然這樣是好事,誰都但願入門簡單)。python

因而我便記錄一下,若是要學Python的話,到底有什麼好學的。記錄一下Python有什麼值得學的,對比其餘語言有什麼特別的地方,有什麼樣的代碼寫出來更Pythonic。一路回味,一路學習。程序員

引上下文管理器

太極生兩儀,兩儀爲陰陽。golang

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

你看這個門,它能開能關,就像這個對象,它能建立能釋放。(扯遠了編程

編程這行,幾十年來都繞不開內存泄露這個問題。內存泄露的根本緣由,就是把某個對象建立了,可是卻沒有去釋放它。直到程序結束前那一刻,這個未被釋放的對象還一直佔着內存,即便程序已經不用這個對象了。泄露的量少的話還好,量大的話就直接打滿內存,而後程序就被kill了。jvm

聰明的程序員通過了這十幾年的努力,創造出不少高級編程語言,這些編程語言已經再也不須要讓程序員過分關注內存的問題了。可是在編程時,一些常見的對象釋放、流關閉仍是要程序員顯式地寫出來。編程語言

最多見的就是文件操做了。函數

常見的文件操做方式

原始的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

輸出與預期異常信息一致:

先這樣吧

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

相關文章
相關標籤/搜索