Python上下文管理器與with語(yǔ)句

什么是上下文管理器

上下文管理器顧名思義是管理上下文的,也就是負(fù)責(zé)沖鋒和墊后,而讓主人專心完成自己的事情。我們?cè)诰帉懗绦虻臅r(shí)候,通常會(huì)將一系列操作放到一個(gè)語(yǔ)句塊中,當(dāng)某一條件為真時(shí)執(zhí)行該語(yǔ)句快。有時(shí)候,我們需要再執(zhí)行一個(gè)語(yǔ)句塊時(shí)保持某種狀態(tài),并且在離開語(yǔ)句塊后結(jié)束這種狀態(tài)。例如對(duì)文件的操作,我們?cè)诖蜷_一個(gè)文件進(jìn)行讀寫操作時(shí)需要保持文件處于打開狀態(tài),而等操作完成之后要將文件關(guān)閉。所以,上下文管理器的任務(wù)是:代碼塊執(zhí)行前準(zhǔn)備,代碼塊執(zhí)行后收拾。上下文管理器是在Python2.5加入的功能,它能夠讓你的代碼可讀性更強(qiáng)并且錯(cuò)誤更少。

需求的產(chǎn)生

在正常的管理各種系統(tǒng)資源(文件、鎖定和連接),在涉及到異常時(shí)通常是個(gè)棘手的問(wèn)題。異常很可能導(dǎo)致控制流跳過(guò)負(fù)責(zé)釋放關(guān)鍵資源的語(yǔ)句。例如打開一個(gè)文件進(jìn)行操作時(shí),如果意外情況發(fā)生(磁盤已滿、特殊的終端信號(hào)讓其終止等),就會(huì)拋出異常,這樣可能最后的文件關(guān)閉操作就不會(huì)執(zhí)行。如果這樣的問(wèn)題頻繁出現(xiàn),則可能耗盡系統(tǒng)資源。

是的,這樣的問(wèn)題并不是不可避免。在沒有接觸到上下文管理器之前,我們可以用“try/finally”語(yǔ)句來(lái)解決這樣的問(wèn)題?;蛟S在有些人看來(lái),“try/finally”語(yǔ)句顯得有些繁瑣。上下文管理器就是被設(shè)計(jì)用來(lái)簡(jiǎn)化“try/finally”語(yǔ)句的,這樣可以讓程序更加簡(jiǎn)潔。

With語(yǔ)句

With語(yǔ)句用于執(zhí)行上下文操作,它也是復(fù)合語(yǔ)句的一種,其基本語(yǔ)法如下所示:

with context_expr [as var]:
    with_suite

With 語(yǔ)句僅能工作于支持上下文管理協(xié)議(context management protocol)的對(duì)象。也就是說(shuō)只有內(nèi)建了"上下文管理"的對(duì)象才能和 with 一起工作。Python內(nèi)置了一些支持該協(xié)議的對(duì)象,如下所列是一個(gè)簡(jiǎn)短列表:

  • file
  • decimal.Context
  • thread.LockType
  • threading.Lock
  • threading.RLock
  • threading.Condition
  • threading.Semaphore
  • threading.BoundedSemaphore

由以上列表可以看出,file 是已經(jīng)內(nèi)置了對(duì)上下文管理協(xié)議的支持。所以我們可以用下邊的方法來(lái)操作文件:

with open('/etc/passwd', 'r') as f:
    for eachLine in f:
        # ...do stuff with eachLine or f...

上邊的代碼試圖打開一個(gè)文件,如果一切正常,把文件對(duì)象賦值給 f。然后用迭代器遍歷文件中的每一行,當(dāng) 完成時(shí),關(guān)閉文件。無(wú)論是在這一段代碼的開始,中間,還是結(jié)束時(shí)發(fā)生異常,會(huì)執(zhí)行清理的代碼,此 外文件仍會(huì)被自動(dòng)的關(guān)閉。

自定義上下文管理器

