在《文件(1)》中提到,如果要打開文件,一種比較好的方法使使用with
語句,因?yàn)檫@種方法,不僅結(jié)構(gòu)簡單,更重要的是不用再單獨(dú)去判斷某種異常情況,也不用專門去執(zhí)行文件關(guān)閉的指令了。
本節(jié)對這個(gè)有點(diǎn)神奇的with
進(jìn)行深入剖析。
跟with
相關(guān)的有一些概念,需要必須澄清。
上下文管理
如果把它作為一個(gè)概念來闡述,似乎有點(diǎn)多余,因?yàn)閺淖置嫔弦部梢杂幸唤z的體會,但是,我要說的是,那點(diǎn)直覺的體會不一定等于理性的嚴(yán)格定義,特別是周遭事物越來越復(fù)雜的時(shí)候。
“上下文”的英文是context,在網(wǎng)上檢索了一下關(guān)于“上下文”的說法,發(fā)現(xiàn)沒有什么嚴(yán)格的定義,另外,不同的語言環(huán)境,對“上下文管理”有不同的說法。根據(jù)我個(gè)人的經(jīng)驗(yàn)和能看到的某些資料,我以為可以把“上下文”理解為某一些語句構(gòu)成的一個(gè)環(huán)境(也可以說使代碼塊),所謂“管理”就是要在這個(gè)環(huán)境中做一些事情,做什么事情呢?就Python而言,是要將前面某個(gè)語句(“上文”)干的事情獨(dú)立成為對象,然后在后面(“下文”)中使用這個(gè)對象來做事情。
上下文管理協(xié)議
英文是Context Management Protocol,既然使協(xié)議,就應(yīng)該是包含某些方法的東西,大家都按照這個(gè)去做(協(xié)商好了的東西)。Python中的上下文管理協(xié)議中必須包含__enter__()
和__exit__()
兩個(gè)方法。
看這個(gè)兩個(gè)方法的名字,估計(jì)讀者也能領(lǐng)悟一二了(名字不是隨便取的,這個(gè)某個(gè)島國取名字的方法不同,當(dāng)然,現(xiàn)在人家也不是隨便取了)。
上下文管理器
網(wǎng)上能夠找到的最通常的說法是:上下文管理器使支持上下文管理協(xié)議的對象,這種對象實(shí)現(xiàn)了__enter__()
和__exit__()
方法。
這個(gè)簡潔而準(zhǔn)確的定義,一般情況下一些高手使理解了。如果讀者有疑惑,就說明...,我還是要把一個(gè)高雅的定義通俗化更好一些。
在Python中,下面的語句,也存在上下文,但它們使一氣呵成執(zhí)行的。
>>> name = "laoqi"
>>> if name == "laoqi":
... print name
...
laoqi
>>> if name == "laoqi":
... for i in name:
... print i,
...
l a o q i
以上兩個(gè)例子中,“上文”進(jìn)行了判斷,然后“下文”執(zhí)行,從上而下,已經(jīng)很通暢了。還有不那么通暢的,就是下面的情況。
>>> f = open("a.txt", "w")
>>> f.write("hello")
>>> f.write("python")
>>> f.close()
在這個(gè)示例中,當(dāng)f = open("a.txt", "w")
之后,其實(shí)這句話并沒有如同前面的示例中那樣被“遺忘”,它是讓計(jì)算機(jī)運(yùn)行到一種狀態(tài)——文件始終處于打開狀態(tài)——然后在這種狀態(tài)中進(jìn)行后面的操作,直到f.close()
為止,這種狀態(tài)才結(jié)束。
在這種情況下,我們就可以使用“上下文管理器”(英文:Context Manager),用它來獲得“上文”狀態(tài)對象,然后在“下文”使用它,并在整個(gè)過程執(zhí)行完畢來收場。
更Python一點(diǎn)的說法,可以說是在某任務(wù)執(zhí)行之初,上下文管理器做好執(zhí)行準(zhǔn)備,當(dāng)任務(wù)(代碼塊)執(zhí)行完畢或者中間出現(xiàn)了異常,上下文管理器負(fù)責(zé)結(jié)束工作。
這么好的一個(gè)東西,是Python2.5以后才進(jìn)來的。
剛才那個(gè)向文件中寫入hello和python兩個(gè)單詞的示例,如果你覺得在工程中也可以這樣做,就大錯(cuò)特錯(cuò)了。因?yàn)樗嬖陔[含的問題,比如在寫入了hello之后,不知道什么原因,后面的python不能寫入了,最能說服你的是恰好遇到了“磁盤已滿”——雖然這種情況的概率可能比抓獎券還還小,但作為嚴(yán)禁的程序員,使必須要考慮的,這也是程序復(fù)雜之原因,這時(shí)候后面的操作就出現(xiàn)了異常,無法執(zhí)行,文件也不能close。解決這個(gè)問題的方法使用try ... finally ...
語句,讀者一定能寫出來。
不錯(cuò),的確解決了。
問題繼續(xù),如果要從一個(gè)文件讀內(nèi)容,寫入到另外一個(gè)文件中,下面的樣子你覺得如何?
首先建立一個(gè)文件,名稱為23501.txt,里面的內(nèi)容如下:
$ cat 23501.txt
hello laoqi
www.itdiffer.com
然后寫出下面的代碼,實(shí)現(xiàn)上述目的:
#!/usr/bin/env python
# coding=utf-8
read_file = open("23501.txt")
write_file = open("23502.txt", "w")
try:
r = read_file.readlines()
for line in r:
write_file.write(line)
finally:
read_file.close()
write_file.close()
如果你不知道“上下文管理器”,這樣做無可厚非,可偏偏現(xiàn)在已經(jīng)知道了,所以,從今以后這樣做就不是最優(yōu)的了,因?yàn)樗梢杂谩吧舷挛墓芾砥鳌睂懙母谩K?,?code>with語句改寫之后,就是很優(yōu)雅的了。
with open("23501.txt") as read_file, open("23503.txt", "w") as write_file:
for line in read_file.readlines():
write_file.write(line)
跟前面的對比一下,是不是有點(diǎn)驚嘆了?!所以,你可以理直氣壯地說“我用Python”。
可見上下文管理器是必要的,因?yàn)樗尨a優(yōu)雅了,當(dāng)然優(yōu)雅只是表象,還有更深層次的含義,繼續(xù)閱讀下面的內(nèi)容能有深入體會。
更深入
前面已經(jīng)說了,上下文管理器執(zhí)行了__enter__()
和__exit__()
方法,可是在with
語句中哪里看到了這兩個(gè)方法呢?
為了解把這個(gè)問題解釋清楚,需要先做點(diǎn)別的操作,雖然工程中一般不需要做。
#!/usr/bin/env python
# coding=utf-8
class ContextManagerOpenDemo(object):
def __enter__(self):
print "Starting the Manager."
def __exit__(self, *others):
print "Exiting the Manager."
with ContextManagerOpenDemo():
print "In the Manager."
在上面的代碼示例中,我們寫了一個(gè)類ContextManagerOpenDemo()
,你就把它理解為我自己寫的Open()
吧,當(dāng)然使最簡版本了。在這個(gè)類中,__enter__()
方法和__exit__()
方法都比較簡單,就是要檢測是否執(zhí)行該方法。
然后用with
語句來執(zhí)行,目的是按照“上下文管理器”的解釋那樣,應(yīng)該首先執(zhí)行類中的__enter__()
方法,它總是在進(jìn)入代碼塊前被調(diào)用的,接著就執(zhí)行代碼塊——with
語句下面的內(nèi)容,當(dāng)代碼塊執(zhí)行完畢,離開的時(shí)候又調(diào)用類中的__exit__()
。
檢驗(yàn)一下,是否按照上述理想路徑執(zhí)行。
$ python 23502.py
Starting the Manager.
In the Manager.
Exiting the Manager.
果然如此。執(zhí)行結(jié)果已經(jīng)基本顯示了上下文管理器的工作原理。
為了讓它更接近open()
,需要再進(jìn)一步改寫,讓它能夠接受參數(shù),以便于指定打開的文件。
#!/usr/bin/env python
# coding=utf-8
class ContextManagerOpenDemo(object):
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
def __enter__(self):
print "Starting the Manager."
self.open_file = open(self.filename, self.mode)
return self.open_file
def __exit__(self, *others):
self.open_file.close()
print "Exiting the Manager."
with ContextManagerOpenDemo("23501.txt", 'r') as reader:
print "In the Manager."
for line in reader:
print line
這段代碼的意圖主要是:
__init__()
能夠讀入文件名和打開模式,以使得看起來更接近open()
;__enter__()
方法,把文件打開,并返回該文件對象;__exit__()
方法,關(guān)閉文件。運(yùn)行結(jié)果是:
$ python 23502.py
Starting the Manager.
In the Manager.
hello laoqi
www.itdiffer.com
Exiting the Manager.
在上述代碼中,我們沒有對異常進(jìn)行處理,也就是把異常隱藏了,不管在代碼塊執(zhí)行時(shí)候遇到什么異常,都是要離開代碼塊,那么就立刻讓__exit__()
方法接管了。
如果要把異常顯現(xiàn)出來,也使可以,可以改寫__exit__()
方法。例如:
def __exit__(self, exc_type, exc_value, exc_traceback):
return False
當(dāng)代碼塊出現(xiàn)異常,則由__exit__()
負(fù)責(zé)善后清理,如果返回False,如上面的示例,則異常讓with
之外的語句邏輯來處理,這是通常使用的方法;如果返回True,意味著不對異常進(jìn)行處理。
從上面我們自己寫的類和方法中,已經(jīng)了解了上下文管理器的運(yùn)行原理了。那么,open()
跟它有什么關(guān)系嗎?
為了能清楚地查看,我們需要建立一個(gè)文件對象,并且使用dir()
來看看是否有我們所期盼的東西。
>>> f = open("a.txt")
>>> dir(f)
['__class__', '__delattr__', '__doc__', '__enter__', '__exit__', '__format__', '__getattribute__', '__hash__', '__init__', '__iter__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'closed', 'encoding', 'errors', 'fileno', 'flush', 'isatty', 'mode', 'name', 'newlines', 'next', 'read', 'readinto', 'readline', 'readlines', 'seek', 'softspace', 'tell', 'truncate', 'write', 'writelines', 'xreadlines']
讀者是否運(yùn)用你那迷迷糊糊的火眼金睛看到了兩個(gè)已經(jīng)很面熟的方法名稱了?如果你找到了,你就心知肚明了。
在with
語句中還有一個(gè)as
,雖然在上面示例中沒有顯示,但是一般我們還是不拋棄它的,它的作用就是將返回的對象付給一個(gè)變量,以便于以后使用。
Python中的這個(gè)模塊使上下文管理中非常好用的東東,這也是標(biāo)準(zhǔn)庫中的一員,不需要另外安裝了。
>>> import contextlib
>>> dir(contextlib)
['GeneratorContextManager', '__all__', '__builtins__', '__doc__', '__file__', '__name__', '__package__', 'closing', 'contextmanager', 'nested', 'sys', 'warn', 'wraps']
常用的是contextmanger
、closing
和nested
。
要想知道contextlib.closing()
的使用方法,最常用的方法就是help()
,這是我們的一貫做法,勝過查閱其它任何資料。
Help on class closing in module contextlib:
class closing(__builtin__.object)
| Context to automatically close something at the end of a block.
|
| Code like this:
|
| with closing(<module>.open(<arguments>)) as f:
| <block>
|
以上省略了部分內(nèi)容。
有一種或許常用到的情景,就是連接數(shù)據(jù)庫,并返回一個(gè)數(shù)據(jù)庫對象,在使用完之后關(guān)閉數(shù)據(jù)庫連接,其形狀如下:
with contextlib.closing(CreateDB()) as db:
db.query()
以上不是可運(yùn)行的代碼,只是一個(gè)架勢,讀者如果在編碼中使用,需要根據(jù)實(shí)際情況改寫。
當(dāng)數(shù)據(jù)庫語句db.query()
結(jié)束之后,數(shù)據(jù)庫連接自動關(guān)閉。
nested的漢語意思是“嵌套的,內(nèi)裝的”,從字面上讀者也可能理解了,這個(gè)方法跟嵌套有關(guān)。前面有一個(gè)示例,是從一個(gè)文件讀取,然后寫入到另外一個(gè)文件。我不知道讀者是否想過可以這么寫:
with open("23501.txt") as read_file:
with open("23503.txt", "w") as write_file:
for line in read_file.readlines():
write_file.write(line)
此種寫法不是不行,但是不提倡,因?yàn)樗籔ythoner了。其實(shí)這里就涉及到了嵌套,因此可以使用contextlib.nested
重。
with contextlib.nested(open("23501.txt", "r"), open("23503.txt", "w")) as (read_file, write_file):
for line in read_file.readlines():
write_file.write(line)
這是一種不錯(cuò)的寫法,當(dāng)然,在本節(jié)最前面所用到的寫法,也是可以的,只要不用剛才那種嵌套。
contextlib.contextmanager是一個(gè)裝飾器,它作用于生成器函數(shù)(也就是帶有yield的函數(shù)),一單生成器函數(shù)被裝飾以后,就返回一個(gè)上下文管理器,即contextlib.contextmanager因?yàn)檠b飾了一個(gè)生成器函數(shù)而產(chǎn)生了__enter__()
和__exit__()
方法。例如:
特別要提醒,被裝飾的生成器函數(shù)只能產(chǎn)生一個(gè)值,否則就會拋出RuntimeError異常;如果有as子句,則所產(chǎn)生的值,會通過as子句賦給某個(gè)變量,就如同前面那樣,例如下面的示例(本示例來自:http://www.ibm.com/developerworks/cn/opensource/os-cn-pythonwith/index.html)。
#!/usr/bin/env python
# coding=utf-8
from contextlib import contextmanager
@contextmanager
def demo():
print "before yield."
yield "contextmanager demo."
print "after yield."
with demo() as dd:
print "the word is: %s" % dd
運(yùn)行結(jié)果是:
$ python 23504.py
before yield.
the word is: contextmanager demo.
after yield.
為了好玩,再借用網(wǎng)上的一個(gè)示例,理解這個(gè)裝飾器的作用(下面代碼來自:http://preshing.com/20110920/the-python-with-statement-by-example/),代碼中用到了`cairo`模塊,該模塊的安裝方法是:
sudo apt-get install libcairo2-dev
如果是windows操作系統(tǒng),可以到官方網(wǎng)站下載:http://cairographics.org/
所執(zhí)行的代碼如下:
#!/usr/bin/env python
# coding=utf-8
import cairo
from contextlib import contextmanager
@contextmanager
def saved(cr):
cr.save()
try:
yield cr
finally:
cr.restore()
def tree(angle):
cr.move_to(0, 0)
cr.translate(0, -65)
cr.line_to(0, 0)
cr.stroke()
cr.scale(0.72, 0.72)
if angle > 0.72:
for a in [-angle, angle]:
with saved(cr):
cr.rotate(a)
tree(angle * 0.75)
surf = cairo.ImageSurface(cairo.FORMAT_ARGB32, 280, 204)
cr = cairo.Context(surf)
cr.translate(140, 203)
cr.set_line_width(5)
tree(0.75)
surf.write_to_png('fractal-tree.png')
不過,我感到很奇怪,我得到的圖片是這樣的:
而原文中得到的圖片是這樣的:
請讀者指正。
更多建議: