python 類支援with調用

  • 2019 年 12 月 13 日
  • 筆記

enter exit

為了讓一個對象兼容 with 語句,你需要實現 __enter__()__exit__() 方法。 例如,考慮如下的一個類,它能為我們創建一個網路連接:

<pre style="box-sizing: border-box; font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", Courier, monospace; font-size: 12px; white-space: pre; margin: 0px; padding: 12px; display: block; overflow: auto; line-height: 1.4;">from socket import socket, AF_INET, SOCK_STREAM

class LazyConnection: def init(self, address, family=AF_INET, type=SOCK_STREAM): self.address = address self.family = family self.type = type self.sock = None

def __enter__(self):      if self.sock is not None:          raise RuntimeError('Already connected')      self.sock = socket(self.family, self.type)      self.sock.connect(self.address)      return self.sock    def __exit__(self, exc_ty, exc_val, tb):      self.sock.close()      self.sock = None

</pre>

這個類的關鍵特點在於它表示了一個網路連接,但是初始化的時候並不會做任何事情(比如它並沒有建立一個連接)。 連接的建立和關閉是使用 with 語句自動完成的,例如:

<pre style="box-sizing: border-box; font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", Courier, monospace; font-size: 12px; white-space: pre; margin: 0px; padding: 12px; display: block; overflow: auto; line-height: 1.4;">from functools import partial

conn = LazyConnection(('www.python.org', 80))

Connection closed

with conn as s: # conn.enter() executes: connection open s.send(b'GET /index.html HTTP/1.0rn') s.send(b'Host: www.python.orgrn') s.send(b'rn') resp = b''.join(iter(partial(s.recv, 8192), b'')) # conn.exit() executes: connection closed </pre>

討論

編寫上下文管理器的主要原理是你的程式碼會放到 with 語句塊中執行。 當出現 with 語句的時候,對象的 __enter__() 方法被觸發, 它返回的值(如果有的話)會被賦值給 as 聲明的變數。然後,with 語句塊裡面的程式碼開始執行。 最後,__exit__() 方法被觸發進行清理工作。

不管 with 程式碼塊中發生什麼,上面的控制流都會執行完,就算程式碼塊中發生了異常也是一樣的。 事實上,__exit__() 方法的第三個參數包含了異常類型、異常值和追溯資訊(如果有的話)。 __exit__() 方法能自己決定怎樣利用這個異常資訊,或者忽略它並返回一個None值。 如果 __exit__() 返回 True ,那麼異常會被清空,就好像什麼都沒發生一樣, with 語句後面的程式繼續在正常執行。

還有一個細節問題就是 LazyConnection 類是否允許多個 with 語句來嵌套使用連接。 很顯然,上面的定義中一次只能允許一個socket連接,如果正在使用一個socket的時候又重複使用 with 語句, 就會產生一個異常了。不過你可以像下面這樣修改下上面的實現來解決這個問題:

<pre style="box-sizing: border-box; font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", Courier, monospace; font-size: 12px; white-space: pre; margin: 0px; padding: 12px; display: block; overflow: auto; line-height: 1.4;">from socket import socket, AF_INET, SOCK_STREAM

class LazyConnection: def init(self, address, family=AF_INET, type=SOCK_STREAM): self.address = address self.family = family self.type = type self.connections = []

def __enter__(self):      sock = socket(self.family, self.type)      sock.connect(self.address)      self.connections.append(sock)      return sock    def __exit__(self, exc_ty, exc_val, tb):      self.connections.pop().close()

Example use

from functools import partial

conn = LazyConnection(('www.python.org', 80)) with conn as s1: pass with conn as s2: pass # s1 and s2 are independent sockets </pre>

在第二個版本中,LazyConnection 類可以被看做是某個連接工廠。在內部,一個列表被用來構造一個棧。 每次 __enter__() 方法執行的時候,它複製創建一個新的連接並將其加入到棧裡面。 __exit__() 方法簡單的從棧中彈出最後一個連接並關閉它。 這裡稍微有點難理解,不過它能允許嵌套使用 with 語句創建多個連接,就如上面演示的那樣。

在需要管理一些資源比如文件、網路連接和鎖的編程環境中,使用上下文管理器是很普遍的。 這些資源的一個主要特徵是它們必須被手動的關閉或釋放來確保程式的正確運行。 例如,如果你請求了一個鎖,那麼你必須確保之後釋放了它,否則就可能產生死鎖。 通過實現 __enter__()__exit__() 方法並使用 with 語句可以很容易的避免這些問題, 因為 __exit__() 方法可以讓你無需擔心這些了。