理解Python中的With...as...語法

使用語言的好特性,而不是那些糟糕的特性————不知道誰說的

好久不學習python的語法了,上次去面試,和面試官聊到了python中的with-as statement(也稱context manager),挺感興趣的,這兩天學習了一番,收穫頗豐在此分享。

先說明一個常見問題,文件打開:

1
2
3
4
5
6
7
try:
    f = open('xxx')
    do something
except:
    do something
finally:
    f.close()

其實我個人不止一次在網上看到有這麼寫的了,這個是錯的。
首先正確的如下:

1
2
3
4
5
6
7
8
9
10
11
try:
    f = open('xxx')
except:
    print 'fail to open'
    exit(-1)
try:
    do something
except:
    do something
finally:
    f.close()

很麻煩不是麼,但正確的方法就是這麼寫。
我們爲什麼要寫finally,是因爲防止程序拋出異常最後不能關閉文件,但是需要關閉文件有一個前提就是文件已經打開了。
在第一段錯誤代碼中,如果異常發生在f=open(‘xxx’)的時候,比如文件不存在,立馬就可以知道執行f.close()是沒有意義的。改正後的解決方案就是第二段代碼。

好了言歸正轉,開始討論with語法。

首先我們從下面這個問題談起,try-finally的語法結構:

1
2
3
4
5
set things up
try:
    do something
finally:
    tear things down

這東西是個常見結構,比如文件打開,set things up就表示f=open('xxx')tear things down就表示f.close()。在比如像多線程鎖,資源請求,最終都有一個釋放的需求。Try…finally結構保證了tear things down這一段永遠都會執行,即使上面do something得工作沒有完全執行。

如果經常用這種結構,我們首先可以採取一個較爲優雅的辦法,封裝!

1
2
3
4
5
6
7
8
9
10
11
def controlled_execution(callback):
    set things up
    try:
        callback(thing)
    finally:
        tear things down
 
def my_function(thing):
    do something
 
controlled_execution(my_function)

封裝是一個支持代碼重用的好辦法,但是這個辦法很dirty,特別是當do something中有修改一些local variables的時候(變成函數調用,少不了帶來變量作用域上的麻煩)。

另一個辦法是使用生成器,但是只需要生成一次數據,我們用for-in結構去調用他:

1
2
3
4
5
6
7
8
9
def controlled_execution():
    set things up
    try:
        yield thing
    finally:
        tear things down
         
for thing in controlled_execution():
    do something with thing

因爲thing只有一個,所以yield語句只需要執行一次。當然,從代碼可讀性也就是優雅的角度來說這簡直是糟糕透了。我們在確定for循環只執行一次的情況下依然使用了for循環,這代碼給不知道的人看一定很難理解這裏的循環是什麼個道理。

最終的python-dev團隊的解決方案。(python 2.5以後增加了with表達式的語法)

1
2
3
4
5
6
7
8
9
class controlled_execution:
    def __enter__(self):
        set things up
        return thing
    def __exit__(self, type, value, traceback):
        tear things down
         
with controlled_execution() as thing:
        do something

在這裏,python使用了with-as的語法。當python執行這一句時,會調用__enter__函數,然後把該函數return的值傳給as後指定的變量。之後,python會執行下面do something的語句塊。最後不論在該語句塊出現了什麼異常,都會在離開時執行__exit__。
另外,__exit__除了用於tear things down,還可以進行異常的監控和處理,注意後幾個參數。要跳過一個異常,只需要返回該函數True即可。下面的樣例代碼跳過了所有的TypeError,而讓其他異常正常拋出。

1
2
def __exit__(self, type, value, traceback):
    return isinstance(value, TypeError)

在python2.5及以後,file對象已經寫好了__enter__和__exit__函數,我們可以這樣測試:

1
2
3
4
5
6
7
8
9
10
11
12
>>> f = open("x.txt")
>>> f
<open file 'x.txt', mode 'r' at 0x00AE82F0>
>>> f.__enter__()
<open file 'x.txt', mode 'r' at 0x00AE82F0>
>>> f.read(1)
'X'
>>> f.__exit__(None, None, None)
>>> f.read(1)
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
ValueError: I/O operation on closed file

之後,我們如果要打開文件並保證最後關閉他,只需要這麼做:

1
2
3
with open("x.txt") as f:
    data = f.read()
    do something with data

如果有多個項,我們可以這麼寫:

1
2
with open("x.txt") as f1, open('xxx.txt') as f2:
    do something with f1,f2

上文說了__exit__函數可以進行部分異常的處理,如果我們不在這個函數中處理異常,他會正常拋出,這時候我們可以這樣寫(python 2.7及以上版本,之前的版本參考使用contextlib.nested這個庫函數):

1
2
3
4
5
try:
    with open( "a.txt" ) as f :
        do something
except xxxError:
    do something about exception

總之,with-as表達式極大的簡化了每次寫finally的工作,這對保持代碼的優雅性是有極大幫助的。

感謝以下參考資料:
stackoverflow: Catching an exception while using a Python ‘with’ statement
Understanding Python’s “with” statement
python docs:
http://docs.python.org/2/reference/compound_stmts.html#with
http://docs.python.org/2/reference/datamodel.html#context-managers
http://docs.python.org/2/library/contextlib.html#contextlib.nested

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章