上下文管理器

2018-02-24 15:48 更新

《文件(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

這段代碼的意圖主要是:

  1. 通過__init__()能夠讀入文件名和打開模式,以使得看起來更接近open();
  2. 當(dāng)進(jìn)入語句塊時(shí),先執(zhí)行__enter__()方法,把文件打開,并返回該文件對象;
  3. 執(zhí)行代碼塊內(nèi)容,打印文件內(nèi)容;
  4. 離開代碼塊的時(shí)候,執(zhí)行__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è)變量,以便于以后使用。

contextlib模塊

Python中的這個(gè)模塊使上下文管理中非常好用的東東,這也是標(biāo)準(zhǔn)庫中的一員,不需要另外安裝了。

>>> import contextlib
>>> dir(contextlib)
    ['GeneratorContextManager', '__all__', '__builtins__', '__doc__', '__file__', '__name__', '__package__', 'closing', 'contextmanager', 'nested', 'sys', 'warn', 'wraps']

常用的是contextmangerclosingnested。

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>
|  

以上省略了部分內(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)閉。

contextlib.nested()

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

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')

不過,我感到很奇怪,我得到的圖片是這樣的:

而原文中得到的圖片是這樣的:

請讀者指正。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號