Python中with的作用和使用解讀
在這里我們來(lái)詳細(xì)解釋一下Python中非常重要的 with 語(yǔ)句。
我會(huì)從 “為什么需要它” 開(kāi)始,然后講解 “它是什么以及如何使用”,最后深入到 “它的工作原理” 和 “如何自定義”。
1. 為什么需要with語(yǔ)句?(The Problem)
在編程中,我們經(jīng)常會(huì)使用一些需要“獲取”和“釋放”的資源,比如:
- 文件操作:打開(kāi)文件后,必須記得關(guān)閉它。
- 數(shù)據(jù)庫(kù)連接:建立連接后,必須記得關(guān)閉連接。
- 線程鎖:獲取鎖之后,必須記得釋放它。
如果我們忘記釋放這些資源,可能會(huì)導(dǎo)致嚴(yán)重的問(wèn)題,比如:
- 文件句柄耗盡,無(wú)法再打開(kāi)新文件。
- 數(shù)據(jù)庫(kù)連接池被占滿(mǎn),應(yīng)用無(wú)法再連接數(shù)據(jù)庫(kù)。
- 線程死鎖,程序卡住。
讓我們看一個(gè)沒(méi)有 with 的文件操作例子:
不安全的寫(xiě)法:
f = open('my_file.txt', 'w')
f.write('hello world')
# 如果在 write 和 close 之間發(fā)生錯(cuò)誤,close() 將永遠(yuǎn)不會(huì)被執(zhí)行!
f.close()
這個(gè)寫(xiě)法非常危險(xiǎn)。如果在 f.write() 時(shí)發(fā)生異常(例如磁盤(pán)滿(mǎn)了),程序會(huì)崩潰,f.close() 就不會(huì)被調(diào)用,文件資源就泄露了。
安全的、但繁瑣的寫(xiě)法 (使用 try...finally):
為了確保資源一定被釋放,我們通常使用 try...finally 結(jié)構(gòu):
f = None # 在 try 外面初始化,確保 finally 中可以訪問(wèn)
try:
f = open('my_file.txt', 'w')
f.write('hello world')
# ... 其他可能出錯(cuò)的操作 ...
finally:
if f:
f.close()
這個(gè)寫(xiě)法是安全的,因?yàn)闊o(wú)論 try 塊中是否發(fā)生異常,finally 塊中的代碼都保證會(huì)被執(zhí)行。但是,它看起來(lái)很冗長(zhǎng),代碼結(jié)構(gòu)也不夠優(yōu)雅。
with 語(yǔ)句就是為了解決這個(gè)問(wèn)題而生的,它能讓我們用更簡(jiǎn)潔、更安全的方式來(lái)管理資源。
2.with語(yǔ)句是什么以及如何使用?(The Solution)
with 語(yǔ)句是一種上下文管理的語(yǔ)法糖(Syntactic Sugar)。它極大地簡(jiǎn)化了上面 try...finally 的寫(xiě)法。
基本語(yǔ)法:
with expression as variable:
# 在這個(gè)代碼塊中,資源是可用的
# ... do something with variable ...
# 離開(kāi) with 代碼塊后,資源會(huì)自動(dòng)被清理
使用 with 重寫(xiě)文件操作:
with open('my_file.txt', 'w') as f:
f.write('hello world')
# 在這里可以進(jìn)行各種文件操作
# 比如 f.read(), f.writelines() 等
# 當(dāng)代碼執(zhí)行離開(kāi)這個(gè) with 塊時(shí)(無(wú)論是正常結(jié)束還是發(fā)生異常),
# Python 會(huì)自動(dòng)調(diào)用 f.close(),我們完全不需要操心。
對(duì)比一下:
try...finally版本:5-6 行代碼,結(jié)構(gòu)復(fù)雜。with版本:2 行代碼,邏輯清晰,意圖明確(“在處理這個(gè)文件的上下文中,做這些事”)。
with 語(yǔ)句的核心優(yōu)勢(shì)是:無(wú)論 with 塊內(nèi)部發(fā)生什么(即使是異常),它都保證能執(zhí)行資源的“清理”操作。
3.with的工作原理:上下文管理器協(xié)議 (The Magic Behind)
with 語(yǔ)句之所以能自動(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語(yǔ)句塊時(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)離開(kāi)
with語(yǔ)句塊時(shí)(無(wú)論是正常退出還是因?yàn)楫惓M顺觯?,該方法被調(diào)用。 - 作用:負(fù)責(zé)“釋放”資源或執(zhí)行清理操作(比如
f.close())。
參數(shù):
exc_type: 異常的類(lèi)型(如果沒(méi)發(fā)生異常,則為None)。exc_value: 異常的值(如果沒(méi)發(fā)生異常,則為None)。traceback: 異常的追溯信息(如果沒(méi)發(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. 無(wú)論如何,都調(diào)用 __exit__ 方法進(jìn)行清理
# (這里簡(jiǎn)單展示,實(shí)際會(huì)傳遞異常信息)
manager.__exit__(None, None, None)
4. 如何創(chuàng)建自己的上下文管理器?
了解了原理,我們就可以創(chuàng)建自己的上下文管理器。有兩種主要方式:
方式一:基于類(lèi)的實(shí)現(xiàn)
我們可以寫(xiě)一個(gè)類(lèi),并實(shí)現(xiàn) __enter__ 和 __exit__ 方法。
示例:一個(gè)簡(jiǎn)單的計(jì)時(shí)器
import time
class Timer:
def __init__(self, name):
self.name = name
def __enter__(self):
print(f"計(jì)時(shí)器 '{self.name}' 開(kāi)始...")
self.start_time = time.time()
# 這個(gè)類(lèi)本身就是資源,所以返回 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("有問(wèn)題的操作") 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ù)處理' 開(kāi)始...
正在處理數(shù)據(jù)...
數(shù)據(jù)處理完成。
計(jì)時(shí)器 '數(shù)據(jù)處理' 結(jié)束,耗時(shí): 2.0021 秒
--------------------
計(jì)時(shí)器 '有問(wèn)題的操作' 開(kāi)始...
準(zhǔn)備執(zhí)行一個(gè)會(huì)出錯(cuò)的操作...
計(jì)時(shí)器 '有問(wèn)題的操作' 結(jié)束,耗時(shí): 1.0011 秒
在 '有問(wèn)題的操作' 中發(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ì)于簡(jiǎn)單的上下文管理器,每次都寫(xiě)一個(gè)類(lèi)有點(diǎn)麻煩。
Python 的 contextlib 模塊提供了一個(gè) @contextmanager 裝飾器,可以讓我們用更簡(jiǎn)潔的方式實(shí)現(xiàn)。
import time
from contextlib import contextmanager
@contextmanager
def timer(name):
print(f"計(jì)時(shí)器 '{name}' 開(kāi)始...")
start_time = time.time()
# yield 之前的部分,相當(dāng)于 __enter__
# yield 的值會(huì)成為 as 后面的變量(如果沒(méi)有 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語(yǔ)句用于自動(dòng)管理資源,確保資源在使用完畢后(無(wú)論是否發(fā)生異常)都能被正確清理。 - 優(yōu)點(diǎn):代碼更簡(jiǎn)潔、更安全、更具可讀性,避免了冗長(zhǎng)的
try...finally結(jié)構(gòu)和資源泄露的風(fēng)險(xiǎn)。 - 原理:依賴(lài)于上下文管理器協(xié)議,即對(duì)象需實(shí)現(xiàn)
__enter__()和__exit__()兩個(gè)方法。 - 自定義:你可以通過(guò)編寫(xiě)類(lèi)或使用
contextlib.contextmanager裝飾器來(lái)創(chuàng)建自己的上下文管理器,封裝任何需要“設(shè)置-清理”邏輯的場(chǎng)景。
在現(xiàn)代 Python 編程中,只要遇到需要獲取和釋放資源的場(chǎng)景,都應(yīng)該優(yōu)先考慮使用 with 語(yǔ)句。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
關(guān)于matplotlib及相關(guān)cmap參數(shù)的取值方式
這篇文章主要介紹了關(guān)于matplotlib及相關(guān)cmap參數(shù)的取值方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-11-11
從基礎(chǔ)到進(jìn)階詳解Python處理Word文檔的完全指南
在Python中處理Word文檔是一項(xiàng)常見(jiàn)且實(shí)用的任務(wù),本文將介紹如何使用幾個(gè)主流的Python庫(kù)來(lái)創(chuàng)建、修改和處理Word文檔,涵蓋從基礎(chǔ)操作到高級(jí)功能的完整流程2026-01-01
Python 實(shí)現(xiàn)數(shù)據(jù)結(jié)構(gòu)-堆棧和隊(duì)列的操作方法
隊(duì)、棧和鏈表一樣,在數(shù)據(jù)結(jié)構(gòu)中非常基礎(chǔ)一種數(shù)據(jù)結(jié)構(gòu),同樣他們也有各種各樣、五花八門(mén)的變形和實(shí)現(xiàn)方式。這篇文章主要介紹了Python 實(shí)現(xiàn)數(shù)據(jù)結(jié)構(gòu)-堆棧和隊(duì)列的操作方法,需要的朋友可以參考下2019-07-07
完美解決安裝完tensorflow后pip無(wú)法使用的問(wèn)題
今天小編就為大家分享一篇完美解決安裝完tensorflow后pip無(wú)法使用的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-06-06
雙向RNN:bidirectional_dynamic_rnn()函數(shù)的使用詳解
今天小編就為大家分享一篇雙向RNN:bidirectional_dynamic_rnn()函數(shù)的使用詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-01-01
python 實(shí)現(xiàn)圖與圖之間的間距調(diào)整subplots_adjust
這篇文章主要介紹了python 實(shí)現(xiàn)圖與圖之間的間距調(diào)整subplots_adjust,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-05-05
詳解Java中一維、二維數(shù)組在內(nèi)存中的結(jié)構(gòu)
這篇文章主要介紹了Java中一維、二維數(shù)組在內(nèi)存中的結(jié)構(gòu),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-02-02

