在[《文件(1)》](https://github.com/qiwsir/StarterLearningPython/blob/master/126.md)中提到,如果要打開文件,一種比較好的方法使使用`with`語句,因為這種方法,不僅結構簡單,更重要的是不用再單獨去判斷某種異常情況,也不用專門去執行文件關閉的指令了。
本節對這個有點神奇的`with`進行深入剖析。
## [](https://github.com/qiwsir/StarterLearningPython/blob/master/235.md#概念)概念
跟`with`相關的有一些概念,需要必須澄清。
**上下文管理**
如果把它作為一個概念來闡述,似乎有點多余,因為從字面上也可以有一絲的體會,但是,我要說的是,那點直覺的體會不一定等于理性的嚴格定義,特別是周遭事物越來越復雜的時候。
“上下文”的英文是context,在網上檢索了一下關于“上下文”的說法,發現沒有什么嚴格的定義,另外,不同的語言環境,對“上下文管理”有不同的說法。根據我個人的經驗和能看到的某些資料,我以為可以把“上下文”理解為某一些語句構成的一個環境(也可以說使代碼塊),所謂“管理”就是要在這個環境中做一些事情,做什么事情呢?就Python而言,是要將前面某個語句(“上文”)干的事情獨立成為對象,然后在后面(“下文”)中使用這個對象來做事情。
**上下文管理協議**
英文是Context Management Protocol,既然使協議,就應該是包含某些方法的東西,大家都按照這個去做(協商好了的東西)。Python中的上下文管理協議中必須包含`__enter__()`和`__exit__()`兩個方法。
看這個兩個方法的名字,估計讀者也能領悟一二了(名字不是隨便取的,這個某個島國取名字的方法不同,當然,現在人家也不是隨便取了)。
**上下文管理器**
網上能夠找到的最通常的說法是:上下文管理器使支持上下文管理協議的對象,這種對象實現了`__enter__()`和`__exit__()`方法。
這個簡潔而準確的定義,一般情況下一些高手使理解了。如果讀者有疑惑,就說明...,我還是要把一個高雅的定義通俗化更好一些。
在Python中,下面的語句,也存在上下文,但它們使一氣呵成執行的。
~~~
>>> name = "laoqi"
>>> if name == "laoqi":
... print name
...
laoqi
>>> if name == "laoqi":
... for i in name:
... print i,
...
l a o q i
~~~
以上兩個例子中,“上文”進行了判斷,然后“下文”執行,從上而下,已經很通暢了。還有不那么通暢的,就是下面的情況。
~~~
>>> f = open("a.txt", "w")
>>> f.write("hello")
>>> f.write("python")
>>> f.close()
~~~
在這個示例中,當`f = open("a.txt", "w")`之后,其實這句話并沒有如同前面的示例中那樣被“遺忘”,它是讓計算機運行到一種狀態——文件始終處于打開狀態——然后在這種狀態中進行后面的操作,直到`f.close()`為止,這種狀態才結束。
在這種情況下,我們就可以使用“上下文管理器”(英文:Context Manager),用它來獲得“上文”狀態對象,然后在“下文”使用它,并在整個過程執行完畢來收場。
更Python一點的說法,可以說是在某任務執行之初,上下文管理器做好執行準備,當任務(代碼塊)執行完畢或者中間出現了異常,上下文管理器負責結束工作。
這么好的一個東西,是Python2.5以后才進來的。
## [](https://github.com/qiwsir/StarterLearningPython/blob/master/235.md#必要性)必要性
剛才那個向文件中寫入hello和python兩個單詞的示例,如果你覺得在工程中也可以這樣做,就大錯特錯了。因為它存在隱含的問題,比如在寫入了hello之后,不知道什么原因,后面的python不能寫入了,最能說服你的是恰好遇到了“磁盤已滿”——雖然這種情況的概率可能比抓獎券還還小,但作為嚴禁的程序員,使必須要考慮的,這也是程序復雜之原因,這時候后面的操作就出現了異常,無法執行,文件也不能close。解決這個問題的方法使用`try ... finally ...`語句,讀者一定能寫出來。
不錯,的確解決了。
問題繼續,如果要從一個文件讀內容,寫入到另外一個文件中,下面的樣子你覺得如何?
首先建立一個文件,名稱為23501.txt,里面的內容如下:
~~~
$ cat 23501.txt
hello laoqi
www.itdiffer.com
~~~
然后寫出下面的代碼,實現上述目的:
~~~
#!/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()
~~~
如果你不知道“上下文管理器”,這樣做無可厚非,可偏偏現在已經知道了,所以,從今以后這樣做就不是最優的了,因為它可以用“上下文管理器”寫的更好。所以,用`with`語句改寫之后,就是很優雅的了。
~~~
with open("23501.txt") as read_file, open("23503.txt", "w") as write_file:
for line in read_file.readlines():
write_file.write(line)
~~~
跟前面的對比一下,是不是有點驚嘆了?!所以,你可以理直氣壯地說“我用Python”。
可見上下文管理器是必要的,因為它讓代碼優雅了,當然優雅只是表象,還有更深層次的含義,繼續閱讀下面的內容能有深入體會。
**更深入**
前面已經說了,上下文管理器執行了`__enter__()`和`__exit__()`方法,可是在`with`語句中哪里看到了這兩個方法呢?
為了解把這個問題解釋清楚,需要先做點別的操作,雖然工程中一般不需要做。
~~~
#!/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."
~~~
在上面的代碼示例中,我們寫了一個類`ContextManagerOpenDemo()`,你就把它理解為我自己寫的`Open()`吧,當然使最簡版本了。在這個類中,`__enter__()`方法和`__exit__()`方法都比較簡單,就是要檢測是否執行該方法。
然后用`with`語句來執行,目的是按照“上下文管理器”的解釋那樣,應該首先執行類中的`__enter__()`方法,它總是在進入代碼塊前被調用的,接著就執行代碼塊——`with`語句下面的內容,當代碼塊執行完畢,離開的時候又調用類中的`__exit__()`。
檢驗一下,是否按照上述理想路徑執行。
~~~
$ python 23502.py
Starting the Manager.
In the Manager.
Exiting the Manager.
~~~
果然如此。執行結果已經基本顯示了上下文管理器的工作原理。
為了讓它更接近`open()`,需要再進一步改寫,讓它能夠接受參數,以便于指定打開的文件。
~~~
#!/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
~~~
這段代碼的意圖主要是:
1. 通過`__init__()`能夠讀入文件名和打開模式,以使得看起來更接近`open()`;
2. 當進入語句塊時,先執行`__enter__()`方法,把文件打開,并返回該文件對象;
3. 執行代碼塊內容,打印文件內容;
4. 離開代碼塊的時候,執行`__exit__()`方法,關閉文件。
運行結果是:
~~~
$ python 23502.py
Starting the Manager.
In the Manager.
hello laoqi
www.itdiffer.com
Exiting the Manager.
~~~
在上述代碼中,我們沒有對異常進行處理,也就是把異常隱藏了,不管在代碼塊執行時候遇到什么異常,都是要離開代碼塊,那么就立刻讓`__exit__()`方法接管了。
如果要把異常顯現出來,也使可以,可以改寫`__exit__()`方法。例如:
~~~
def __exit__(self, exc_type, exc_value, exc_traceback):
return False
~~~
當代碼塊出現異常,則由`__exit__()`負責善后清理,如果返回False,如上面的示例,則異常讓`with`之外的語句邏輯來處理,這是通常使用的方法;如果返回True,意味著不對異常進行處理。
從上面我們自己寫的類和方法中,已經了解了上下文管理器的運行原理了。那么,`open()`跟它有什么關系嗎?
為了能清楚地查看,我們需要建立一個文件對象,并且使用`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']
~~~
讀者是否運用你那迷迷糊糊的火眼金睛看到了兩個已經很面熟的方法名稱了?如果你找到了,你就心知肚明了。
在`with`語句中還有一個`as`,雖然在上面示例中沒有顯示,但是一般我們還是不拋棄它的,它的作用就是將返回的對象付給一個變量,以便于以后使用。
## [](https://github.com/qiwsir/StarterLearningPython/blob/master/235.md#contextlib模塊)contextlib模塊
Python中的這個模塊使上下文管理中非常好用的東東,這也是標準庫中的一員,不需要另外安裝了。
~~~
>>> import contextlib
>>> dir(contextlib)
['GeneratorContextManager', '__all__', '__builtins__', '__doc__', '__file__', '__name__', '__package__', 'closing', 'contextmanager', 'nested', 'sys', 'warn', 'wraps']
~~~
常用的是`contextmanger`、`closing`和`nested`。
### [](https://github.com/qiwsir/StarterLearningPython/blob/master/235.md#contextlibclosing)contextlib.closing()
要想知道`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>
|
~~~
以上省略了部分內容。
有一種或許常用到的情景,就是連接數據庫,并返回一個數據庫對象,在使用完之后關閉數據庫連接,其形狀如下:
~~~
with contextlib.closing(CreateDB()) as db:
db.query()
~~~
以上不是可運行的代碼,只是一個架勢,讀者如果在編碼中使用,需要根據實際情況改寫。
當數據庫語句`db.query()`結束之后,數據庫連接自動關閉。
### [](https://github.com/qiwsir/StarterLearningPython/blob/master/235.md#contextlibnested)contextlib.nested()
nested的漢語意思是“嵌套的,內裝的”,從字面上讀者也可能理解了,這個方法跟嵌套有關。前面有一個示例,是從一個文件讀取,然后寫入到另外一個文件。我不知道讀者是否想過可以這么寫:
~~~
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)
~~~
此種寫法不是不行,但是不提倡,因為它太不Pythoner了。其實這里就涉及到了嵌套,因此可以使用`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)
~~~
這是一種不錯的寫法,當然,在本節最前面所用到的寫法,也是可以的,只要不用剛才那種嵌套。
### [](https://github.com/qiwsir/StarterLearningPython/blob/master/235.md#contextlibcontextmanager)contextlib.contextmanager
contextlib.contextmanager是一個裝飾器,它作用于生成器函數(也就是帶有yield的函數),一單生成器函數被裝飾以后,就返回一個上下文管理器,即contextlib.contextmanager因為裝飾了一個生成器函數而產生了`__enter__()`和`__exit__()`方法。例如:
特別要提醒,被裝飾的生成器函數只能產生一個值,否則就會拋出RuntimeError異常;如果有as子句,則所產生的值,會通過as子句賦給某個變量,就如同前面那樣,例如下面的示例(本示例來自:[http://www.ibm.com/developerworks/cn/opensource/os-cn-pythonwith/index.html)。](http://www.ibm.com/developerworks/cn/opensource/os-cn-pythonwith/index.html%EF%BC%89%E3%80%82)
~~~
#!/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
~~~
運行結果是:
~~~
$ python 23504.py
before yield.
the word is: contextmanager demo.
after yield.
~~~
為了好玩,再借用網上的一個示例,理解這個裝飾器的作用(下面代碼來自:[http://preshing.com/20110920/the-python-with-statement-by-example/),代碼中用到了`cairo`模塊,該模塊的安裝方法是:](http://preshing.com/20110920/the-python-with-statement-by-example/%EF%BC%89%EF%BC%8C%E4%BB%A3%E7%A0%81%E4%B8%AD%E7%94%A8%E5%88%B0%E4%BA%86%60cairo%60%E6%A8%A1%E5%9D%97%EF%BC%8C%E8%AF%A5%E6%A8%A1%E5%9D%97%E7%9A%84%E5%AE%89%E8%A3%85%E6%96%B9%E6%B3%95%E6%98%AF%EF%BC%9A)
~~~
sudo apt-get install libcairo2-dev
~~~
如果是windows操作系統,可以到官方網站下載:[http://cairographics.org/](http://cairographics.org/)
所執行的代碼如下:
~~~
#!/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')
~~~
不過,我感到很奇怪,我得到的圖片是這樣的:
[](https://github.com/qiwsir/StarterLearningPython/blob/master/2code/fractal-tree.png)
而原文中得到的圖片是這樣的:
[](https://box.kancloud.cn/2015-09-07_55ed42db34283.jpg)
請讀者指正。
- 第零章 預備
- 關于Python的故事
- 從小工到專家
- Python安裝
- 集成開發環境
- 第壹章 基本數據類型
- 數和四則運算
- 除法
- 常用數學函數和運算優先級
- 寫一個簡單的程序
- 字符串(1)
- 字符串(2)
- 字符串(3)
- 字符串(4)
- 字符編碼
- 列表(1)
- 列表(2)
- 列表(3)
- 回顧list和str
- 元組
- 字典(1)
- 字典(2)
- 集合(1)
- 集合(2)
- 第貳章 語句和文件
- 運算符
- 語句(1)
- 語句(2)
- 語句(3)
- 語句(4)
- 語句(5)
- 文件(1)
- 文件(2)
- 迭代
- 練習
- 自省
- 第叁章 函數
- 函數(1)
- 函數(2)
- 函數(3)
- 函數(4)
- 函數練習
- 第肆章 類
- 類(1)
- 類(2)
- 類(3)
- 類(4)
- 類(5)
- 多態和封裝
- 特殊方法(1)
- 特殊方法(2)
- 迭代器
- 生成器
- 上下文管理器
- 第伍章 錯誤和異常
- 錯誤和異常(1)
- 錯誤和異常(2)
- 錯誤和異常(3)
- 第陸章 模塊
- 編寫模塊
- 標準庫(1)
- 標準庫(2)
- 標準庫(3)
- 標準庫(4)
- 標準庫(5)
- 標準庫(6)
- 標準庫(7)
- 標準庫(8)
- 第三方庫
- 第柒章 保存數據
- 將數據存入文件
- mysql數據庫(1)
- MySQL數據庫(2)
- mongodb數據庫(1)
- SQLite數據庫
- 電子表格
- 第捌章 用Tornado做網站
- 為做網站而準備
- 分析Hello
- 用tornado做網站(1)
- 用tornado做網站(2)
- 用tornado做網站(3)
- 用tornado做網站(4)
- 用tornado做網站(5)
- 用tornado做網站(6)
- 用tornado做網站(7)
- 第玖章 科學計算
- 為計算做準備
- Pandas使用(1)
- Pandas使用(2)
- 處理股票數據
- 附:網絡文摘
- 如何成為Python高手
- ASCII、Unicode、GBK和UTF-8字符編碼的區別聯系