完全理解關鍵字”with”與上下文管理器

劉志軍發表於2019-02-28

如果你有閱讀原始碼的習慣,可能會看到一些優秀的程式碼經常出現帶有 “with” 關鍵字的語句,它通常用在什麼場景呢?今天就來說說 with 和 上下文管理器。

對於系統資源如檔案、資料庫連線、socket 而言,應用程式開啟這些資源並執行完業務邏輯之後,必須做的一件事就是要關閉(斷開)該資源。

比如 Python 程式開啟一個檔案,往檔案中寫內容,寫完之後,就要關閉該檔案,否則會出現什麼情況呢?極端情況下會出現 “Too many open files” 的錯誤,因為系統允許你開啟的最大檔案數量是有限的。

同樣,對於資料庫,如果連線數過多而沒有及時關閉的話,就可能會出現 “Can not connect to MySQL server Too many connections”,因為資料庫連線是一種非常昂貴的資源,不可能無限制的被建立。

來看看如何正確關閉一個檔案。

普通版:

def m1():
    f = open("output.txt", "w")
    f.write("python之禪")
    f.close()複製程式碼

這樣寫有一個潛在的問題,如果在呼叫 write 的過程中,出現了異常進而導致後續程式碼無法繼續執行,close 方法無法被正常呼叫,因此資源就會一直被該程式佔用者釋放。那麼該如何改進程式碼呢?

進階版:

def m2():
    f = open("output.txt", "w")
    try:
        f.write("python之禪")
    except IOError:
        print("oops error")
    finally:
        f.close()複製程式碼

改良版本的程式是對可能發生異常的程式碼處進行 try 捕獲,使用 try/finally 語句,該語句表示如果在 try 程式碼塊中程式出現了異常,後續程式碼就不再執行,而直接跳轉到 except 程式碼塊。而無論如何,finally 塊的程式碼最終都會被執行。因此,只要把 close 放在 finally 程式碼中,檔案就一定會關閉。

高階版:

def m3():
    with open("output.txt", "r") as f:
        f.write("Python之禪")複製程式碼

一種更加簡潔、優雅的方式就是用 with 關鍵字。open 方法的返回值賦值給變數 f,當離開 with 程式碼塊的時候,系統會自動呼叫 f.close() 方法, with 的作用和使用 try/finally 語句是一樣的。那麼它的實現原理是什麼?在講 with 的原理前要涉及到另外一個概念,就是上下文管理器(Context Manager)。

上下文管理器

任何實現了 __enter__()__exit__() 方法的物件都可稱之為上下文管理器,上下文管理器物件可以使用 with 關鍵字。顯然,檔案(file)物件也實現了上下文管理器。

那麼檔案物件是如何實現這兩個方法的呢?我們可以模擬實現一個自己的檔案類,讓該類實現 __enter__()__exit__() 方法。

class File():

    def __init__(self, filename, mode):
        self.filename = filename
        self.mode = mode

    def __enter__(self):
        print("entering")
        self.f = open(self.filename, self.mode)
        return self.f

    def __exit__(self, *args):
        print("will exit")
        self.f.close()複製程式碼

__enter__() 方法返回資源物件,這裡就是你將要開啟的那個檔案物件,__exit__() 方法處理一些清除工作。

因為 File 類實現了上下文管理器,現在就可以使用 with 語句了。

with File(`out.txt`, `w`) as f:
    print("writing")
    f.write(`hello, python`)複製程式碼

這樣,你就無需顯示地呼叫 close 方法了,由系統自動去呼叫,哪怕中間遇到異常 close 方法也會被呼叫。

contextlib

Python 還提供了一個 contextmanager 的裝飾器,更進一步簡化了上下文管理器的實現方式。通過 yield 將函式分割成兩部分,yield 之前的語句在 __enter__ 方法中執行,yield 之後的語句在 __exit__ 方法中執行。緊跟在 yield 後面的值是函式的返回值。

from contextlib import contextmanager

@contextmanager
def my_open(path, mode):
    f = open(path, mode)
    yield f
    f.close()複製程式碼

呼叫

with my_open(`out.txt`, `w`) as f:
    f.write("hello , the simplest context manager")複製程式碼

總結

Python 提供了 with 語法用於簡化資源操作的後續清除操作,是 try/finally 的替代方法,實現原理建立在上下文管理器之上。此外,Python 還提供了一個 contextmanager 裝飾器,更進一步簡化上下管理器的實現方式。

同步發表部落格:foofish.net/with-and-co…
公眾號:Python之禪 (id:VTtalk),分享 Python 等技術乾貨

完全理解關鍵字”with”與上下文管理器
Python之禪

相關文章