要實(shí)現(xiàn)上下文管理器,必須實(shí)現(xiàn)兩個(gè)方法:一個(gè)負(fù)責(zé)進(jìn)入語(yǔ)句塊的準(zhǔn)備操作,另一個(gè)負(fù)責(zé)離開語(yǔ)句塊的善后操作。Python類包含兩個(gè)特殊的方法,分別名為:__enter____exit__。

  • enter: 該方法進(jìn)入運(yùn)行時(shí)上下文環(huán)境,并返回自身或另一個(gè)與運(yùn)行時(shí)上下文相關(guān)的對(duì)象。返回值會(huì)賦給 as 從句后面的變量,as 從句是可選的。
  • exit: 該方法退出當(dāng)前運(yùn)行時(shí)上下文并返回一個(gè)布爾值,該布爾值標(biāo)明了“如果 with_suit 的退出是由異常引發(fā)的,該異常是否須要被忽略”。如果 exit() 的返回值等于 False,那么這個(gè)異常將被重新引發(fā)一次;如果 exit() 的返回值等于 True,那么這個(gè)異常就被無(wú)視掉,繼續(xù)執(zhí)行后面的代碼。

With 語(yǔ)句的實(shí)際執(zhí)行流程是這樣的:

1. 執(zhí)行 context_exp 以獲取上下文管理器
2. 加載上下文管理器的 exit() 方法以備稍后調(diào)用
3. 調(diào)用上下文管理器的 enter() 方法
4. 如果有 as var 從句,則將 enter() 方法的返回值賦給 var
5. 執(zhí)行子代碼塊 with_suit
6. 調(diào)用上下文管理器的 exit() 方法,如果 with_suit 的退出是由異常引發(fā)的,那么該異常的 type、value 和 traceback 會(huì)作為參數(shù)傳給 exit(),否則傳三個(gè) None
7. 如果 with_suit 的退出由異常引發(fā),并且 exit() 的返回值等于 False,那么這個(gè)異常將被重新引發(fā)一次;如果 exit() 的返回值等于 True,那么這個(gè)異常就被無(wú)視掉,繼續(xù)執(zhí)行后面的代碼

下面我們自己來(lái)實(shí)現(xiàn)一個(gè)支持上下文管理協(xié)議的文件操作:

#! /usr/bin/env python
# -*- coding: utf-8 -*-

# *************************************************************
#     Filename @  contextfile.py
#       Author @  Huoty
#  Create date @  2015-08-08 17:02:13
#  Description @  
# *************************************************************

filename = 'my_file.txt'
mode = 'w' # Mode that allows to write to the file
writer = open(filename, mode)

class PypixOpen(object):
    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()

# Script starts from here

with PypixOpen(filename, mode) as writer:
    writer.write("Hello World from our new Context Manager!")

更加優(yōu)雅的上下文管理(contextlib模塊)

contextlib模塊提供更易用的上下文管理器。

contextlib.closing

contextlib.closing 方法在語(yǔ)句塊結(jié)束后調(diào)用對(duì)象的 close 方法。

from contextlib import closing
import urllib

with closing(urllib.urlopen('http://www.python.org')) as page:
    for line in page:
        print line

contextlib.nested

contextlib.nested 方法用于替換嵌套的 with 語(yǔ)句。例如,有兩個(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)行優(yōu)化:

with contextlib.nested(open('fileToRead.txt', 'r'), \
           open('fileToWrite.txt', 'w')) as (reader, writer):
    writer.write(reader.read())

contextlib.contextmanager

contextlib.contextmanager 是一個(gè)裝飾器,它可以用來(lái)裝飾被 yield 語(yǔ)句分割成兩部分的函數(shù),以此進(jìn)行上下文管理。任何在yield之前的內(nèi)容都可以看做在代碼塊執(zhí)行前的操作,而任何yield之后的操作都可以看做是代碼塊結(jié)束后要做的操作。如果希望在上下文管理器中使用 “as” 關(guān)鍵字,那么就用 yield 返回你需要的值,它將通過(guò) as 關(guān)鍵字賦值給新的變量。

from contextlib import contextmanager

@contextmanager
def tag(name):
    print "<%s>" % name
    yield
    print "</%s>" % name

使用 contextlib.contextmanager 時(shí),可以大致套用如下的框架:

from contextlib import contextmanager

@contextmanager
def closing(thing):
    try:
        yield thing
    finally:
        thing.close()
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容