
深入解析Python中的上下文管理器
Python中把進(jìn)入代碼塊前調(diào)用__enter__ 方法并在離開代碼塊后調(diào)用__exit__方法的對(duì)象作為上下文管理器,本文中我們就來深入解析Python中的上下文管理器,來看看上下文管理器的作用及用法:
1. 上下文管理器是什么?
舉個(gè)例子,你在寫Python代碼的時(shí)候經(jīng)常將一系列操作放在一個(gè)語句塊中:
(1)當(dāng)某條件為真 – 執(zhí)行這個(gè)語句塊
(2)當(dāng)某條件為真 – 循環(huán)執(zhí)行這個(gè)語句塊
有時(shí)候我們需要在當(dāng)程序在語句塊中運(yùn)行時(shí)保持某種狀態(tài),并且在離開語句塊后結(jié)束這種狀態(tài)。
所以,事實(shí)上上下文管理器的任務(wù)是 – 代碼塊執(zhí)行前準(zhǔn)備,代碼塊執(zhí)行后收拾。
上下文管理器是在Python2.5加入的功能,它能夠讓你的代碼可讀性更強(qiáng)并且錯(cuò)誤更少。接下來,讓我們來看看該如何使用。
2. 如何使用上下文管理器?
看代碼是最好的學(xué)習(xí)方式,來看看我們通常是如何打開一個(gè)文件并寫入”Hello World”?
filename = 'my_file.txt'
mode = 'w' # Mode that allows to write to the file
writer = open(filename, mode)
writer.write('Hello ')
writer.write('World')
writer.close()
1-2行,我們指明文件名以及打開方式(寫入)。
第3行,打開文件,4-5行寫入“Hello world”,第6行關(guān)閉文件。
這樣不就行了,為什么還需要上下文管理器?但是我們忽略了一個(gè)很小但是很重要的細(xì)節(jié):如果我們沒有機(jī)會(huì)到達(dá)第6行關(guān)閉文件,那會(huì)怎樣?
舉個(gè)例子,磁盤已滿,因此我們?cè)诘?行嘗試寫入文件時(shí)就會(huì)拋出異常,而第6行則根本沒有機(jī)會(huì)執(zhí)行。
當(dāng)然,我們可以使用try-finally語句塊來進(jìn)行包裝:
writer = open(filename, mode)
try:
writer.write('Hello ')
writer.write('World')
finally:
writer.close()
finally語句塊中的代碼無論try語句塊中發(fā)生了什么都會(huì)執(zhí)行。因此可以保證文件一定會(huì)關(guān)閉。這么做有什么問題么?當(dāng)然沒有,但當(dāng)我們進(jìn)行一些比寫入“Hello world”更復(fù)雜的事情時(shí),try-finally語句就會(huì)變得丑陋無比。例如我們要打開兩個(gè)文件,一個(gè)讀一個(gè)寫,兩個(gè)文件之間進(jìn)行拷貝操作,那么通過with語句能夠保證兩者能夠同時(shí)被關(guān)閉。
OK,讓我們把事情分解一下:
(1)首先,創(chuàng)建一個(gè)名為“writer”的文件變量。
(2)然后,對(duì)writer執(zhí)行一些操作。
(3)最后,關(guān)閉writer。
這樣是不是優(yōu)雅多了?
with open(filename, mode) as writer:
writer.write('Hello ')
writer.write('World')
讓我們深入一點(diǎn),“with”是一個(gè)新關(guān)鍵詞,并且總是伴隨著上下文管理器出現(xiàn)?!皁pen(filename, mode)”曾經(jīng)在之前的代碼中出現(xiàn)?!癮s”是另一個(gè)關(guān)鍵詞,它指代了從“open”函數(shù)返回的內(nèi)容,并且把它賦值給了一個(gè)新的變量?!皐riter”是一個(gè)新的變量名。
2-3行,縮進(jìn)開啟一個(gè)新的代碼塊。在這個(gè)代碼塊中,我們能夠?qū)riter做任意操作。這樣我們就使用了“open”上下文管理器,它保證我們的代碼既優(yōu)雅又安全。它出色的完成了try-finally的任務(wù)。
open函數(shù)既能夠當(dāng)做一個(gè)簡單的函數(shù)使用,又能夠作為上下文管理器。這是因?yàn)閛pen函數(shù)返回了一個(gè)文件類型(file type)變量,而這個(gè)文件類型實(shí)現(xiàn)了我們之前用到的write方法,但是想要作為上下文管理器還必須實(shí)現(xiàn)一些特殊的方法,我會(huì)在接下來的小節(jié)中介紹。
3. 自定義上下文管理器
讓我們來寫一個(gè)“open”上下文管理器。
要實(shí)現(xiàn)上下文管理器,必須實(shí)現(xiàn)兩個(gè)方法 – 一個(gè)負(fù)責(zé)進(jìn)入語句塊的準(zhǔn)備操作,另一個(gè)負(fù)責(zé)離開語句塊的善后操作。同時(shí),我們需要兩個(gè)參數(shù):文件名和打開方式。
Python類包含兩個(gè)特殊的方法,分別名為:__enter__以及__exit__(雙下劃線作為前綴及后綴)。
當(dāng)一個(gè)對(duì)象被用作上下文管理器時(shí):
(1)__enter__ 方法將在進(jìn)入代碼塊前被調(diào)用。
(2)__exit__ 方法則在離開代碼塊之后被調(diào)用(即使在代碼塊中遇到了異常)。
下面是上下文管理器的一個(gè)例子,它分別進(jìn)入和離開代碼塊時(shí)進(jìn)行打印。
class PypixContextManagerDemo:
def __enter__(self):
print 'Entering the block'
def __exit__(self, *unused):
print 'Exiting the block'
with PypixContextManagerDemo():
print 'In the block'
#Output:
#Entering the block
#In the block
#Exiting the block
注意一些東西:
(1)沒有傳遞任何參數(shù)。
(2)在此沒有使用“as”關(guān)鍵詞。
稍后我們將討論__exit__方法的參數(shù)設(shè)置。
我們?nèi)绾谓o一個(gè)類傳遞參數(shù)?其實(shí)在任何類中,都可以使用__init__方法,在此我們將重寫它以接收兩個(gè)必要參數(shù)(filename, mode)。
當(dāng)我們進(jìn)入語句塊時(shí),將會(huì)使用open函數(shù),正如第一個(gè)例子中那樣。而當(dāng)我們離開語句塊時(shí),將關(guān)閉一切在__enter__函數(shù)中打開的東西。
以下是我們的代碼:
class PypixOpen:
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
def __enter__(self):
self.openedFile = open(self.filename, self.mode)
return self.openedFile
def __exit__(self, *unused):
self.openedFile.close()
with PypixOpen(filename, mode) as writer:
writer.write("Hello World from our new Context Manager!")
來看看有哪些變化:
(1)3-5行,通過__init__接收了兩個(gè)參數(shù)。
(2)7-9行,打開文件并返回。
(3)12行,當(dāng)離開語句塊時(shí)關(guān)閉文件。
(4)14-15行,模仿open使用我們自己的上下文管理器。
除此之外,還有一些需要強(qiáng)調(diào)的事情:
4.如何處理異常
我們完全忽視了語句塊內(nèi)部可能出現(xiàn)的問題。
如果語句塊內(nèi)部發(fā)生了異常,__exit__方法將被調(diào)用,而異常將會(huì)被重新拋出(re-raised)。當(dāng)處理文件寫入操作時(shí),大部分時(shí)間你肯定不希望隱藏這些異常,所以這是可以的。而對(duì)于不希望重新拋出的異常,我們可以讓__exit__方法簡單的返回True來忽略語句塊中發(fā)生的所有異常(大部分情況下這都不是明智之舉)。
我們可以在異常發(fā)生時(shí)了解到更多詳細(xì)的信息,完備的__exit__函數(shù)簽名應(yīng)該是這樣的:
def __exit__(self, exc_type, exc_val, exc_tb)
這樣__exit__函數(shù)就能夠拿到關(guān)于異常的所有信息(異常類型,異常值以及異常追蹤信息),這些信息將幫助異常處理操作。在這里我將不會(huì)詳細(xì)討論異常處理該如何寫,以下是一個(gè)示例,只負(fù)責(zé)拋出SyntaxErrors異常。
class RaiseOnlyIfSyntaxError:
def __enter__(self):
pass
def __exit__(self, exc_type, exc_val, exc_tb):
return SyntaxError != exc_type
捕獲異常:
當(dāng)一個(gè)異常在with塊中拋出時(shí),它作為參數(shù)傳遞給__exit__。三個(gè)參數(shù)被使用,和sys.exc_info()返回的相同:類型、值和回溯(traceback)。當(dāng)沒有異常拋出時(shí),三個(gè)參數(shù)都是None。上下文管理器可以通過從__exit__返回一個(gè)真(True)值來“吞下”異常。例外可以輕易忽略,因?yàn)槿绻鸰_exit__不使用return直接結(jié)束,返回None——一個(gè)假(False)值,之后在__exit__結(jié)束后重新拋出。
捕獲異常的能力創(chuàng)造了有意思的可能性。一個(gè)來自單元測試的經(jīng)典例子——我們想確保一些代碼拋出正確種類的異常:
class assert_raises(object):
# based on pytest and unittest.TestCase
def __init__(self, type):
self.type = type
def __enter__(self):
pass
def __exit__(self, type, value, traceback):
if type is None:
raise AssertionError('exception expected')
if issubclass(type, self.type):
return True # swallow the expected exception
raise AssertionError('wrong exception type')
with assert_raises(KeyError):
{}['foo']
5. 談一些關(guān)于上下文庫(contextlib)的內(nèi)容
contextlib是一個(gè)Python模塊,作用是提供更易用的上下文管理器。
(1)contextlib.closing
假設(shè)我們有一個(gè)創(chuàng)建數(shù)據(jù)庫函數(shù),它將返回一個(gè)數(shù)據(jù)庫對(duì)象,并且在使用完之后關(guān)閉相關(guān)資源(數(shù)據(jù)庫連接會(huì)話等)
我們可以像以往那樣處理或是通過上下文管理器:
with contextlib.closing(CreateDatabase()) as database:
database.query()
contextlib.closing方法將在語句塊結(jié)束后調(diào)用數(shù)據(jù)庫的關(guān)閉方法。
(2)contextlib.nested
另一個(gè)很cool的特性能夠有效地幫助我們減少嵌套:
假設(shè)我們有兩個(gè)文件,一個(gè)讀一個(gè)寫,需要進(jìn)行拷貝。
以下是不提倡的:
with open('toReadFile', 'r') as reader:
with open('toWriteFile', 'w') as writer:
writer.writer(reader.read())
可以通過contextlib.nested進(jìn)行簡化:
with contextlib.nested(open('fileToRead.txt', 'r'),
open('fileToWrite.txt', 'w')) as (reader, writer):
writer.write(reader.read())
在Python2.7中這種寫法被一種新語法取代:
with open('fileToRead.txt', 'r') as reader, \
open('fileToWrite.txt', 'w') as writer:
writer.write(reader.read())
contextlib.contextmanager
對(duì)于Python高級(jí)玩家來說,任何能夠被yield關(guān)鍵詞分割成兩部分的函數(shù),都能夠通過裝飾器裝飾的上下文管理器來實(shí)現(xiàn)。任何在yield之前的內(nèi)容都可以看做在代碼塊執(zhí)行前的操作,而任何yield之后的操作都可以放在exit函數(shù)中。
這里我舉一個(gè)線程鎖的例子:
鎖機(jī)制保證兩段代碼在同時(shí)執(zhí)行時(shí)不會(huì)互相干擾。例如我們有兩塊并行執(zhí)行的代碼同時(shí)寫一個(gè)文件,那我們將得到一個(gè)混合兩份輸入的錯(cuò)誤文件。但如果我們能有一個(gè)鎖,任何想要寫文件的代碼都必須首先獲得這個(gè)鎖,那么事情就好辦了。如果你想了解更多關(guān)于并發(fā)編程的內(nèi)容,請(qǐng)參閱相關(guān)文獻(xiàn)。
下面是線程安全寫函數(shù)的例子:
import threading
lock = threading.Lock()
def safeWriteToFile(openedFile, content):
lock.acquire()
openedFile.write(content)
lock.release()
接下來,讓我們用上下文管理器來實(shí)現(xiàn),回想之前關(guān)于yield和contextlib的分析:
@contextlib.contextmanager
def loudLock():
print 'Locking'
lock.acquire()
yield
print 'Releasing'
lock.release()
with loudLock():
print 'Lock is locked: %s' % lock.locked()
print 'Doing something that needs locking'
#Output:
#Locking
#Lock is locked: True
#Doing something that needs locking
#Releasing
特別注意,這不是異常安全(exception safe)的寫法。如果你想保證異常安全,請(qǐng)對(duì)yield使用try語句。幸運(yùn)的是threading。lock已經(jīng)是一個(gè)上下文管理器了,所以我們只需要簡單地:
@contextlib.contextmanager
def loudLock():
print 'Locking'
with lock:
yield
print 'Releasing'
因?yàn)閠hreading.lock在異常發(fā)生時(shí)會(huì)通過__exit__函數(shù)返回False,這將在yield被調(diào)用是被重新拋出。這種情況下鎖將被釋放,但對(duì)于“print ‘Releasing'”的調(diào)用則不會(huì)被執(zhí)行,除非我們重寫try-finally。
如果你希望在上下文管理器中使用“as”關(guān)鍵字,那么就用yield返回你需要的值,它將通過as關(guān)鍵字賦值給新的變量。下面我們就仔細(xì)來講一下。
6.使用生成器定義上下文管理器
當(dāng)討論生成器時(shí),據(jù)說我們相比實(shí)現(xiàn)為類的迭代器更傾向于生成器,因?yàn)樗鼈兏绦》奖?,狀態(tài)被局部保存而非實(shí)例和變量中。另一方面,正如雙向通信章節(jié)描述的那樣,生成器和它的調(diào)用者之間的數(shù)據(jù)流可以是雙向的。包括異常,可以直接傳遞給生成器。我們想將上下文管理器實(shí)現(xiàn)為特殊的生成器函數(shù)。事實(shí)上,生成器協(xié)議被設(shè)計(jì)成支持這個(gè)用例。
@contextlib.contextmanager
def some_generator(<arguments>):
<setup>
try:
yield <value>
finally:
<cleanup>
contextlib.contextmanager裝飾一個(gè)生成器并轉(zhuǎn)換為上下文管理器。生成器必須遵循一些被包裝(wrapper)函數(shù)強(qiáng)制執(zhí)行的法則——最重要的是它至少yield一次。yield之前的部分從__enter__執(zhí)行,上下文管理器中的代碼塊當(dāng)生成器停在yield時(shí)執(zhí)行,剩下的在__exit__中執(zhí)行。如果異常被拋出,解釋器通過__exit__的參數(shù)將之傳遞給包裝函數(shù),包裝函數(shù)于是在yield語句處拋出異常。通過使用生成器,上下文管理器變得更短小精煉。
讓我們用生成器重寫closing的例子:
@contextlib.contextmanager
def closing(obj):
try:
yield obj
finally:
obj.close()
再把a(bǔ)ssert_raises改寫成生成器:
@contextlib.contextmanager
def assert_raises(type):
try:
yield
except type:
return
except Exception as value:
raise AssertionError('wrong exception type')
else:
raise AssertionError('exception expected')
這里我們用裝飾器將生成函數(shù)轉(zhuǎn)化為上下文管理器!
數(shù)據(jù)分析咨詢請(qǐng)掃描二維碼
若不方便掃碼,搜微信號(hào):CDAshujufenxi
LSTM 模型輸入長度選擇技巧:提升序列建模效能的關(guān)鍵? 在循環(huán)神經(jīng)網(wǎng)絡(luò)(RNN)家族中,長短期記憶網(wǎng)絡(luò)(LSTM)憑借其解決長序列 ...
2025-07-11CDA 數(shù)據(jù)分析師報(bào)考條件詳解與準(zhǔn)備指南? ? 在數(shù)據(jù)驅(qū)動(dòng)決策的時(shí)代浪潮下,CDA 數(shù)據(jù)分析師認(rèn)證愈發(fā)受到矚目,成為眾多有志投身數(shù) ...
2025-07-11數(shù)據(jù)透視表中兩列相乘合計(jì)的實(shí)用指南? 在數(shù)據(jù)分析的日常工作中,數(shù)據(jù)透視表憑借其強(qiáng)大的數(shù)據(jù)匯總和分析功能,成為了 Excel 用戶 ...
2025-07-11尊敬的考生: 您好! 我們誠摯通知您,CDA Level I和 Level II考試大綱將于 2025年7月25日 實(shí)施重大更新。 此次更新旨在確保認(rèn) ...
2025-07-10BI 大數(shù)據(jù)分析師:連接數(shù)據(jù)與業(yè)務(wù)的價(jià)值轉(zhuǎn)化者? ? 在大數(shù)據(jù)與商業(yè)智能(Business Intelligence,簡稱 BI)深度融合的時(shí)代,BI ...
2025-07-10SQL 在預(yù)測分析中的應(yīng)用:從數(shù)據(jù)查詢到趨勢預(yù)判? ? 在數(shù)據(jù)驅(qū)動(dòng)決策的時(shí)代,預(yù)測分析作為挖掘數(shù)據(jù)潛在價(jià)值的核心手段,正被廣泛 ...
2025-07-10數(shù)據(jù)查詢結(jié)束后:分析師的收尾工作與價(jià)值深化? ? 在數(shù)據(jù)分析的全流程中,“query end”(查詢結(jié)束)并非工作的終點(diǎn),而是將數(shù) ...
2025-07-10CDA 數(shù)據(jù)分析師考試:從報(bào)考到取證的全攻略? 在數(shù)字經(jīng)濟(jì)蓬勃發(fā)展的今天,數(shù)據(jù)分析師已成為各行業(yè)爭搶的核心人才,而 CDA(Certi ...
2025-07-09【CDA干貨】單樣本趨勢性檢驗(yàn):捕捉數(shù)據(jù)背后的時(shí)間軌跡? 在數(shù)據(jù)分析的版圖中,單樣本趨勢性檢驗(yàn)如同一位耐心的偵探,專注于從單 ...
2025-07-09year_month數(shù)據(jù)類型:時(shí)間維度的精準(zhǔn)切片? ? 在數(shù)據(jù)的世界里,時(shí)間是最不可或缺的維度之一,而year_month數(shù)據(jù)類型就像一把精準(zhǔn) ...
2025-07-09CDA 備考干貨:Python 在數(shù)據(jù)分析中的核心應(yīng)用與實(shí)戰(zhàn)技巧? ? 在 CDA 數(shù)據(jù)分析師認(rèn)證考試中,Python 作為數(shù)據(jù)處理與分析的核心 ...
2025-07-08SPSS 中的 Mann-Kendall 檢驗(yàn):數(shù)據(jù)趨勢與突變分析的有力工具? ? ? 在數(shù)據(jù)分析的廣袤領(lǐng)域中,準(zhǔn)確捕捉數(shù)據(jù)的趨勢變化以及識(shí)別 ...
2025-07-08備戰(zhàn) CDA 數(shù)據(jù)分析師考試:需要多久?如何規(guī)劃? CDA(Certified Data Analyst)數(shù)據(jù)分析師認(rèn)證作為國內(nèi)權(quán)威的數(shù)據(jù)分析能力認(rèn)證 ...
2025-07-08LSTM 輸出不確定的成因、影響與應(yīng)對(duì)策略? 長短期記憶網(wǎng)絡(luò)(LSTM)作為循環(huán)神經(jīng)網(wǎng)絡(luò)(RNN)的一種變體,憑借獨(dú)特的門控機(jī)制,在 ...
2025-07-07統(tǒng)計(jì)學(xué)方法在市場調(diào)研數(shù)據(jù)中的深度應(yīng)用? 市場調(diào)研是企業(yè)洞察市場動(dòng)態(tài)、了解消費(fèi)者需求的重要途徑,而統(tǒng)計(jì)學(xué)方法則是市場調(diào)研數(shù) ...
2025-07-07CDA數(shù)據(jù)分析師證書考試全攻略? 在數(shù)字化浪潮席卷全球的當(dāng)下,數(shù)據(jù)已成為企業(yè)決策、行業(yè)發(fā)展的核心驅(qū)動(dòng)力,數(shù)據(jù)分析師也因此成為 ...
2025-07-07剖析 CDA 數(shù)據(jù)分析師考試題型:解鎖高效備考與答題策略? CDA(Certified Data Analyst)數(shù)據(jù)分析師考試作為衡量數(shù)據(jù)專業(yè)能力的 ...
2025-07-04SQL Server 字符串截取轉(zhuǎn)日期:解鎖數(shù)據(jù)處理的關(guān)鍵技能? 在數(shù)據(jù)處理與分析工作中,數(shù)據(jù)格式的規(guī)范性是保證后續(xù)分析準(zhǔn)確性的基礎(chǔ) ...
2025-07-04CDA 數(shù)據(jù)分析師視角:從數(shù)據(jù)迷霧中探尋商業(yè)真相? 在數(shù)字化浪潮席卷全球的今天,數(shù)據(jù)已成為企業(yè)決策的核心驅(qū)動(dòng)力,CDA(Certifie ...
2025-07-04CDA 數(shù)據(jù)分析師:開啟數(shù)據(jù)職業(yè)發(fā)展新征程? ? 在數(shù)據(jù)成為核心生產(chǎn)要素的今天,數(shù)據(jù)分析師的職業(yè)價(jià)值愈發(fā)凸顯。CDA(Certified D ...
2025-07-03