Python中with的作用和使用解讀
在這里我們來詳細(xì)解釋一下Python中非常重要的 with 語句。
我會從 “為什么需要它” 開始,然后講解 “它是什么以及如何使用”,最后深入到 “它的工作原理” 和 “如何自定義”。
1. 為什么需要with語句?(The Problem)
在編程中,我們經(jīng)常會使用一些需要“獲取”和“釋放”的資源,比如:
- 文件操作:打開文件后,必須記得關(guān)閉它。
- 數(shù)據(jù)庫連接:建立連接后,必須記得關(guān)閉連接。
- 線程鎖:獲取鎖之后,必須記得釋放它。
如果我們忘記釋放這些資源,可能會導(dǎo)致嚴(yán)重的問題,比如:
- 文件句柄耗盡,無法再打開新文件。
- 數(shù)據(jù)庫連接池被占滿,應(yīng)用無法再連接數(shù)據(jù)庫。
- 線程死鎖,程序卡住。
讓我們看一個沒有 with 的文件操作例子:
不安全的寫法:
f = open('my_file.txt', 'w')
f.write('hello world')
# 如果在 write 和 close 之間發(fā)生錯誤,close() 將永遠(yuǎn)不會被執(zhí)行!
f.close()
這個寫法非常危險。如果在 f.write() 時發(fā)生異常(例如磁盤滿了),程序會崩潰,f.close() 就不會被調(diào)用,文件資源就泄露了。
安全的、但繁瑣的寫法 (使用 try...finally):
為了確保資源一定被釋放,我們通常使用 try...finally 結(jié)構(gòu):
f = None # 在 try 外面初始化,確保 finally 中可以訪問
try:
f = open('my_file.txt', 'w')
f.write('hello world')
# ... 其他可能出錯的操作 ...
finally:
if f:
f.close()
這個寫法是安全的,因為無論 try 塊中是否發(fā)生異常,finally 塊中的代碼都保證會被執(zhí)行。但是,它看起來很冗長,代碼結(jié)構(gòu)也不夠優(yōu)雅。
with 語句就是為了解決這個問題而生的,它能讓我們用更簡潔、更安全的方式來管理資源。
2.with語句是什么以及如何使用?(The Solution)
with 語句是一種上下文管理的語法糖(Syntactic Sugar)。它極大地簡化了上面 try...finally 的寫法。
基本語法:
with expression as variable:
# 在這個代碼塊中,資源是可用的
# ... do something with variable ...
# 離開 with 代碼塊后,資源會自動被清理
使用 with 重寫文件操作:
with open('my_file.txt', 'w') as f:
f.write('hello world')
# 在這里可以進行各種文件操作
# 比如 f.read(), f.writelines() 等
# 當(dāng)代碼執(zhí)行離開這個 with 塊時(無論是正常結(jié)束還是發(fā)生異常),
# Python 會自動調(diào)用 f.close(),我們完全不需要操心。
對比一下:
try...finally版本:5-6 行代碼,結(jié)構(gòu)復(fù)雜。with版本:2 行代碼,邏輯清晰,意圖明確(“在處理這個文件的上下文中,做這些事”)。
with 語句的核心優(yōu)勢是:無論 with 塊內(nèi)部發(fā)生什么(即使是異常),它都保證能執(zhí)行資源的“清理”操作。
3.with的工作原理:上下文管理器協(xié)議 (The Magic Behind)
with 語句之所以能自動管理資源,是因為它遵循了上下文管理器協(xié)議(Context Manager Protocol)。
一個對象只要實現(xiàn)了下面這兩個特殊方法,它就是一個上下文管理器:
__enter__(self)
- 何時調(diào)用:當(dāng)進入
with語句塊時,該方法被調(diào)用。 - 作用:負(fù)責(zé)“獲取”資源或進行初始化設(shè)置。
- 返回值:這個方法的返回值會賦給
as后面的變量(如果as存在的話)。如果你不需要as變量,這個方法可以不返回任何東西。
__exit__(self, exc_type, exc_value, traceback)
- 何時調(diào)用:當(dāng)離開
with語句塊時(無論是正常退出還是因為異常退出),該方法被調(diào)用。 - 作用:負(fù)責(zé)“釋放”資源或執(zhí)行清理操作(比如
f.close())。
參數(shù):
exc_type: 異常的類型(如果沒發(fā)生異常,則為None)。exc_value: 異常的值(如果沒發(fā)生異常,則為None)。traceback: 異常的追溯信息(如果沒發(fā)生異常,則為None)。
返回值:
- 如果
__exit__方法返回True,表示它已經(jīng)處理了這個異常,異常會被“吞掉”(suppress),程序不會向外拋出。 - 如果它返回
False或None(默認(rèn)情況),任何發(fā)生的異常都會在__exit__執(zhí)行完畢后被重新拋出。
所以,with open(...) as f: 這段代碼大致等同于下面的偽代碼:
# 1. 創(chuàng)建上下文管理器對象
manager = open('my_file.txt', 'w')
# 2. 調(diào)用 __enter__ 方法,返回值賦給 f
f = manager.__enter__()
# 3. 執(zhí)行 with 塊中的代碼
try:
f.write('hello world')
finally:
# 4. 無論如何,都調(diào)用 __exit__ 方法進行清理
# (這里簡單展示,實際會傳遞異常信息)
manager.__exit__(None, None, None)
4. 如何創(chuàng)建自己的上下文管理器?
了解了原理,我們就可以創(chuàng)建自己的上下文管理器。有兩種主要方式:
方式一:基于類的實現(xiàn)
我們可以寫一個類,并實現(xiàn) __enter__ 和 __exit__ 方法。
示例:一個簡單的計時器
import time
class Timer:
def __init__(self, name):
self.name = name
def __enter__(self):
print(f"計時器 '{self.name}' 開始...")
self.start_time = time.time()
# 這個類本身就是資源,所以返回 self
return self
def __exit__(self, exc_type, exc_value, traceback):
self.end_time = time.time()
duration = self.end_time - self.start_time
print(f"計時器 '{self.name}' 結(jié)束,耗時: {duration:.4f} 秒")
# 如果有異常,這里可以記錄日志
if exc_type:
print(f"在 '{self.name}' 中發(fā)生了異常: {exc_value}")
# 返回 False 或 None,讓異常正常拋出
return False
# 使用自定義的 Timer
with Timer("數(shù)據(jù)處理") as t:
print("正在處理數(shù)據(jù)...")
time.sleep(2)
print("數(shù)據(jù)處理完成。")
print("-" * 20)
with Timer("有問題的操作") as t:
print("準(zhǔn)備執(zhí)行一個會出錯的操作...")
time.sleep(1)
result = 1 / 0 # 這里會產(chǎn)生一個 ZeroDivisionError
print("這行代碼不會被執(zhí)行")
輸出:
計時器 '數(shù)據(jù)處理' 開始...
正在處理數(shù)據(jù)...
數(shù)據(jù)處理完成。
計時器 '數(shù)據(jù)處理' 結(jié)束,耗時: 2.0021 秒
--------------------
計時器 '有問題的操作' 開始...
準(zhǔn)備執(zhí)行一個會出錯的操作...
計時器 '有問題的操作' 結(jié)束,耗時: 1.0011 秒
在 '有問題的操作' 中發(fā)生了異常: division by zero
Traceback (most recent call last):
File "...", line 36, in <module>
result = 1 / 0 # 這里會產(chǎn)生一個 ZeroDivisionError
ZeroDivisionError: division by zero
可以看到,即使發(fā)生了異常,__exit__ 方法仍然被調(diào)用,成功打印了耗時和異常信息。
方式二:基于生成器的實現(xiàn)(使用contextlib模塊)
對于簡單的上下文管理器,每次都寫一個類有點麻煩。
Python 的 contextlib 模塊提供了一個 @contextmanager 裝飾器,可以讓我們用更簡潔的方式實現(xiàn)。
import time
from contextlib import contextmanager
@contextmanager
def timer(name):
print(f"計時器 '{name}' 開始...")
start_time = time.time()
# yield 之前的部分,相當(dāng)于 __enter__
# yield 的值會成為 as 后面的變量(如果沒有 yield 值,則為 None)
try:
yield
finally:
# yield 之后的部分,相當(dāng)于 __exit__
end_time = time.time()
duration = end_time - start_time
print(f"計時器 '{name}' 結(jié)束,耗時: {duration:.4f} 秒")
# 使用方法完全一樣
with timer("數(shù)據(jù)處理_v2"):
print("正在處理數(shù)據(jù)...")
time.sleep(2)
print("數(shù)據(jù)處理完成。")
這種方式更加 Pythonic,代碼也更緊湊。try...yield...finally 結(jié)構(gòu)完美地對應(yīng)了“進入-執(zhí)行-清理”的模式。
總結(jié)
- 用途:
with語句用于自動管理資源,確保資源在使用完畢后(無論是否發(fā)生異常)都能被正確清理。 - 優(yōu)點:代碼更簡潔、更安全、更具可讀性,避免了冗長的
try...finally結(jié)構(gòu)和資源泄露的風(fēng)險。 - 原理:依賴于上下文管理器協(xié)議,即對象需實現(xiàn)
__enter__()和__exit__()兩個方法。 - 自定義:你可以通過編寫類或使用
contextlib.contextmanager裝飾器來創(chuàng)建自己的上下文管理器,封裝任何需要“設(shè)置-清理”邏輯的場景。
在現(xiàn)代 Python 編程中,只要遇到需要獲取和釋放資源的場景,都應(yīng)該優(yōu)先考慮使用 with 語句。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
pyqt6的本地環(huán)境部署(conda和vscode環(huán)境)
本文主要介紹了pyqt6的本地環(huán)境部署(conda和vscode環(huán)境),文中通過示例介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-05-05
pytorch中forwod函數(shù)在父類中的調(diào)用方式解讀
這篇文章主要介紹了pytorch中forwod函數(shù)在父類中的調(diào)用方式解讀,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-02-02
Python matplotlib繪制實時數(shù)據(jù)動畫
Matplotlib作為Python的2D繪圖庫,它以各種硬拷貝格式和跨平臺的交互式環(huán)境生成出版質(zhì)量級別的圖形。本文將利用Matplotlib庫繪制實時數(shù)據(jù)動畫,感興趣的可以了解一下2022-03-03
Python計算標(biāo)準(zhǔn)差之numpy.std和torch.std的區(qū)別
Torch自稱為神經(jīng)網(wǎng)絡(luò)中的numpy,它會將torch產(chǎn)生的tensor放在GPU中加速運算,就像numpy會把array放在CPU中加速運算,下面這篇文章主要給大家介紹了關(guān)于Python?Numpy計算標(biāo)準(zhǔn)差之numpy.std和torch.std區(qū)別的相關(guān)資料,需要的朋友可以參考下2022-08-08
Python實現(xiàn)交通數(shù)據(jù)可視化的示例代碼
本文主要分享了Python交通數(shù)據(jù)分析與可視化的實戰(zhàn)!其中主要是使用TransBigData庫快速高效地處理、分析、挖掘出租車GPS數(shù)據(jù),感興趣的可以了解一下2023-04-04

