管理外部資源的背景
- 在編程中會面臨的一個常見問題是如何正確管理外部資源,例如文件、鎖和網絡連接
- 有時,程序會永遠保留這些資源,即使不再需要它們,這種現象稱為內存泄漏
- 因為每次創建和打開給定資源的新實例而不關閉現有資源時,可用內存都會減少
如何正確管理資源
- 正確管理資源通常是一個棘手的問題
- 它需要一個設置階段和一個清理階段
- 后一個階段需要執行一些清理操作,例如關閉文件、釋放鎖或關閉網絡連接
- 如果忘記執行這些清理操作,那么應用程序將使資源保持活動狀態,這可能會損害寶貴的系統資源,例如內存和網絡帶寬
數據庫連接數問題
- 最常見的數據庫連接數問題
- 使用數據庫時,可能會出現程序不斷創建新連接而不釋放或重用它們
- 在這種情況下,數據庫后端可以停止接受新連接
- 這可能需要管理員登錄並手動終止那些陳舊的連接以使數據庫再次可用
寫入文件問題
- 將文本寫入文件通常是一種緩沖操作
- 這意味着對文件調用 .write() 不會立即導致將文本寫入物理文件,而是寫入臨時緩沖區
- 有時,當緩沖區未滿而開發人員忘記調用 .close() 時,部分數據可能會永遠丟失
with 的作用
常規說法
- with 語句適用於對資源進行訪問的場合,確保不管使用過程中是否發生異常都會執行必要的“清理”操作,釋放資源
- 比如文件使用后自動關閉/線程中鎖的自動獲取和釋放等。
官方解釋
- 僅適用於執行上下文管理器定義的方法的代碼塊
- 允許對普通的 try...except...finally 使用模式進行封裝以方便地重用
一句話總結
使用 with as 語句操作上下文管理器(context manager),它能夠幫助我們自動分配並且釋放資源
什么是上下文管理器
with as 的基本語法
with 表達式 [as target]:
代碼塊
執行順序
- 調用表達式以獲取上下文管理器
- 存儲上下文管理器的 .__enter__() 和 .__exit__() 方法供以后使用
- 在上下文管理器上調用 .__enter__() 並將其返回值綁定到 target(如果有的話)
- 執行 with 代碼塊
- 當 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
高精度計算
# 高精度計算 from decimal import Decimal, localcontext with localcontext() as ctx: ctx.prec = 42 res = Decimal("1") / Decimal("42") print(res)
輸出結果
0.0238095238095238095238095238095238095238095
擴展閱讀
https://realpython.com/python-with-statement/