標籤:

深刻理解上下文管理器

上下文管理器(context manager)是 Python 編程中的重要概念,並且在 Python 3 中得以增強。要加深對 Python 的理解,我們必須要熟練掌握它。然而現在網上的文章少有對其深刻剖析的,建議大家多看 Python 官網文檔進行理解。以下是我對 Python 3 文檔的解讀。

要說上下文管理器,我覺得還是先從 with 語句開始好。(等下說原因)

我們經常會用到 try ... catch ... finally 語句確保一些系統資源得以正確釋放。如:

try:n f = open(somefile)n for line in f:n print(line)nexcept Exception as e:n print(e)nfinally:n f.close()n

我們經常用到上面的代碼模式,用復用代碼的模式來講,並不夠好。於是 with 語句出現了,通過定義一個上下文管理器來封裝這個代碼塊:

with open(somefile) as f:n for line in f:n print(line)n

顯然,with 語句比 try 語句好太多了。

多上下文管理器

實際上,我們可以同時處理多個上下文管理器:

with A() as a, B() as b:n suiten

所以我們大可不必寫嵌套 with 語句。

上下文管理器類

由於 with 語句利用了上下文管理器,在深入理解 with 語句之前,我們先看看上下文管理器。我們要定義一個上下文管理器其實很簡單,只要一個類實現了__enter__(self)和__exit__(self, exc_type, exc_valye, traceback)就可以了。

__enter__(self) 返回一個對象,可以是當前類的實例,也可以是其他對象。

class SomeThing:n def __enter__(self):n return selfn # ...n

請注意,我們通常會返回類實例,但這真不是必須的。比如我們有下面的類:

class LineLength:n def __init__(self, filepath):n self.__file = open(self.__filepath)nn def print_line(self):n for line in self.__file:n print(len(line), line)nn def __enter__(self):n return self.__filenn def __exit__(self, exc_type, exc_value, traceback):n self.__file.close()n return Truen

我們並沒有在__enter__()中返回 LineLength的實例。實際上,Python 也是這麼做的:

In [1]: f = open(/etc/hosts)nIn [2]: fnOut[2]: <_io.TextIOWrapper name=/etc/hosts mode=r encoding=UTF-8>nIn [3]: f = open(/etc/hosts, br)nIn [4]: fnOut[4]: <_io.BufferedReader name=/etc/hosts>n

執行過程

下面讓我們看看 with 語句具體是如何執行的。

第一步:執行上下文表達式以獲得上下文管理器對象。上下文表達式就是 with 和 as 之間的代碼。

第二步:載入上下文管理器對象的 __exit__()方法,備用。

第三步:執行上下文管理器對象的__enter__()方法。

第四步:將__enter__()方法返回值綁定到 as 後面的 變數中。

第五步:執行 with 內的代碼塊。

第六步:執行上下文管理器的__exit__()方法。

如果在代碼塊中發生了異常,異常被傳入__exit__()中。如果沒有,__exit__()的三個參數會傳入 None, None, None。__exit__()需要明確地返回 True 或 False。並且不能在__exit__()中再次拋出被傳入的異常,這是解釋器的工作,解釋器會根據返回值來確定是否繼續向上層代碼傳遞異常。當返回 True 時,異常不會被向上拋出,當返回 False 時曾會向上拋出。當沒有異常發生傳入__exit__()時,解釋器會忽略返回值。問題思考:如果在__exit__()中發生異常呢?

contextlib模塊

contextlib 模塊提供了幾個類和函數可以便於我們的常規代碼。

AbstractContextManager: 此類在 Python3.6中新增,提供了默認的__enter__()和__exit__()實現。__enter__()返回自身,__exit__()返回 None。

ContextDecorator: 我們可以實現一個上下文管理器,同時可以用作裝飾器。

class AContext(ContextDecorator):nn def __enter__(self):n print(Starting)n return selfnn def __exit__(self, exc_type, exc_value, traceback):n print(Finishing)n return Falsenn# 在 with 中使用nwith AContext():n print(祖國偉大)nn# 用作裝飾器n@AContext()ndef print_sth(sth):n print(sth)nnprint_sth(祖國偉大)nn#在這兩種寫法中,有沒有發現,第二種寫法更好,因為我們減少了一次代碼縮進,可讀性更強n

還有一種好處:當我們已經實現了某個上下文管理器時,只要增加一個繼承類,該上下文管理器立刻編程裝飾器。

from contextlib import ContextDecoratornclass mycontext(ContextBaseClass, ContextDecorator):n def __enter__(self):n return selfnn def __exit__(self, *exc):n return Falsen

contextmanager: 我們要實現上下文管理器,總是要寫一個類。此函數則容許我們通過一個裝飾一個生成器函數得到一個上下文管理器。

import timenfrom contextlib import contextmanagernn@contextmanagerndef tag(name):n print("<%s>" % name)n yieldn time.sleep(3)n print("</%s>" % name)nn>>> with tag("h1"):n... print("foo")n...n<h1>nfoon</h1>n

yield 只能返回一次,返回的對象 被綁定到 as 後的變數,不需要返回時可以直接 yield,不帶返回值。退出時則從 yield 之後執行。由於contextmanager繼承自ContextDecorator,所以被contextmanager裝飾過的生成器也可以用作裝飾器。n

closing: 當某對象擁有 close()方法,但不是上下文管理器對象時,為了避免 try 語句,我們可以這樣寫:

from contextlib import closingnfrom urllib.request import urlopennnwith closing(urlopen(http://www.baidu.com)) as page:n for line in page:n print(line)n

suppress:當希望阻止一些異常拋出時,我們可以用:

from contextlib import suppressnnwith suppress(ImportError):n import hahahann# 不好的寫法ntry:n import hahahanexcept ImportError:n passn

redirect_stdout、redirect_stderr:將標準輸出、標準錯誤輸出到其他地方

import ionnf = io.StringIO()nwith redirect_stdout(f):n help(pow)ns = f.getvalue()n

為了加深理解,請思考以下問題:

  1. 有了 with 語句,我們還要用 try 嗎?
  2. __enter__()或__exit__()拋出異常的話會發生什麼?

推薦閱讀:

TAG:装饰器 | Python |