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