同學杂伟,你知道Python的上下文管理器嗎移层?
初學者可能對with語句比較熟悉,但是對于上下文管理器這樣的概念不太清楚赫粥,但是作為一個程序員或者準程序員观话,那么你一定聽說過內(nèi)存泄露吧?內(nèi)存泄露的根本原因在于創(chuàng)建了某個對象越平,卻沒有及時的釋放掉频蛔,直到程序結(jié)束前,這個未被釋放的對象一直占著內(nèi)存秦叛。那這樣有什么問題嗎晦溪?其實量少的話還好,如果量大那么就會直接把內(nèi)存占滿挣跋,導致程序被kill掉三圆,這就是內(nèi)存泄露。那內(nèi)存泄露和上下文管理器有什么關(guān)系呢避咆?接下來我們揭曉一下舟肉。
內(nèi)存泄露和上下文管理器
首先,現(xiàn)在我們使用的很多高級編程語言已經(jīng)不需要讓我們過多的去關(guān)注內(nèi)存的問題了查库,但是在某些情況下還是需要我們編寫程序來關(guān)閉或釋放某些對象路媚。而最常見的就是文件操作。
在任何一門編程語言中樊销,文件的輸入輸出整慎、數(shù)據(jù)庫的連接斷開等,都是很常見的資源管理操作围苫。但資源都是有限的裤园,在寫程序時,我們必須保證這些資源在使用過后得到釋放够吩,不然就容易造成資源泄露,輕者使得系統(tǒng)處理緩慢丈氓,重則會使系統(tǒng)崩潰周循。
比如下面這個例子,我們打開了1千萬個文件万俗,進行寫入操作湾笛。但是沒有及時的關(guān)閉文件,如果運行就會報錯闰歪。
for x in range(10000000):
f = open('test.txt', 'w')
f.write('后廠程序員')
# 沒有寫文件的關(guān)閉操作
OSError: [Errno 23] Too many open files in system: 'test.txt'
這就是一個典型的資源泄露的例子嚎研。
因為程序中同時打開了太多的文件,占據(jù)了太多的資源,造成系統(tǒng)崩潰临扮。
為了解決這個問題论矾,不同的編程語言都引入了不同的機制。
而在 Python 中杆勇,對應(yīng)的解決方式便是上下文管理器(context manager)贪壳。
上下文管理器,能夠幫助你自動分配并且釋放資源蚜退,其中最典型的應(yīng)用便是 with 語句闰靴。
如果我們把上面的代碼改成with語句的形式:
for x in range(10000000):
with open('test.txt', 'w') as f:
f.write('后廠程序員')
這樣我們每次打開文件,操作完成后這個文件便會自動關(guān)閉钻注,這樣相應(yīng)的資源也可以得到釋放蚂且,防止資源泄露。當然with 語句的代碼幅恋,也可以用下面的形式表示:
f = open('test.txt', 'w')
try:
f.write('后廠程序員')
finally:
f.close()
其中 finally 是哪怕在寫入文件時發(fā)生錯誤異常杏死,也可以保證該文件最終被關(guān)閉。不過我一般更傾向于使用 with 語句佳遣。
當然with語句的應(yīng)用不僅于此识埋,比如我想要獲取一個鎖,執(zhí)行相應(yīng)的操作零渐,完成后再釋放窒舟,那么代碼就可以寫成下面這樣:
some_lock = threading.Lock()
with somelock:
...
我們可以從這兩個例子中看到,with 語句的使用诵盼,可以簡化了代碼惠豺,有效避免資源泄露的發(fā)生。
此時同學心里可能在想了风宁,mmp鬧了半天你就是要給我講個with語句敖嗲健?
其實戒财,我們很多同學在學習文件操作的時候都學到了with語句的使用热监,但是很多同學或者很多課程也就僅限停留在with語句上,只知道在文件操作上使用with語句很方便饮寞,但是并不清楚with語句的實現(xiàn)和上下文管理器的原理孝扛。因此接下來我們通過上下文管理器的實現(xiàn)來更好的理解它們。
上下文管理器的實現(xiàn)
首先我們想要實現(xiàn)上下文管理器幽崩,那么我們要先知道上下文管理器協(xié)議苦始。其實上下文管理器的協(xié)議也非常簡單,就是必須在一個類中實現(xiàn)__enter__()
和__exit__()
兩個方法慌申,然后這個類的實例就是一個上下文管理器陌选。
其中,方法__enter__()
返回需要被管理的資源,方法__exit__()
里通常會存在一些釋放咨油、清理資源的操作您炉,比如關(guān)閉文件、關(guān)閉數(shù)據(jù)庫連接等臼勉。
基于類的上下文管理器
# 定一個類
class FileManager:
def __init__(self, name, mode):
print('calling __init__ method')
self.name = name
self.mode = mode
self.file = None
# 在類中實現(xiàn)__enter__邻吭,并完成文件的打開操作
def __enter__(self):
print('calling __enter__ method')
self.file = open(self.name, self.mode)
return self.file
# 在類中實現(xiàn)__exit__,并完成文件的關(guān)閉操作
def __exit__(self, exc_type, exc_val, exc_tb):
print('calling __exit__ method')
if self.file:
self.file.close()
# 使用with語句來執(zhí)行上下文管理器
with FileManager('test.txt', 'w') as f:
print('ready to write to file')
f.write('hello world')
# 當我們用 with 語句宴霸,執(zhí)行這個上下文管理器時:
# 1. 方法`__init__()`被調(diào)用囱晴,程序初始化對象 FileManager,使得文件名(name)是"test.txt"瓢谢,文件模式 (mode) 是'w'畸写;
# 2. 方法`__enter__()`被調(diào)用,文件“test.txt”以寫入的模式被打開氓扛,并且返回 FileManager 對象賦予變量 f枯芬;
# 3. 字符串“hello world”被寫入文件“test.txt”;
# 4. 方法`__exit__()`被調(diào)用采郎,負責關(guān)閉之前打開的文件流千所。
# 最終的輸出結(jié)果:
calling __init__ method
calling __enter__ method
ready to write to file
calling __exit__ meth
另外,__exit__()
方法中的參數(shù)“exc_type, exc_val, exc_tb”蒜埋,分別表示 exception_type淫痰、exception_value 和 traceback。當我們執(zhí)行含有上下文管理器的 with 語句時整份,如果有異常拋出待错,異常的信息就會包含在這三個變量中,傳入方法__exit__()
烈评。
比如像下面這樣:
class Foo:
def __init__(self):
print('__init__ called')
def __enter__(self):
print('__enter__ called')
return self
# 在__exit__方法中捕獲并輸出異常信息
def __exit__(self, exc_type, exc_value, exc_tb):
print('__exit__ called')
if exc_type:
print(f'exc_type: {exc_type}')
print(f'exc_value: {exc_value}')
print(f'exc_traceback: {exc_tb}')
print('exception handled')
return True # 異常處理后必須返回True
# 調(diào)用并手動拋出異常
with Foo() as obj:
raise Exception('exception raised').with_traceback(None)
# 輸出
# __init__ called
# __enter__ called
# __exit__ called
# exc_type: <class 'Exception'>
# exc_value: exception raised
# exc_traceback: <traceback object at 0x1046036c8>
# exception handled
需要注意火俄,如果方法__exit__()
沒有返回 True,異常仍然會被拋出讲冠。因此瓜客,如果異常已經(jīng)被處理了則必須在__exit__()
方法中返回True。
同樣的竿开,數(shù)據(jù)庫的連接操作谱仪,也可以用上下文管理器來表示:
class DBCM:
# 負責對數(shù)據(jù)庫進行初始化,也就是將主機名德迹、接口(這里是 localhost 和 8080)分別賦予變量 hostname 和 port芽卿;
def __init__(self, hostname, port):
self.hostname = hostname
self.port = port
self.connection = None
# 連接數(shù)據(jù)庫揭芍,并且返回對象 DBCM胳搞;
def __enter__(self):
self.connection = DBClient(self.hostname, self.port)
return self
# 負責關(guān)閉數(shù)據(jù)庫的連接
def __exit__(self, exc_type, exc_val, exc_tb):
self.connection.close()
with DBCM('localhost', '8080') as db_client:
....
這樣只要你寫完了 DBCM 這個類,那么在程序每次連接數(shù)據(jù)庫時,我們都只需要簡單地調(diào)用 with 語句即可肌毅,并不需要關(guān)心數(shù)據(jù)庫的關(guān)閉筷转、異常等等,大大提高了開發(fā)的效率悬而。
基于生成器的上下文管理器
Python中的上下文管理器除了基于類呜舒,還可以基于生成器來實現(xiàn)。
使用裝飾器 contextlib.contextmanager笨奠,來自定義基于生成器的上下文管理器袭蝗,用以支持 with 語句。
還是拿前面的類上下文管理器 FileManager 來說般婆,我們也可以用下面形式來表示:
from contextlib import contextmanager
@contextmanager
def file_manager(name, mode):
try:
f = open(name, mode)
yield f
finally:
f.close()
with file_manager('test.txt', 'w') as f:
f.write('hello world')
這段代碼中到腥,函數(shù) file_manager() 是一個生成器,當我們執(zhí)行 with 語句時蔚袍,便會打開文件乡范,并返回文件對象 f;
當 with 語句執(zhí)行完后啤咽,finally block 中的關(guān)閉文件操作便會執(zhí)行晋辆。
注意:基于生成器定義的上下文管理需要使用裝飾器 @contextmanager,不在使用生成器協(xié)議方法宇整。
基于類的上下文管理器和基于生成器的上下文管理器瓶佳,這兩者在功能上是一致的。
這樣小伙伴是不是對python中的with語句有了更深的認識没陡,并且在了解了上下文管理器后涩哟,未來在開發(fā)中也可以自定義上下文管理器來實現(xiàn)資源的上下文管理了。
如果喜歡或者對你有幫助的小伙伴盼玄,歡迎大家關(guān)注我的公眾號:后廠程序員贴彼,并分享、點贊埃儿、在看 三連