管理外部資源的背景

  • 在程式設計中會面臨的一個常見問題是如何正確管理外部資源,例如檔案、鎖和網路連線
  • 有時,程式會永遠保留這些資源,即使不再需要它們,這種現象稱為記憶體洩漏
  • 因為每次建立和開啟給定資源的新例項而不關閉現有資源時,可用記憶體都會減少

如何正確管理資源

  • 正確管理資源通常是一個棘手的問題
  • 它需要一個設定階段和一個清理階段
  • 後一個階段需要執行一些清理操作,例如關閉檔案、釋放鎖或關閉網路連線
  • 如果忘記執行這些清理操作,那麼應用程式將使資源保持活動狀態,這可能會損害寶貴的系統資源,例如記憶體和網路頻寬

資料庫連線數問題

  • 最常見的資料庫連線數問題
  • 使用資料庫時,可能會出現程式不斷建立新連線而不釋放或重用它們
  • 在這種情況下,資料庫後端可以停止接受新連線
  • 這可能需要管理員登入並手動終止那些陳舊的連線以使資料庫再次可用

寫入檔案問題

  • 將文字寫入檔案通常是一種緩衝操作
  • 這意味著對檔案呼叫 .write() 不會立即導致將文字寫入物理檔案,而是寫入臨時緩衝區
  • 有時,當緩衝區未滿而開發人員忘記呼叫 .close() 時,部分資料可能會永遠丟失

with 的作用

常規說法

  • with 語句適用於對資源進行訪問的場合,確保不管使用過程中是否發生異常都會執行必要的“清理”操作,釋放資源
  • 比如檔案使用後自動關閉/執行緒中鎖的自動獲取和釋放等。

官方解釋

  • 僅適用於執行上下文管理器定義的方法的程式碼塊
  • 允許對普通的 try...except...finally  使用模式進行封裝以方便地重用

一句話總結

使用 with as 語句操作上下文管理器(context manager),它能夠幫助我們自動分配並且釋放資源

什麼是上下文管理器

詳細教程

with as 的基本語法

with 表示式 [as target]:
程式碼塊

執行順序

  1. 呼叫表示式以獲取上下文管理器
  2. 儲存上下文管理器的 .__enter__() 和 .__exit__() 方法供以後使用
  3. 在上下文管理器上呼叫 .__enter__() 並將其返回值繫結到 target(如果有的話)
  4. 執行 with 程式碼塊
  5. 當 with 程式碼塊完成時,在上下文管理器上呼叫 .__exit__()

訪問檔案的程式碼演進

最基礎的寫法

# 1、開啟檔案
file = open("1.txt") # 2、讀取檔案
data = file.read() # 3、手動關閉檔案
file.close()  

存在的問題

在第二步假設檔案讀取的時候發生異常,沒有做任何處理,就不會執行第三步,導致程式可能會洩露檔案描述符

使用 try...except...finally 優化

try:
# 開啟檔案、讀取檔案
f = open('xxx')
data = f.read()
except Exception as e:
# 捕獲異常
pass
finally:
# 關閉檔案
f.close()
  • 無論是否丟擲異常,最後還是會關閉檔案,解決上面提到的問題
  • 但新的問題在於,程式碼比較冗餘,而且要手動關閉檔案

使用 with 優化

with open("1.txt") as file:
data = file.read()
  • 作用和 try 寫法一樣
  • 優勢:程式碼簡潔,自動關閉檔案,釋放資源
  • with 程式碼塊執行完後,會自動呼叫檔案物件的 .close() 方法

支援多個上下文管理器

with open("input.txt") as in_file, open("output.txt", "w") as out_file:
# 從 input.txt 讀取內容
# 轉換內容
# 將轉換後的內容寫入output.txt
pass

等價寫法

with open("input.txt") as in_file:
with open("output.txt", "w") as out_file:
pass

使用 pathlib.Path.open()

import pathlib

file_path = pathlib.Path("a.txt")
with file_path.open("w") as file:
file.write("Hello, World!")
  • 由於 pathlib 提供了一種優雅、直接和 Pythonic 的方式來操作檔案系統路徑
  • 因此應該考慮在 with 語句中使用 Path.open() 作為 Python 中的最佳實踐

捕獲異常的栗子

無論何時載入外部檔案的程式都應檢查可能存在的問題,例如檔案丟失、讀寫訪問等

import pathlib
import logging file_path = pathlib.Path("a.txt")
try:
with file_path.open("w") as file:
file.write("Hello, World!")
except OSError as error:
logging.error("Writing to file %s failed due to: %s", file_path, error)
  • 在 with as 外層新增 try ... except 用於捕獲異常
  • 如果在執行 with 期間發生 OSError,則使用日誌記錄錯誤資訊

遍歷目錄的栗子

import os

with os.scandir(".") as entries:
for entry in entries:
print(entry.name, "->", entry.stat().st_size, "bytes")
  • scandir() 會返回一個支援上下文管理協議的迭代器
  • .__exit__() 將呼叫 scandir.close() 關閉迭代器並釋放獲取的資源

輸出結果

__init__.py -> 178 bytes
a.txt -> 13 bytes
1_上下文管理器.py -> 2168 bytes

高精度計算