同學,你知道Python的上下文管理器嗎?

同學杂伟,你知道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)注我的公眾號:后廠程序員贴彼,并分享、點贊埃儿、在看 三連

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末器仗,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子童番,更是在濱河造成了極大的恐慌精钮,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件剃斧,死亡現(xiàn)場離奇詭異轨香,居然都是意外死亡,警方通過查閱死者的電腦和手機幼东,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進店門臂容,熙熙樓的掌柜王于貴愁眉苦臉地迎上來科雳,“玉大人,你說我怎么就攤上這事脓杉≡忝兀” “怎么了?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵球散,是天一觀的道長尿赚。 經(jīng)常有香客問我,道長蕉堰,這世上最難降的妖魔是什么凌净? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮屋讶,結(jié)果婚禮上泻蚊,老公的妹妹穿的比我還像新娘。我一直安慰自己丑婿,他們只是感情好性雄,可當我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著羹奉,像睡著了一般秒旋。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上诀拭,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天迁筛,我揣著相機與錄音,去河邊找鬼耕挨。 笑死细卧,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的筒占。 我是一名探鬼主播贪庙,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼翰苫!你這毒婦竟也來了止邮?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤奏窑,失蹤者是張志新(化名)和其女友劉穎导披,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體埃唯,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡撩匕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了墨叛。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片止毕。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡并村,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出滓技,到底是詐尸還是另有隱情,我是刑警寧澤棚潦,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布令漂,位于F島的核電站,受9級特大地震影響丸边,放射性物質(zhì)發(fā)生泄漏叠必。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一妹窖、第九天 我趴在偏房一處隱蔽的房頂上張望纬朝。 院中可真熱鬧,春花似錦骄呼、人聲如沸共苛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽隅茎。三九已至,卻和暖如春嫉沽,著一層夾襖步出監(jiān)牢的瞬間辟犀,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工绸硕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留堂竟,地道東北人。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓玻佩,卻偏偏與公主長得像出嘹,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子咬崔,可洞房花燭夜當晚...
    茶點故事閱讀 43,490評論 2 348

推薦閱讀更多精彩內(nèi)容